1
0
mirror of https://github.com/stonith404/pingvin-share.git synced 2024-07-02 07:20:38 +02:00

feat: file preview

This commit is contained in:
Elias Schneider 2023-01-31 09:03:03 +01:00
parent 0a2b7b1243
commit 91a6b3f716
No known key found for this signature in database
GPG Key ID: 07E623B294202B6C
10 changed files with 188 additions and 28 deletions

View File

@ -51,7 +51,7 @@ export class FileController {
const zip = this.fileService.getZip(shareId); const zip = this.fileService.getZip(shareId);
res.set({ res.set({
"Content-Type": "application/zip", "Content-Type": "application/zip",
"Content-Disposition": `attachment ; filename="pingvin-share-${shareId}.zip"`, "Content-Disposition": contentDisposition(`pingvin-share-${shareId}.zip`),
}); });
return new StreamableFile(zip); return new StreamableFile(zip);
@ -62,14 +62,21 @@ export class FileController {
async getFile( async getFile(
@Res({ passthrough: true }) res: Response, @Res({ passthrough: true }) res: Response,
@Param("shareId") shareId: string, @Param("shareId") shareId: string,
@Param("fileId") fileId: string @Param("fileId") fileId: string,
@Query("download") download = "true"
) { ) {
const file = await this.fileService.get(shareId, fileId); const file = await this.fileService.get(shareId, fileId);
res.set({
const headers = {
"Content-Type": file.metaData.mimeType, "Content-Type": file.metaData.mimeType,
"Content-Length": file.metaData.size, "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); return new StreamableFile(file.file);
} }

View File

@ -21,6 +21,7 @@
"cookies-next": "^2.1.1", "cookies-next": "^2.1.1",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"jose": "^4.11.2", "jose": "^4.11.2",
"mime-types": "^2.1.35",
"moment": "^2.29.4", "moment": "^2.29.4",
"next": "^13.1.2", "next": "^13.1.2",
"next-cookies": "^2.0.3", "next-cookies": "^2.0.3",
@ -33,6 +34,7 @@
"yup": "^0.32.11" "yup": "^0.32.11"
}, },
"devDependencies": { "devDependencies": {
"@types/mime-types": "^2.1.1",
"@types/node": "18.11.18", "@types/node": "18.11.18",
"@types/react": "18.0.26", "@types/react": "18.0.26",
"@types/react-dom": "18.0.10", "@types/react-dom": "18.0.10",
@ -2656,6 +2658,12 @@
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.181.tgz", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.181.tgz",
"integrity": "sha512-n3tyKthHJbkiWhDZs3DkhkCzt2MexYHXlX0td5iMplyfwketaOeKboEVBqzceH7juqvEg3q5oUoBFxSLu7zFag==" "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": { "node_modules/@types/minimatch": {
"version": "3.0.5", "version": "3.0.5",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", "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", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.181.tgz",
"integrity": "sha512-n3tyKthHJbkiWhDZs3DkhkCzt2MexYHXlX0td5iMplyfwketaOeKboEVBqzceH7juqvEg3q5oUoBFxSLu7zFag==" "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": { "@types/minimatch": {
"version": "3.0.5", "version": "3.0.5",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz",

View File

@ -22,6 +22,7 @@
"cookies-next": "^2.1.1", "cookies-next": "^2.1.1",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"jose": "^4.11.2", "jose": "^4.11.2",
"mime-types": "^2.1.35",
"moment": "^2.29.4", "moment": "^2.29.4",
"next": "^13.1.2", "next": "^13.1.2",
"next-cookies": "^2.0.3", "next-cookies": "^2.0.3",
@ -34,6 +35,7 @@
"yup": "^0.32.11" "yup": "^0.32.11"
}, },
"devDependencies": { "devDependencies": {
"@types/mime-types": "^2.1.1",
"@types/node": "18.11.18", "@types/node": "18.11.18",
"@types/react": "18.0.26", "@types/react": "18.0.26",
"@types/react-dom": "18.0.10", "@types/react-dom": "18.0.10",

View File

@ -0,0 +1,13 @@
import { Center, Loader, Stack } from "@mantine/core";
const CenterLoader = () => {
return (
<Center style={{ height: "70vh" }}>
<Stack align="center" spacing={10}>
<Loader />
</Stack>
</Center>
);
};
export default CenterLoader;

View File

@ -1,7 +1,9 @@
import { ActionIcon, Loader, Skeleton, Table } from "@mantine/core"; import { ActionIcon, Group, Skeleton, Table } from "@mantine/core";
import { TbCircleCheck, TbDownload } from "react-icons/tb"; import mime from "mime-types";
import Link from "next/link";
import { TbDownload, TbEye } from "react-icons/tb";
import shareService from "../../services/share.service"; import shareService from "../../services/share.service";
import { FileMetaData } from "../../types/File.type";
import { byteToHumanSizeString } from "../../utils/fileSize.util"; import { byteToHumanSizeString } from "../../utils/fileSize.util";
const FileList = ({ const FileList = ({
@ -9,7 +11,7 @@ const FileList = ({
shareId, shareId,
isLoading, isLoading,
}: { }: {
files?: any[]; files?: FileMetaData[];
shareId: string; shareId: string;
isLoading: boolean; isLoading: boolean;
}) => { }) => {
@ -28,15 +30,21 @@ const FileList = ({
: files!.map((file) => ( : files!.map((file) => (
<tr key={file.name}> <tr key={file.name}>
<td>{file.name}</td> <td>{file.name}</td>
<td>{byteToHumanSizeString(file.size)}</td> <td>{byteToHumanSizeString(parseInt(file.size))}</td>
<td> <td>
{file.uploadingState ? ( <Group position="right">
file.uploadingState != "finished" ? ( {shareService.doesFileSupportPreview(file.name) && (
<Loader size={22} /> <ActionIcon
) : ( component={Link}
<TbCircleCheck color="green" size={22} /> href={`/share/${shareId}/preview/${
) file.id
) : ( }?type=${mime.contentType(file.name)}`}
target="_blank"
size={25}
>
<TbEye />
</ActionIcon>
)}
<ActionIcon <ActionIcon
size={25} size={25}
onClick={async () => { onClick={async () => {
@ -45,7 +53,7 @@ const FileList = ({
> >
<TbDownload /> <TbDownload />
</ActionIcon> </ActionIcon>
)} </Group>
</td> </td>
</tr> </tr>
))} ))}

View File

@ -4,7 +4,6 @@ import {
Button, Button,
Center, Center,
Group, Group,
LoadingOverlay,
Stack, Stack,
Table, Table,
Text, Text,
@ -18,6 +17,7 @@ import { useRouter } from "next/router";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { TbInfoCircle, TbLink, TbPlus, TbTrash } from "react-icons/tb"; import { TbInfoCircle, TbLink, TbPlus, TbTrash } from "react-icons/tb";
import showShareLinkModal from "../../components/account/showShareLinkModal"; import showShareLinkModal from "../../components/account/showShareLinkModal";
import CenterLoader from "../../components/core/CenterLoader";
import Meta from "../../components/Meta"; import Meta from "../../components/Meta";
import showCreateReverseShareModal from "../../components/share/modals/showCreateReverseShareModal"; import showCreateReverseShareModal from "../../components/share/modals/showCreateReverseShareModal";
import useConfig from "../../hooks/config.hook"; import useConfig from "../../hooks/config.hook";
@ -50,7 +50,7 @@ const MyShares = () => {
if (!user) { if (!user) {
router.replace("/"); router.replace("/");
} else { } else {
if (!reverseShares) return <LoadingOverlay visible />; if (!reverseShares) return <CenterLoader />;
return ( return (
<> <>
<Meta title="My shares" /> <Meta title="My shares" />

View File

@ -2,13 +2,13 @@ import { Box, Group, Text, Title } from "@mantine/core";
import { useModals } from "@mantine/modals"; import { useModals } from "@mantine/modals";
import { GetServerSidePropsContext } from "next"; import { GetServerSidePropsContext } from "next";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import Meta from "../../components/Meta"; import Meta from "../../../components/Meta";
import DownloadAllButton from "../../components/share/DownloadAllButton"; import DownloadAllButton from "../../../components/share/DownloadAllButton";
import FileList from "../../components/share/FileList"; import FileList from "../../../components/share/FileList";
import showEnterPasswordModal from "../../components/share/showEnterPasswordModal"; import showEnterPasswordModal from "../../../components/share/showEnterPasswordModal";
import showErrorModal from "../../components/share/showErrorModal"; import showErrorModal from "../../../components/share/showErrorModal";
import shareService from "../../services/share.service"; import shareService from "../../../services/share.service";
import { Share as ShareType } from "../../types/share.type"; import { Share as ShareType } from "../../../types/share.type";
export function getServerSideProps(context: GetServerSidePropsContext) { export function getServerSideProps(context: GetServerSidePropsContext) {
return { return {

View File

@ -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 (
<Center style={{ height: "70vh" }}>
<Stack align="center" spacing={10}>
<Title order={3}>Preview not supported</Title>
<Text>
A preview for thise file type is unsupported. Please download the file
to view it.
</Text>
</Stack>
</Center>
);
};
const FilePreview = ({
shareId,
fileId,
mimeType,
}: {
shareId: string;
fileId: string;
mimeType: string;
}) => {
const [isNotSupported, setIsNotSupported] = useState(false);
if (isNotSupported) return <UnSupportedFile />;
if (mimeType == "application/pdf") {
window.location.href = `/api/shares/${shareId}/files/${fileId}?download=false`;
return null;
} else if (mimeType.startsWith("video/")) {
return (
<video
width="100%"
controls
onError={() => {
setIsNotSupported(true);
}}
>
<source src={`/api/shares/${shareId}/files/${fileId}?download=false`} />
</video>
);
} else if (mimeType.startsWith("image/")) {
return (
// eslint-disable-next-line @next/next/no-img-element
<img
onError={() => {
setIsNotSupported(true);
}}
src={`/api/shares/${shareId}/files/${fileId}?download=false`}
alt={`${fileId}_preview`}
width="100%"
/>
);
} else if (mimeType.startsWith("audio/")) {
return (
<Center style={{ height: "70vh" }}>
<Stack align="center" spacing={10} style={{ width: "100%" }}>
<audio
controls
style={{ width: "100%" }}
onError={() => {
setIsNotSupported(true);
}}
>
<source
src={`/api/shares/${shareId}/files/${fileId}?download=false`}
/>
</audio>
</Stack>
</Center>
);
} else {
return <UnSupportedFile />;
}
};
export default FilePreview;

View File

@ -1,5 +1,7 @@
import { setCookie } from "cookies-next"; import { setCookie } from "cookies-next";
import mime from "mime-types";
import { FileUploadResponse } from "../types/File.type"; import { FileUploadResponse } from "../types/File.type";
import { import {
CreateShare, CreateShare,
MyReverseShare, MyReverseShare,
@ -47,7 +49,22 @@ const getShareToken = async (id: string, password?: string) => {
}; };
const isShareIdAvailable = async (id: string): Promise<boolean> => { const isShareIdAvailable = async (id: string): Promise<boolean> => {
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) => { const downloadFile = async (shareId: string, fileId: string) => {
@ -114,6 +131,7 @@ export default {
get, get,
remove, remove,
getMetaData, getMetaData,
doesFileSupportPreview,
getMyShares, getMyShares,
isShareIdAvailable, isShareIdAvailable,
downloadFile, downloadFile,

View File

@ -1,3 +1,9 @@
export type FileUpload = File & { uploadingProgress: number }; export type FileUpload = File & { uploadingProgress: number };
export type FileUploadResponse = { id: string; name: string }; export type FileUploadResponse = { id: string; name: string };
export type FileMetaData = {
id: string;
name: string;
size: string;
};