diff --git a/files/src/controllers/filesController.ts b/files/src/controllers/filesController.ts index a756add..cebd0b7 100644 --- a/files/src/controllers/filesController.ts +++ b/files/src/controllers/filesController.ts @@ -1,11 +1,11 @@ import fs from "fs"; import path from "path"; import multer from "multer"; -import {validateFile} from "../validators/fileValidators"; -import {ADD_ONE_UPLOAD} from "../graphql/mutations"; -import {Request} from "express"; +import { validateFile } from "../validators/fileValidators"; +import { ADD_ONE_UPLOAD } from "../graphql/mutations"; +import { Request } from "express"; import axios from "axios"; -import {v4 as uuidv4} from "uuid"; +import { v4 as uuidv4 } from "uuid"; import archiver from "archiver"; const UPLOADS_DIR = path.join(__dirname, "../uploads"); @@ -13,15 +13,15 @@ const TEMP_DIR = path.resolve(UPLOADS_DIR, "temp"); const FINAL_DIR = path.resolve(UPLOADS_DIR, "final"); if (!fs.existsSync(UPLOADS_DIR)) { - fs.mkdirSync(UPLOADS_DIR, {recursive: true}); + fs.mkdirSync(UPLOADS_DIR, { recursive: true }); } if (!fs.existsSync(TEMP_DIR)) { - fs.mkdirSync(TEMP_DIR, {recursive: true}); + fs.mkdirSync(TEMP_DIR, { recursive: true }); } if (!fs.existsSync(FINAL_DIR)) { - fs.mkdirSync(FINAL_DIR, {recursive: true}); + fs.mkdirSync(FINAL_DIR, { recursive: true }); } export const storage = multer.diskStorage({ @@ -31,7 +31,7 @@ export const storage = multer.diskStorage({ filename: (req, file, cb) => { const fileUuid = uuidv4(); const fileExtension = path.extname(file.originalname); - const newFilename = `${fileUuid}${fileExtension}`; + const newFilename = ${fileUuid}${fileExtension}; (file as any).original_name = file.originalname; @@ -39,7 +39,7 @@ export const storage = multer.diskStorage({ }, }); -const upload = multer({storage}).array("files", 10); +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) => { @@ -58,7 +58,7 @@ export const addNewUpload = async (req: Request, res: any) => { for (const file of filesArray) { const fileUuid = uuidv4(); const fileExtension = path.extname(file.originalname); - const fileFinalName = `${fileUuid}${fileExtension}`; + const fileFinalName = ${fileUuid}${fileExtension}; const tempPath = file.path; const finalPath = path.join(FINAL_DIR, fileFinalName); @@ -66,7 +66,8 @@ export const addNewUpload = async (req: Request, res: any) => { if (isValid) { fs.renameSync(tempPath, finalPath); validFiles.push({ - original_name: (file as any).original_name || file.originalname, + original_name: + (file as any).original_name || file.originalname, default_name: fileFinalName, path: finalPath, size: file.size, @@ -79,25 +80,29 @@ export const addNewUpload = async (req: Request, res: any) => { } 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), - }, - }) + 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); + console.error( + "GraphQL Errors:", + err.response.data.errors + ); } else { console.error("Unexpected Error:", err); } @@ -109,9 +114,8 @@ export const addNewUpload = async (req: Request, res: any) => { }); }; - export const deleteFile = async (req: Request, res: any) => { - const filename = req.query.filename + const filename = req.query.filename; if (!filename) { return res.status(400).send("No filename provided."); @@ -120,14 +124,14 @@ export const deleteFile = async (req: Request, res: any) => { const filePath = path.join(FINAL_DIR, filename as string); if (!fs.existsSync(filePath)) { - return res.status(404).send(`File not found.`); + return res.status(404).send(File not found.); } // fs.unlinkSync deletes the file fs.unlinkSync(filePath); return res.status(200).send("File deleted."); -} +}; export const downloadFiles = async (req: Request, res: any) => { const FILES_DIR = path.join(__dirname, "../uploads/final"); @@ -140,7 +144,7 @@ export const downloadFiles = async (req: Request, res: any) => { res.setHeader("Content-Type", "application/zip"); res.attachment("files.zip"); - const archive = archiver("zip", {zlib: {level: 9}}); + const archive = archiver("zip", { zlib: { level: 9 } }); archive.on("error", (err) => { res.status(500).send("Error creating ZIP archive."); @@ -151,9 +155,9 @@ export const downloadFiles = async (req: Request, res: any) => { for (const file of files) { const filePath = path.join(FILES_DIR, file); if (fs.existsSync(filePath)) { - archive.file(filePath, {name: file}); + archive.file(filePath, { name: file }); } else { - console.warn(`File not found: ${file}`); + console.warn(File not found: ${file}); } } @@ -166,9 +170,9 @@ export const downloadFiles = async (req: Request, res: any) => { export const getOneFile = (req: Request, res: any) => { try { - const {fileDefaultName} = req.body; + const { fileDefaultName } = req.body; - console.log(fileDefaultName) + console.log(fileDefaultName); if (!fileDefaultName) { return res.status(400).send("File path is required."); @@ -176,7 +180,7 @@ export const getOneFile = (req: Request, res: any) => { const fullPath = path.join(FINAL_DIR, fileDefaultName); - console.log(fullPath) + console.log(fullPath); if (!fs.existsSync(fullPath)) { return res.status(404).send("File not found."); @@ -185,7 +189,7 @@ export const getOneFile = (req: Request, res: any) => { res.setHeader("Content-Type", "application/octet-stream"); res.setHeader( "Content-Disposition", - `inline; filename="${path.basename(fullPath)}"` + inline; filename="${path.basename(fullPath)}" ); const fileStream = fs.createReadStream(fullPath); @@ -199,8 +203,4 @@ export const getOneFile = (req: Request, res: any) => { console.error(err); res.status(500).send("Error fetching file for preview."); } -}; - - - - +}; \ No newline at end of file diff --git a/files/src/router/index.ts b/files/src/router/index.ts index b8c84bf..06e4ae5 100644 --- a/files/src/router/index.ts +++ b/files/src/router/index.ts @@ -1,14 +1,18 @@ import express from "express"; -import {addNewUpload, deleteFile, downloadFiles, getOneFile} from "../controllers/filesController"; +import { + addNewUpload, + deleteFile, + downloadFiles, + getOneFile, +} from "../controllers/filesController"; -const router = express.Router() +const router = express.Router(); -router.post('/get-one', getOneFile) -router.post('/upload', addNewUpload) -router.post('/download', downloadFiles) +router.post("/get-one", getOneFile); +router.post("/upload", addNewUpload); +router.post("/download", downloadFiles); -router.delete('/delete', deleteFile) +router.delete("/delete", deleteFile); // blob:http://localhost:7002/files/f6b04590-02f1-45db-a7e7-ec9ee352ef4f - -export default router \ No newline at end of file +export default router; diff --git a/frontend/src/pages/VisitorDownloadPage.tsx b/frontend/src/pages/VisitorDownloadPage.tsx index 43c589f..ed4f097 100644 --- a/frontend/src/pages/VisitorDownloadPage.tsx +++ b/frontend/src/pages/VisitorDownloadPage.tsx @@ -1,11 +1,11 @@ import styled from "@emotion/styled"; -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' +import { Button, Card, Modal, notification, Table, Checkbox } 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 { key: string; @@ -22,10 +22,12 @@ const VisitorDownloadPage = () => { const [notifApi, contextHolder] = notification.useNotification(); const [searchParams] = useSearchParams(); const [files, setFiles] = useState([]); - const [filePreview, setFilePreview] = useState(null) + const [filePreview, setFilePreview] = useState(null); + const [selectedFiles, setSelectedFiles] = useState([]); + const [selectAll, setSelectAll] = useState(false); const token = searchParams.get("token"); - const [getFiles, {loading, error, data}] = useMutation( + const [getFiles, { loading, error, data }] = useMutation( GET_FILES_FROM_UPLOAD, { onError: (error: ApolloError) => { @@ -40,11 +42,12 @@ 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) => ({ ...file, url: `http://localhost:7002/access/download?token=${token}&fileId=${file.id}`, + key: index, }) ); setFiles(fetchedFiles); @@ -76,12 +79,16 @@ const VisitorDownloadPage = () => { if (diffDays > 0) { result += `${diffDays} day${diffDays > 1 ? "s" : ""}`; if (diffHours % 24 > 0) { - result += ` and ${diffHours % 24} hour${(diffHours % 24) > 1 ? "s" : ""}`; + result += ` and ${diffHours % 24} hour${ + diffHours % 24 > 1 ? "s" : "" + }`; } } else if (diffHours > 0) { result += `${diffHours} hour${diffHours > 1 ? "s" : ""}`; if (diffMinutes % 60 > 0) { - result += ` and ${diffMinutes % 60} minute${(diffMinutes % 60) > 1 ? "s" : ""}`; + result += ` and ${diffMinutes % 60} minute${ + diffMinutes % 60 > 1 ? "s" : "" + }`; } } else if (diffMinutes > 0) { result += `${diffMinutes} minute${diffMinutes > 1 ? "s" : ""}`; @@ -93,6 +100,21 @@ const VisitorDownloadPage = () => { return result + " ago"; }; + 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 formatSizeInMB = (sizeInBytes: string) => { const size = parseInt(sizeInBytes, 10); @@ -100,19 +122,32 @@ const VisitorDownloadPage = () => { return (size / (1024 * 1024)).toFixed(2) + " MB"; }; - const downloadAllFiles = async () => { + const handleSelectAll = (checked: boolean) => { + setSelectAll(checked); + setSelectedFiles(checked ? files : []); + }; + + const handleSelectFile = (file: FileData, checked: boolean) => { + if (checked) { + setSelectedFiles((prev) => [...prev, file]); + } else { + setSelectedFiles((prev) => prev.filter((f) => f.key !== file.key)); + } + }; + + const downloadSelectedFiles = async () => { try { const response = await axios.post( "http://localhost:7002/files/download", - {files: files.map((file) => file.default_name)}, - {responseType: "blob"} + { files: selectedFiles.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.download = "selected_files.zip"; link.click(); URL.revokeObjectURL(link.href); @@ -121,28 +156,27 @@ const VisitorDownloadPage = () => { } }; - 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: ( + handleSelectAll(e.target.checked)} + checked={selectAll} + /> + ), + dataIndex: "checkbox", + key: "checkbox", + render: (_: any, file: FileData) => ( + handleSelectFile(file, e.target.checked)} + checked={selectedFiles.some((f) => f.key === file.key)} + /> + ), + }, { title: "Name", dataIndex: "name", key: "name", - render: (text: string) => ( - - {text} - - ), }, { title: "Size", @@ -160,15 +194,21 @@ const VisitorDownloadPage = () => { title: "Action", dataIndex: "action", key: "action", - render: (text: string, file) => handlePreviewFile(file.default_name)}>preview, + render: (_: any, file: FileData) => ( + handlePreviewFile(file.default_name)} + > + Preview + + ), }, ]; return ( + {contextHolder} @@ -187,86 +227,98 @@ const VisitorDownloadPage = () => { pagination={false} showHeader={true} bordered={false} - style={{marginBottom: "20px"}} + style={{ marginBottom: "20px" }} /> - setFilePreview(null)}> - Bonjour + setFilePreview(null)} + maskClosable={true} + centered + footer={ + + } + > + Preview ); }; -// 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; -`; - -const PreviewLink = styled.a` - color: #7f58ff; - text-decoration: underline; -`; - +// Styles 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; +`; + +const PreviewLink = styled.a` + color: #7f58ff; + text-decoration: underline; `; export default VisitorDownloadPage;