From d1c652cef06c88e32371a98cbde578671e2bca25 Mon Sep 17 00:00:00 2001 From: Gessmass Date: Thu, 12 Dec 2024 10:18:13 +0100 Subject: [PATCH] File preview in visitor download ok --- files/package.json | 1 + files/src/controllers/filesController.ts | 254 ++++++++++++--------- files/src/router/index.ts | 6 +- frontend/src/pages/VisitorDownloadPage.tsx | 153 +++++++------ 4 files changed, 233 insertions(+), 181 deletions(-) diff --git a/files/package.json b/files/package.json index 59a33a3..9be8a4a 100644 --- a/files/package.json +++ b/files/package.json @@ -23,6 +23,7 @@ "express": "^4.18.2", "graphql": "^16.9.0", "jsonwebtoken": "^9.0.2", + "mime": "^4.0.4", "multer": "^1.4.5-lts.1", "ts-node-dev": "^2.0.0", "uuid": "^11.0.2" diff --git a/files/src/controllers/filesController.ts b/files/src/controllers/filesController.ts index 44b5a4a..a756add 100644 --- a/files/src/controllers/filesController.ts +++ b/files/src/controllers/filesController.ts @@ -25,90 +25,90 @@ if (!fs.existsSync(FINAL_DIR)) { } export const storage = multer.diskStorage({ - destination: (req, file, cb) => { - cb(null, TEMP_DIR); - }, - filename: (req, file, cb) => { - const fileUuid = uuidv4(); - const fileExtension = path.extname(file.originalname); - const newFilename = `${fileUuid}${fileExtension}`; - - (file as any).original_name = file.originalname; - - cb(null, newFilename); - }, + destination: (req, file, cb) => { + cb(null, TEMP_DIR); + }, + filename: (req, file, cb) => { + const fileUuid = uuidv4(); + const fileExtension = path.extname(file.originalname); + const newFilename = `${fileUuid}${fileExtension}`; + + (file as any).original_name = file.originalname; + + cb(null, newFilename); + }, }); const upload = multer({storage}).array("files", 10); // This function works but it's WAY too long, we will need to refactor it someday export const addNewUpload = async (req: Request, res: any) => { - upload(req, res, async (err) => { - if (err) { - return res.status(500).send("Error uploading files."); - } - - if (!req.files || req.files.length === 0) { - return res.status(400).send("No files uploaded."); - } - - const filesArray = req.files as Express.Multer.File[]; - const validFiles = []; - - for (const file of filesArray) { - const fileUuid = uuidv4(); - const fileExtension = path.extname(file.originalname); - const fileFinalName = `${fileUuid}${fileExtension}`; - const tempPath = file.path; - const finalPath = path.join(FINAL_DIR, fileFinalName); - - const isValid = await validateFile(file, tempPath); - if (isValid) { - fs.renameSync(tempPath, finalPath); - validFiles.push({ - original_name: (file as any).original_name || file.originalname, - default_name: fileFinalName, - path: finalPath, - size: file.size, - uuid: fileUuid, + upload(req, res, async (err) => { + if (err) { + return res.status(500).send("Error uploading files."); + } + + if (!req.files || req.files.length === 0) { + return res.status(400).send("No files uploaded."); + } + + const filesArray = req.files as Express.Multer.File[]; + const validFiles = []; + + for (const file of filesArray) { + const fileUuid = uuidv4(); + const fileExtension = path.extname(file.originalname); + const fileFinalName = `${fileUuid}${fileExtension}`; + const tempPath = file.path; + const finalPath = path.join(FINAL_DIR, fileFinalName); + + const isValid = await validateFile(file, tempPath); + if (isValid) { + fs.renameSync(tempPath, finalPath); + validFiles.push({ + original_name: (file as any).original_name || file.originalname, + default_name: fileFinalName, + path: finalPath, + size: file.size, + uuid: fileUuid, mimetype: file.mimetype, - }); - } else { - fs.unlinkSync(tempPath); - } - } - - if (validFiles.length > 0) { - axios.post("http://backend:4000/graphql", { - query: ADD_ONE_UPLOAD, - variables: { - senderEmail: req.body.senderEmail, - receiversEmails: Array.isArray(req.body.receiversEmails) - ? req.body.receiversEmails - : [req.body.receiversEmails], - title: req.body.title || "Default Title", - message: req.body.message || "Default Message", - fileData: JSON.stringify(validFiles), - }, - }) - .then((response) => { - console.log("DATA:", response.data); - res.status(200).json(response.data); - }) - .catch((err) => { - if (err.response && err.response.data) { - console.error("GraphQL Errors:", err.response.data.errors); - } else { - console.error("Unexpected Error:", err); - } - res.status(500).send("Error during file upload mutation."); - }); - } else { - res.status(400).send("No valid files uploaded."); - } - }); + }); + } else { + fs.unlinkSync(tempPath); + } + } + + if (validFiles.length > 0) { + axios.post("http://backend:4000/graphql", { + query: ADD_ONE_UPLOAD, + variables: { + senderEmail: req.body.senderEmail, + receiversEmails: Array.isArray(req.body.receiversEmails) + ? req.body.receiversEmails + : [req.body.receiversEmails], + title: req.body.title || "Default Title", + message: req.body.message || "Default Message", + fileData: JSON.stringify(validFiles), + }, + }) + .then((response) => { + console.log("DATA:", response.data); + res.status(200).json(response.data); + }) + .catch((err) => { + if (err.response && err.response.data) { + console.error("GraphQL Errors:", err.response.data.errors); + } else { + console.error("Unexpected Error:", err); + } + res.status(500).send("Error during file upload mutation."); + }); + } else { + res.status(400).send("No valid files uploaded."); + } + }); }; - + export const deleteFile = async (req: Request, res: any) => { const filename = req.query.filename @@ -130,39 +130,77 @@ export const deleteFile = async (req: Request, res: any) => { } export const downloadFiles = async (req: Request, res: any) => { - const FILES_DIR = path.join(__dirname, "../uploads/final"); - const files = req.body.files as string[]; - - if (!files || files.length === 0) { - return res.status(400).send("No files provided."); - } - - res.setHeader("Content-Type", "application/zip"); - res.attachment("files.zip"); - - const archive = archiver("zip", { zlib: { level: 9 } }); - - archive.on("error", (err) => { - res.status(500).send("Error creating ZIP archive."); - }); - - archive.pipe(res); - - for (const file of files) { - const filePath = path.join(FILES_DIR, file); - if (fs.existsSync(filePath)) { - archive.file(filePath, { name: file }); - } else { - console.warn(`File not found: ${file}`); - } - } - - try { - await archive.finalize(); - } catch (err) { - res.status(500).send("Error finalizing ZIP archive."); - } + const FILES_DIR = path.join(__dirname, "../uploads/final"); + const files = req.body.files as string[]; + + if (!files || files.length === 0) { + return res.status(400).send("No files provided."); + } + + res.setHeader("Content-Type", "application/zip"); + res.attachment("files.zip"); + + const archive = archiver("zip", {zlib: {level: 9}}); + + archive.on("error", (err) => { + res.status(500).send("Error creating ZIP archive."); + }); + + archive.pipe(res); + + for (const file of files) { + const filePath = path.join(FILES_DIR, file); + if (fs.existsSync(filePath)) { + archive.file(filePath, {name: file}); + } else { + console.warn(`File not found: ${file}`); + } + } + + try { + await archive.finalize(); + } catch (err) { + res.status(500).send("Error finalizing ZIP archive."); + } }; +export const getOneFile = (req: Request, res: any) => { + try { + const {fileDefaultName} = req.body; + + console.log(fileDefaultName) + + if (!fileDefaultName) { + return res.status(400).send("File path is required."); + } + + const fullPath = path.join(FINAL_DIR, fileDefaultName); + + console.log(fullPath) + + if (!fs.existsSync(fullPath)) { + return res.status(404).send("File not found."); + } + + res.setHeader("Content-Type", "application/octet-stream"); + res.setHeader( + "Content-Disposition", + `inline; filename="${path.basename(fullPath)}"` + ); + + const fileStream = fs.createReadStream(fullPath); + fileStream.pipe(res); + + fileStream.on("error", (err) => { + console.error("Error streaming file:", err); + res.status(500).send("Error sending file."); + }); + } catch (err) { + console.error(err); + res.status(500).send("Error fetching file for preview."); + } +}; + + diff --git a/files/src/router/index.ts b/files/src/router/index.ts index b7ffcdf..b8c84bf 100644 --- a/files/src/router/index.ts +++ b/files/src/router/index.ts @@ -1,12 +1,14 @@ import express from "express"; -import {addNewUpload, deleteFile, downloadFiles} from "../controllers/filesController"; +import {addNewUpload, deleteFile, downloadFiles, getOneFile} from "../controllers/filesController"; const router = express.Router() +router.post('/get-one', getOneFile) router.post('/upload', addNewUpload) +router.post('/download', downloadFiles) router.delete('/delete', deleteFile) +// blob:http://localhost:7002/files/f6b04590-02f1-45db-a7e7-ec9ee352ef4f -router.post('/download', downloadFiles) export default router \ No newline at end of file diff --git a/frontend/src/pages/VisitorDownloadPage.tsx b/frontend/src/pages/VisitorDownloadPage.tsx index 562531c..43c589f 100644 --- a/frontend/src/pages/VisitorDownloadPage.tsx +++ b/frontend/src/pages/VisitorDownloadPage.tsx @@ -1,10 +1,10 @@ import styled from "@emotion/styled"; -import { Button, Card, Table, notification } from "antd"; -import { DownloadOutlined } from "@ant-design/icons"; -import { useSearchParams } from "react-router-dom"; -import { ApolloError, useMutation } from "@apollo/client"; -import { GET_FILES_FROM_UPLOAD } from "../graphql/mutations"; -import { useEffect, useState } from "react"; +import {Button, Card, Modal, notification, Table} from "antd"; +import {DownloadOutlined} from "@ant-design/icons"; +import {useSearchParams} from "react-router-dom"; +import {ApolloError, useMutation} from "@apollo/client"; +import {GET_FILES_FROM_UPLOAD} from "../graphql/mutations"; +import {useEffect, useState} from "react"; import axios from 'axios' interface FileData { @@ -22,9 +22,10 @@ const VisitorDownloadPage = () => { const [notifApi, contextHolder] = notification.useNotification(); const [searchParams] = useSearchParams(); const [files, setFiles] = useState([]); + const [filePreview, setFilePreview] = useState(null) const token = searchParams.get("token"); - const [getFiles, { loading, error, data }] = useMutation( + const [getFiles, {loading, error, data}] = useMutation( GET_FILES_FROM_UPLOAD, { onError: (error: ApolloError) => { @@ -39,16 +40,10 @@ const VisitorDownloadPage = () => { useEffect(() => { if (token) { - getFiles({ variables: { token } }).then((response) => { + getFiles({variables: {token}}).then((response) => { const fetchedFiles = response.data.getFilesFromUpload.map( (file: FileData, index: number) => ({ - key: index.toString(), - name: file.name, - size: file.size, - created_at: file.created_at, - default_name: file.default_name, - sent: "Just now", - action: "Preview", + ...file, url: `http://localhost:7002/access/download?token=${token}&fileId=${file.id}`, }) ); @@ -66,18 +61,18 @@ const VisitorDownloadPage = () => { const now = new Date(); const targetDate = new Date(date); const diffMs = now.getTime() - targetDate.getTime(); - + if (diffMs < 0) { return "Sent just now"; } - + const diffSeconds = Math.floor(diffMs / 1000); const diffMinutes = Math.floor(diffSeconds / 60); const diffHours = Math.floor(diffMinutes / 60); const diffDays = Math.floor(diffHours / 24); - + let result = "Sent "; - + if (diffDays > 0) { result += `${diffDays} day${diffDays > 1 ? "s" : ""}`; if (diffHours % 24 > 0) { @@ -94,10 +89,10 @@ const VisitorDownloadPage = () => { result += "just now"; return result; } - + return result + " ago"; }; - + const formatSizeInMB = (sizeInBytes: string) => { const size = parseInt(sizeInBytes, 10); @@ -109,23 +104,35 @@ const VisitorDownloadPage = () => { try { const response = await axios.post( "http://localhost:7002/files/download", - { files: files.map((file) => file.default_name) }, - { responseType: "blob" } + {files: files.map((file) => file.default_name)}, + {responseType: "blob"} ); - - const blob = new Blob([response.data], { type: "application/zip" }); - + + const blob = new Blob([response.data], {type: "application/zip"}); + const link = document.createElement("a"); link.href = URL.createObjectURL(blob); link.download = "files.zip"; link.click(); - + URL.revokeObjectURL(link.href); } catch (e) { console.error("Error downloading files:", e); } }; + const handlePreviewFile = async (fileDefaultName) => { + await axios.post(`http://localhost:7002/files/get-one`, + {fileDefaultName}, + {responseType: 'blob'} + ).then((res) => { + console.log(res.data) + + const fileUrl = URL.createObjectURL(res.data) + setFilePreview(fileUrl) + }).catch(err => console.error(err)) + } + const columns = [ { title: "Name", @@ -133,7 +140,7 @@ const VisitorDownloadPage = () => { key: "name", render: (text: string) => ( - {text} + {text} ), }, @@ -153,14 +160,15 @@ const VisitorDownloadPage = () => { title: "Action", dataIndex: "action", key: "action", - render: (text: string) => {text}, + render: (text: string, file) => handlePreviewFile(file.default_name)}>preview, }, ]; return ( @@ -179,83 +187,86 @@ const VisitorDownloadPage = () => { pagination={false} showHeader={true} bordered={false} - style={{ marginBottom: "20px" }} + style={{marginBottom: "20px"}} /> + setFilePreview(null)}> + Bonjour + ); }; // Styles pour les icĂ´nes et boutons const FileIcon = styled.span` - display: inline-block; - width: 20px; - height: 20px; - background-color: #ddd; - margin-right: 10px; - border-radius: 4px; + display: inline-block; + width: 20px; + height: 20px; + background-color: #ddd; + margin-right: 10px; + border-radius: 4px; `; const PreviewLink = styled.a` - color: #7f58ff; - text-decoration: underline; + color: #7f58ff; + text-decoration: underline; `; const TitleContainer = styled.div` - display: flex; - justify-content: space-between; + display: flex; + justify-content: space-between; `; const TextContainer = styled.div` - display: flex; - flex-direction: column; - align-items: center; - flex-grow: 1; - line-height: 1.4; + display: flex; + flex-direction: column; + align-items: center; + flex-grow: 1; + line-height: 1.4; - h3 { - margin: 0; - font-size: 18px; - } + h3 { + margin: 0; + font-size: 18px; + } - p { - margin: 0; - font-size: 16px; - color: #555; - } + p { + margin: 0; + font-size: 16px; + color: #555; + } `; const ExpirationText = styled.span` - font-size: 12px; - color: #888; - white-space: nowrap; - flex-shrink: 0; + font-size: 12px; + color: #888; + white-space: nowrap; + flex-shrink: 0; `; const ButtonContainer = styled.div` - display: flex; - justify-content: center; - margin-top: 20px; + display: flex; + justify-content: center; + margin-top: 20px; `; const VisitorDownloadWrapper = styled.div` - position: relative; - width: 100%; - height: 100%; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - margin-top: 40px; + position: relative; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + margin-top: 40px; `; export default VisitorDownloadPage;