From 91a6b3f716d37d7831e17a7be1cdb35cb23da705 Mon Sep 17 00:00:00 2001 From: Elias Schneider Date: Tue, 31 Jan 2023 09:03:03 +0100 Subject: [PATCH] feat: file preview --- backend/src/file/file.controller.ts | 17 +++- frontend/package-lock.json | 14 +++ frontend/package.json | 2 + frontend/src/components/core/CenterLoader.tsx | 13 +++ frontend/src/components/share/FileList.tsx | 34 ++++--- frontend/src/pages/account/reverseShares.tsx | 4 +- .../{[shareId].tsx => [shareId]/index.tsx} | 14 +-- .../share/[shareId]/preview/[fileId].tsx | 92 +++++++++++++++++++ frontend/src/services/share.service.ts | 20 +++- frontend/src/types/File.type.ts | 6 ++ 10 files changed, 188 insertions(+), 28 deletions(-) create mode 100644 frontend/src/components/core/CenterLoader.tsx rename frontend/src/pages/share/{[shareId].tsx => [shareId]/index.tsx} (83%) create mode 100644 frontend/src/pages/share/[shareId]/preview/[fileId].tsx diff --git a/backend/src/file/file.controller.ts b/backend/src/file/file.controller.ts index 40b9020..d861bb7 100644 --- a/backend/src/file/file.controller.ts +++ b/backend/src/file/file.controller.ts @@ -51,7 +51,7 @@ export class FileController { const zip = this.fileService.getZip(shareId); res.set({ "Content-Type": "application/zip", - "Content-Disposition": `attachment ; filename="pingvin-share-${shareId}.zip"`, + "Content-Disposition": contentDisposition(`pingvin-share-${shareId}.zip`), }); return new StreamableFile(zip); @@ -62,14 +62,21 @@ export class FileController { async getFile( @Res({ passthrough: true }) res: Response, @Param("shareId") shareId: string, - @Param("fileId") fileId: string + @Param("fileId") fileId: string, + @Query("download") download = "true" ) { const file = await this.fileService.get(shareId, fileId); - res.set({ + + const headers = { "Content-Type": file.metaData.mimeType, "Content-Length": file.metaData.size, - "Content-Disposition": contentDisposition(file.metaData.name), - }); + }; + + if (download === "true") { + headers["Content-Disposition"] = contentDisposition(file.metaData.name); + } + + res.set(headers); return new StreamableFile(file.file); } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 8450931..bf72c16 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -21,6 +21,7 @@ "cookies-next": "^2.1.1", "file-saver": "^2.0.5", "jose": "^4.11.2", + "mime-types": "^2.1.35", "moment": "^2.29.4", "next": "^13.1.2", "next-cookies": "^2.0.3", @@ -33,6 +34,7 @@ "yup": "^0.32.11" }, "devDependencies": { + "@types/mime-types": "^2.1.1", "@types/node": "18.11.18", "@types/react": "18.0.26", "@types/react-dom": "18.0.10", @@ -2656,6 +2658,12 @@ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.181.tgz", "integrity": "sha512-n3tyKthHJbkiWhDZs3DkhkCzt2MexYHXlX0td5iMplyfwketaOeKboEVBqzceH7juqvEg3q5oUoBFxSLu7zFag==" }, + "node_modules/@types/mime-types": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.1.tgz", + "integrity": "sha512-vXOTGVSLR2jMw440moWTC7H19iUyLtP3Z1YTj7cSsubOICinjMxFeb/V57v9QdyyPGbbWolUFSSmSiRSn94tFw==", + "dev": true + }, "node_modules/@types/minimatch": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", @@ -9913,6 +9921,12 @@ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.181.tgz", "integrity": "sha512-n3tyKthHJbkiWhDZs3DkhkCzt2MexYHXlX0td5iMplyfwketaOeKboEVBqzceH7juqvEg3q5oUoBFxSLu7zFag==" }, + "@types/mime-types": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.1.tgz", + "integrity": "sha512-vXOTGVSLR2jMw440moWTC7H19iUyLtP3Z1YTj7cSsubOICinjMxFeb/V57v9QdyyPGbbWolUFSSmSiRSn94tFw==", + "dev": true + }, "@types/minimatch": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", diff --git a/frontend/package.json b/frontend/package.json index 022e368..3fb4c1b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -22,6 +22,7 @@ "cookies-next": "^2.1.1", "file-saver": "^2.0.5", "jose": "^4.11.2", + "mime-types": "^2.1.35", "moment": "^2.29.4", "next": "^13.1.2", "next-cookies": "^2.0.3", @@ -34,6 +35,7 @@ "yup": "^0.32.11" }, "devDependencies": { + "@types/mime-types": "^2.1.1", "@types/node": "18.11.18", "@types/react": "18.0.26", "@types/react-dom": "18.0.10", diff --git a/frontend/src/components/core/CenterLoader.tsx b/frontend/src/components/core/CenterLoader.tsx new file mode 100644 index 0000000..9d99b38 --- /dev/null +++ b/frontend/src/components/core/CenterLoader.tsx @@ -0,0 +1,13 @@ +import { Center, Loader, Stack } from "@mantine/core"; + +const CenterLoader = () => { + return ( +
+ + + +
+ ); +}; + +export default CenterLoader; diff --git a/frontend/src/components/share/FileList.tsx b/frontend/src/components/share/FileList.tsx index 5bc3d7a..1cadbfc 100644 --- a/frontend/src/components/share/FileList.tsx +++ b/frontend/src/components/share/FileList.tsx @@ -1,7 +1,9 @@ -import { ActionIcon, Loader, Skeleton, Table } from "@mantine/core"; -import { TbCircleCheck, TbDownload } from "react-icons/tb"; +import { ActionIcon, Group, Skeleton, Table } from "@mantine/core"; +import mime from "mime-types"; +import Link from "next/link"; +import { TbDownload, TbEye } from "react-icons/tb"; import shareService from "../../services/share.service"; - +import { FileMetaData } from "../../types/File.type"; import { byteToHumanSizeString } from "../../utils/fileSize.util"; const FileList = ({ @@ -9,7 +11,7 @@ const FileList = ({ shareId, isLoading, }: { - files?: any[]; + files?: FileMetaData[]; shareId: string; isLoading: boolean; }) => { @@ -28,15 +30,21 @@ const FileList = ({ : files!.map((file) => ( {file.name} - {byteToHumanSizeString(file.size)} + {byteToHumanSizeString(parseInt(file.size))} - {file.uploadingState ? ( - file.uploadingState != "finished" ? ( - - ) : ( - - ) - ) : ( + + {shareService.doesFileSupportPreview(file.name) && ( + + + + )} { @@ -45,7 +53,7 @@ const FileList = ({ > - )} + ))} diff --git a/frontend/src/pages/account/reverseShares.tsx b/frontend/src/pages/account/reverseShares.tsx index 2993dd7..7be0319 100644 --- a/frontend/src/pages/account/reverseShares.tsx +++ b/frontend/src/pages/account/reverseShares.tsx @@ -4,7 +4,6 @@ import { Button, Center, Group, - LoadingOverlay, Stack, Table, Text, @@ -18,6 +17,7 @@ import { useRouter } from "next/router"; import { useEffect, useState } from "react"; import { TbInfoCircle, TbLink, TbPlus, TbTrash } from "react-icons/tb"; import showShareLinkModal from "../../components/account/showShareLinkModal"; +import CenterLoader from "../../components/core/CenterLoader"; import Meta from "../../components/Meta"; import showCreateReverseShareModal from "../../components/share/modals/showCreateReverseShareModal"; import useConfig from "../../hooks/config.hook"; @@ -50,7 +50,7 @@ const MyShares = () => { if (!user) { router.replace("/"); } else { - if (!reverseShares) return ; + if (!reverseShares) return ; return ( <> diff --git a/frontend/src/pages/share/[shareId].tsx b/frontend/src/pages/share/[shareId]/index.tsx similarity index 83% rename from frontend/src/pages/share/[shareId].tsx rename to frontend/src/pages/share/[shareId]/index.tsx index a4526e8..e1c293f 100644 --- a/frontend/src/pages/share/[shareId].tsx +++ b/frontend/src/pages/share/[shareId]/index.tsx @@ -2,13 +2,13 @@ import { Box, Group, Text, Title } from "@mantine/core"; import { useModals } from "@mantine/modals"; import { GetServerSidePropsContext } from "next"; import { useEffect, useState } from "react"; -import Meta from "../../components/Meta"; -import DownloadAllButton from "../../components/share/DownloadAllButton"; -import FileList from "../../components/share/FileList"; -import showEnterPasswordModal from "../../components/share/showEnterPasswordModal"; -import showErrorModal from "../../components/share/showErrorModal"; -import shareService from "../../services/share.service"; -import { Share as ShareType } from "../../types/share.type"; +import Meta from "../../../components/Meta"; +import DownloadAllButton from "../../../components/share/DownloadAllButton"; +import FileList from "../../../components/share/FileList"; +import showEnterPasswordModal from "../../../components/share/showEnterPasswordModal"; +import showErrorModal from "../../../components/share/showErrorModal"; +import shareService from "../../../services/share.service"; +import { Share as ShareType } from "../../../types/share.type"; export function getServerSideProps(context: GetServerSidePropsContext) { return { diff --git a/frontend/src/pages/share/[shareId]/preview/[fileId].tsx b/frontend/src/pages/share/[shareId]/preview/[fileId].tsx new file mode 100644 index 0000000..e90a0f7 --- /dev/null +++ b/frontend/src/pages/share/[shareId]/preview/[fileId].tsx @@ -0,0 +1,92 @@ +import { Center, Stack, Text, Title } from "@mantine/core"; +import { GetServerSidePropsContext } from "next"; +import { useState } from "react"; + +export function getServerSideProps(context: GetServerSidePropsContext) { + const { shareId, fileId } = context.params!; + + const mimeType = context.query.type as string; + + return { + props: { shareId, fileId, mimeType }, + }; +} + +const UnSupportedFile = () => { + return ( +
+ + Preview not supported + + A preview for thise file type is unsupported. Please download the file + to view it. + + +
+ ); +}; + +const FilePreview = ({ + shareId, + fileId, + mimeType, +}: { + shareId: string; + fileId: string; + mimeType: string; +}) => { + const [isNotSupported, setIsNotSupported] = useState(false); + + if (isNotSupported) return ; + + if (mimeType == "application/pdf") { + window.location.href = `/api/shares/${shareId}/files/${fileId}?download=false`; + return null; + } else if (mimeType.startsWith("video/")) { + return ( + + ); + } else if (mimeType.startsWith("image/")) { + return ( + // eslint-disable-next-line @next/next/no-img-element + { + setIsNotSupported(true); + }} + src={`/api/shares/${shareId}/files/${fileId}?download=false`} + alt={`${fileId}_preview`} + width="100%" + /> + ); + } else if (mimeType.startsWith("audio/")) { + return ( +
+ + + +
+ ); + } else { + return ; + } +}; + +export default FilePreview; diff --git a/frontend/src/services/share.service.ts b/frontend/src/services/share.service.ts index ebe72b3..a7e1dd5 100644 --- a/frontend/src/services/share.service.ts +++ b/frontend/src/services/share.service.ts @@ -1,5 +1,7 @@ import { setCookie } from "cookies-next"; +import mime from "mime-types"; import { FileUploadResponse } from "../types/File.type"; + import { CreateShare, MyReverseShare, @@ -47,7 +49,22 @@ const getShareToken = async (id: string, password?: string) => { }; const isShareIdAvailable = async (id: string): Promise => { - return (await api.get(`shares/isShareIdAvailable/${id}`)).data.isAvailable; + return (await api.get(`/shares/isShareIdAvailable/${id}`)).data.isAvailable; +}; + +const doesFileSupportPreview = (fileName: string) => { + const mimeType = mime.contentType(fileName); + + if (!mimeType) return false; + + const supportedMimeTypes = [ + mimeType.startsWith("video/"), + mimeType.startsWith("image/"), + mimeType.startsWith("audio/"), + mimeType == "application/pdf", + ]; + + return supportedMimeTypes.some((isSupported) => isSupported); }; const downloadFile = async (shareId: string, fileId: string) => { @@ -114,6 +131,7 @@ export default { get, remove, getMetaData, + doesFileSupportPreview, getMyShares, isShareIdAvailable, downloadFile, diff --git a/frontend/src/types/File.type.ts b/frontend/src/types/File.type.ts index d5ccf63..7c0927a 100644 --- a/frontend/src/types/File.type.ts +++ b/frontend/src/types/File.type.ts @@ -1,3 +1,9 @@ export type FileUpload = File & { uploadingProgress: number }; export type FileUploadResponse = { id: string; name: string }; + +export type FileMetaData = { + id: string; + name: string; + size: string; +};