diff --git a/backend/controllers/file.ts b/backend/controllers/file.ts index a87a219..2eb591b 100644 --- a/backend/controllers/file.ts +++ b/backend/controllers/file.ts @@ -509,7 +509,7 @@ class FileController { } }; - deleteFile = async (req: RequestType, res: Response) => { + restoreFile = async (req: RequestType, res: Response) => { if (!req.user) { return; } @@ -518,7 +518,26 @@ class FileController { const userID = req.user._id; const fileID = req.body.id; - console.log("id", fileID); + const file = await fileService.restoreFile(userID, fileID); + + res.send(file); + } catch (e: unknown) { + if (e instanceof Error) { + console.log("\nRestore File Error File Route:", e.message); + } + + res.status(500).send("Server error restoring file"); + } + }; + + deleteFile = async (req: RequestType, res: Response) => { + if (!req.user) { + return; + } + + try { + const userID = req.user._id; + const fileID = req.body.id; await this.chunkService.deleteFile(userID, fileID); @@ -532,6 +551,29 @@ class FileController { } }; + deleteMulti = async (req: RequestType, res: Response) => { + if (!req.user) { + return; + } + + try { + const userID = req.user._id; + const items = req.body.items; + + console.log("items", req.body); + + await this.chunkService.deleteMulti(userID, items); + + res.send(); + } catch (e: unknown) { + if (e instanceof Error) { + console.log("Delete Multi Error File Route:", e.message); + } + + res.status(500).send("Server error deleting multi"); + } + }; + trashMulti = async (req: RequestType, res: Response) => { if (!req.user) { return; @@ -552,6 +594,27 @@ class FileController { res.status(500).send("Server error trashing multi"); } }; + + restoreMulti = async (req: RequestType, res: Response) => { + if (!req.user) { + return; + } + + try { + const userID = req.user._id; + const items = req.body.items; + + await this.chunkService.restoreMulti(userID, items); + + res.send(); + } catch (e: unknown) { + if (e instanceof Error) { + console.log("\nRestore Multi Error File Route:", e.message); + } + + res.status(500).send("Server error restoring multi"); + } + }; } export default FileController; diff --git a/backend/controllers/folder.ts b/backend/controllers/folder.ts index 4fe9ba3..1424c37 100644 --- a/backend/controllers/folder.ts +++ b/backend/controllers/folder.ts @@ -241,6 +241,27 @@ class FolderController { } }; + restoreFolder = async (req: RequestType, res: Response) => { + if (!req.user) { + return; + } + + try { + const userID = req.user._id; + const folderID = req.body.id; + + await folderService.restoreFolder(userID, folderID); + + res.send(); + } catch (e: unknown) { + if (e instanceof Error) { + console.log("Restore Folder Error Folder Route:", e.message); + } + + res.status(500).send("Server error restore folder"); + } + }; + renameFolder = async (req: RequestType, res: Response) => { if (!req.user) { return; diff --git a/backend/db/utils/fileUtils/index.ts b/backend/db/utils/fileUtils/index.ts index 0c2413b..496aafd 100644 --- a/backend/db/utils/fileUtils/index.ts +++ b/backend/db/utils/fileUtils/index.ts @@ -87,6 +87,18 @@ class DbUtil { return result; }; + restoreFile = async (fileID: string, userID: string) => { + const result = await File.updateOne( + { _id: new ObjectId(fileID), "metadata.owner": userID }, + { + $set: { + "metadata.trashed": null, + }, + } + ); + return result; + }; + getFileInfo = async (fileID: string, userID: string) => { //TODO: Using mongoose like this causes the object to be returned in raw form // const file = await File.findOne({ @@ -168,6 +180,21 @@ class DbUtil { return result; }; + restoreFilesByParent = async (parentList: string, userID: string) => { + const result = await File.updateMany( + { + "metadata.owner": userID, + "metadata.parentList": { $regex: `.*${parentList}.*` }, // REGEX + }, + { + $set: { + "metadata.trashed": null, + }, + } + ); + return result; + }; + renameFile = async (fileID: string, userID: string, title: string) => { const updateFileResponse = await File.findOneAndUpdate( { _id: new ObjectId(fileID), "metadata.owner": userID }, diff --git a/backend/db/utils/folderUtils/index.ts b/backend/db/utils/folderUtils/index.ts index 73c5674..77734e3 100644 --- a/backend/db/utils/folderUtils/index.ts +++ b/backend/db/utils/folderUtils/index.ts @@ -173,6 +173,21 @@ class DbUtil { ); return result; }; + + restoreFoldersByParent = async (parentList: string[], userID: string) => { + const result = await Folder.updateMany( + { + owner: userID, + parentList: { $all: parentList }, + }, + { + $set: { + trashed: null, + }, + } + ); + return result; + }; } export default DbUtil; diff --git a/backend/express-routers/file.ts b/backend/express-routers/file.ts index 5ffa9fe..344c062 100644 --- a/backend/express-routers/file.ts +++ b/backend/express-routers/file.ts @@ -103,6 +103,10 @@ router.patch("/file-service/trash", auth, fileController.trashFile); router.patch("/file-service/trash-multi", auth, fileController.trashMulti); +router.patch("/file-service/restore", auth, fileController.restoreFile); + +router.patch("/file-service/restore-multi", auth, fileController.restoreMulti); + router.delete("/file-service/remove-link/:id", auth, fileController.removeLink); router.delete( @@ -113,6 +117,8 @@ router.delete( router.delete("/file-service/remove", auth, fileController.deleteFile); +router.delete("/file-service/remove-multi", auth, fileController.deleteMulti); + router.post( "/file-service/send-share-email", auth, diff --git a/backend/express-routers/folder.ts b/backend/express-routers/folder.ts index b1640b2..339c9c3 100644 --- a/backend/express-routers/folder.ts +++ b/backend/express-routers/folder.ts @@ -39,6 +39,8 @@ router.patch("/folder-service/move", auth, folderController.moveFolder); router.patch("/folder-service/trash", auth, folderController.trashFolder); +router.patch("/folder-service/restore", auth, folderController.restoreFolder); + router.get( "/folder-service/subfolder-list-full", auth, diff --git a/backend/models/folder.ts b/backend/models/folder.ts index 06df161..52dd002 100644 --- a/backend/models/folder.ts +++ b/backend/models/folder.ts @@ -39,7 +39,7 @@ export interface FolderInterface parentList: string[]; _doc?: any; personalFolder?: boolean; - trashed?: boolean; + trashed: boolean | null; } const Folder = mongoose.model("Folder", folderSchema); diff --git a/backend/services/ChunkService/FileSystemService.ts b/backend/services/ChunkService/FileSystemService.ts index d318c35..01f1455 100644 --- a/backend/services/ChunkService/FileSystemService.ts +++ b/backend/services/ChunkService/FileSystemService.ts @@ -504,6 +504,60 @@ class FileSystemService implements ChunkInterface { } }; + restoreMulti = async ( + userID: string, + items: { + type: "file" | "folder" | "quick-item"; + id: string; + file?: FileInterface; + folder?: FolderInterface; + }[] + ) => { + const fileList = items.filter( + (item) => item.type === "file" || item.type === "quick-item" + ); + const folderList = items + .filter((item) => item.type === "folder") + .sort((a, b) => { + if (!a.folder || !b.folder) return 0; + return b.folder.parentList.length - a.folder.parentList.length; + }); + + for (const file of fileList) { + await fileService.restoreFile(userID, file.id); + } + for (const folder of folderList) { + await folderService.restoreFolder(userID, folder.id); + } + }; + + deleteMulti = async ( + userID: string, + items: { + type: "file" | "folder" | "quick-item"; + id: string; + file?: FileInterface; + folder?: FolderInterface; + }[] + ) => { + const fileList = items.filter( + (item) => item.type === "file" || item.type === "quick-item" + ); + const folderList = items + .filter((item) => item.type === "folder") + .sort((a, b) => { + if (!a.folder || !b.folder) return 0; + return b.folder.parentList.length - a.folder.parentList.length; + }); + + for (const file of fileList) { + await this.deleteFile(userID, file.id); + } + for (const folder of folderList) { + await this.deleteFolder(userID, folder.id); + } + }; + deleteFile = async (userID: string, fileID: string) => { const file = await dbUtilsFile.getFileInfo(fileID, userID); diff --git a/backend/services/ChunkService/S3Service.ts b/backend/services/ChunkService/S3Service.ts index 5f73664..e3773a3 100644 --- a/backend/services/ChunkService/S3Service.ts +++ b/backend/services/ChunkService/S3Service.ts @@ -614,6 +614,26 @@ class S3Service implements ChunkInterface { folder?: FolderInterface; }[] ) => {}; + + restoreMulti = async ( + userID: string, + items: { + type: "file" | "folder" | "quick-item"; + id: string; + file?: FileInterface; + folder?: FolderInterface; + }[] + ) => {}; + + deleteMulti = async ( + userID: string, + items: { + type: "file" | "folder" | "quick-item"; + id: string; + file?: FileInterface; + folder?: FolderInterface; + }[] + ) => {}; } export default S3Service; diff --git a/backend/services/ChunkService/utils/tempCreateVideoThumbnailFS.ts b/backend/services/ChunkService/utils/tempCreateVideoThumbnailFS.ts index 602e618..15a5f97 100644 --- a/backend/services/ChunkService/utils/tempCreateVideoThumbnailFS.ts +++ b/backend/services/ChunkService/utils/tempCreateVideoThumbnailFS.ts @@ -28,9 +28,9 @@ const tempCreateVideoThumbnailFS = ( const writeStream = fs.createWriteStream( env.fsDirectory + thumbnailFilename ); - const tempWriteStream = fs.createWriteStream( - env.fsDirectory + "temp/" + thumbnailFilename - ); + + const tempDirectory = env.fsDirectory + "temp/" + thumbnailFilename; + const tempWriteStream = fs.createWriteStream(tempDirectory); const decipher = crypto.createDecipheriv( "aes256", CIPHER_KEY, @@ -49,7 +49,7 @@ const tempCreateVideoThumbnailFS = ( decryptedReadStream.pipe(tempWriteStream, { end: true }); - ffmpeg(env.fsDirectory + "temp/" + thumbnailFilename, { + ffmpeg(tempDirectory, { timeout: 60, }) .seek(1) @@ -97,7 +97,9 @@ const tempCreateVideoThumbnailFS = ( if (!updatedFile) return reject(); - resolve(updatedFile?.toObject()); + fs.unlink(tempDirectory, (err) => { + resolve(updatedFile?.toObject()); + }); }) .on("error", (err, _, stderr) => { console.log("error", err, stderr); diff --git a/backend/services/FileService/index.ts b/backend/services/FileService/index.ts index 2030001..2b17e49 100644 --- a/backend/services/FileService/index.ts +++ b/backend/services/FileService/index.ts @@ -236,6 +236,12 @@ class MongoFileService { return trashedFile; }; + restoreFile = async (userID: string, fileID: string) => { + const restoredFile = await dbUtilsFile.restoreFile(fileID, userID); + if (!restoredFile) throw new NotFoundError("Restore File Not Found Error"); + return restoredFile; + }; + renameFile = async (userID: string, fileID: string, title: string) => { const file = await dbUtilsFile.renameFile(fileID, userID, title); diff --git a/backend/services/FolderService/index.ts b/backend/services/FolderService/index.ts index 436a8bc..8d9aa0c 100644 --- a/backend/services/FolderService/index.ts +++ b/backend/services/FolderService/index.ts @@ -225,6 +225,21 @@ class FolderService { await utilsFile.trashFilesByParent(parentList.toString(), userID); }; + restoreFolder = async (userID: string, folderID: string) => { + const folder = await utilsFolder.getFolderInfo(folderID, userID); + + if (!folder) throw new NotFoundError("Restore Folder Not Found Error"); + + folder.trashed = null; + await folder.save(); + + const parentList = [...folder.parentList, folder._id!.toString()]; + + await utilsFolder.restoreFoldersByParent(parentList, userID); + + await utilsFile.restoreFilesByParent(parentList.toString(), userID); + }; + moveFolder = async (userID: string, folderID: string, parentID: string) => { let parentList = ["/"]; diff --git a/src/api/filesAPI.ts b/src/api/filesAPI.ts index 6259e59..1335016 100644 --- a/src/api/filesAPI.ts +++ b/src/api/filesAPI.ts @@ -113,6 +113,20 @@ export const trashMultiAPI = async (items: any) => { return response.data; }; +export const restoreFileAPI = async (fileID: string) => { + const response = await axios.patch(`/file-service/restore`, { + id: fileID, + }); + return response.data; +}; + +export const restoreMultiAPI = async (items: any) => { + const response = await axios.patch(`/file-service/restore-multi`, { + items, + }); + return response.data; +}; + export const renameFileAPI = async (fileID: string, name: string) => { const response = await axios.patch(`/file-service/rename`, { id: fileID, @@ -130,3 +144,12 @@ export const deleteFileAPI = async (fileID: string) => { }); return response.data; }; + +export const deleteMultiAPI = async (items: any) => { + const response = await axios.delete(`/file-service/remove-multi`, { + data: { + items, + }, + }); + return response.data; +}; diff --git a/src/api/foldersAPI.ts b/src/api/foldersAPI.ts index bb4a621..6f476c7 100644 --- a/src/api/foldersAPI.ts +++ b/src/api/foldersAPI.ts @@ -56,8 +56,15 @@ export const renameFolder = async (folderID: string, name: string) => { return response.data; }; -export const trashFolderAPI = (folderID: string) => { - const response = axios.patch("/folder-service/trash", { +export const trashFolderAPI = async (folderID: string) => { + const response = await axios.patch("/folder-service/trash", { + id: folderID, + }); + return response.data; +}; + +export const restoreFolderAPI = async (folderID: string) => { + const response = await axios.patch("/folder-service/restore", { id: folderID, }); return response.data; diff --git a/src/components/ContextMenu/index.jsx b/src/components/ContextMenu/index.jsx index cce1d60..cb0ded8 100644 --- a/src/components/ContextMenu/index.jsx +++ b/src/components/ContextMenu/index.jsx @@ -6,6 +6,7 @@ import { renameFileAPI, downloadFileAPI, trashFileAPI, + restoreFileAPI, } from "../../api/filesAPI"; import { useFilesClient, useQuickFilesClient } from "../../hooks/files"; import { useFoldersClient } from "../../hooks/folders"; @@ -15,6 +16,7 @@ import { setShareSelected } from "../../actions/selectedItem"; import { deleteFolder, renameFolder, + restoreFolderAPI, trashFolderAPI, } from "../../api/foldersAPI"; import { useClickOutOfBounds, useUtils } from "../../hooks/utils"; @@ -27,6 +29,7 @@ import ShareIcon from "../../icons/ShareIcon"; import DownloadIcon from "../../icons/DownloadIcon"; import MoveIcon from "../../icons/MoveIcon"; import RestoreIcon from "../../icons/RestoreIcon"; +import { restoreItemPopup } from "../../popups/file"; const ContextMenu = (props) => { const { invalidateFilesCache } = useFilesClient(); @@ -148,6 +151,19 @@ const ContextMenu = (props) => { } }; + const restoreItem = async () => { + props.closeContext(); + const result = await restoreItemPopup(); + if (!result) return; + if (!props.folderMode) { + await restoreFileAPI(props.file._id); + invalidateFilesCache(); + } else { + await restoreFolderAPI(props.folder._id); + invalidateFoldersCache(); + } + }; + const openMoveItemModal = async () => { props.closeContext(); if (!props.folderMode) { @@ -268,7 +284,7 @@ const ContextMenu = (props) => { )} {isTrash && ( -
  • +
  • diff --git a/src/components/FileItem/index.jsx b/src/components/FileItem/index.jsx index 550aac4..cad4404 100644 --- a/src/components/FileItem/index.jsx +++ b/src/components/FileItem/index.jsx @@ -13,6 +13,7 @@ import { setPopupFile } from "../../actions/popupFile"; import bytes from "bytes"; import { useAppDispatch, useAppSelector } from "../../hooks/store"; import { setMainSelect, setMultiSelectMode } from "../../reducers/selected"; +import PlayButtonIcon from "../../icons/PlayIcon"; const FileItem = React.memo((props) => { const { file } = props; @@ -215,11 +216,18 @@ const FileItem = React.memo((props) => { )} > {hasThumbnail ? ( - +
    + + {file.metadata.isVideo && ( +
    + +
    + )} +
    ) : ( { const dispatch = useAppDispatch(); @@ -21,11 +30,13 @@ const MultiSelectBar = () => { const { invalidateFoldersCache } = useFoldersClient(); const { invalidateQuickFilesCache } = useQuickFilesClient(); + const { isTrash } = useUtils(); + const closeMultiSelect = () => { dispatch(resetMultiSelect()); }; - const deleteItems = async () => { + const trashItems = async () => { const result = await Swal.fire({ title: "Move to trash?", text: "Items in the trash will eventually be deleted.", @@ -36,7 +47,7 @@ const MultiSelectBar = () => { confirmButtonText: "Yes", }); - if (result.value) { + if (result) { const itemsToTrash = Object.values(multiSelectMap); await trashMultiAPI(itemsToTrash); invalidateFilesCache(); @@ -46,6 +57,31 @@ const MultiSelectBar = () => { } }; + const deleteItems = async () => { + const result = await deleteItemsPopup(); + if (result) { + const itemsToTrash = Object.values(multiSelectMap); + await deleteMultiAPI(itemsToTrash); + invalidateFilesCache(); + invalidateFoldersCache(); + invalidateQuickFilesCache(); + closeMultiSelect(); + } + }; + + const restoreItems = async () => { + const result = await restoreItemsPopup(); + + if (result) { + const itemsToTrash = Object.values(multiSelectMap); + await restoreMultiAPI(itemsToTrash); + invalidateFilesCache(); + invalidateFoldersCache(); + invalidateQuickFilesCache(); + closeMultiSelect(); + } + }; + if (!multiSelectMode) return
    ; return ( @@ -62,56 +98,25 @@ const MultiSelectBar = () => {
    - - - + )} + {isTrash && ( + + - - - - - - - - + + )} + + {!isTrash && ( + {}} /> + )}
    diff --git a/src/components/PopupWindow/PopupWindow.jsx b/src/components/PopupWindow/PopupWindow.jsx index a133656..bf13296 100644 --- a/src/components/PopupWindow/PopupWindow.jsx +++ b/src/components/PopupWindow/PopupWindow.jsx @@ -58,7 +58,7 @@ class PopupWindow extends React.Component { ) : (