mirror of
https://github.com/stonith404/pingvin-share.git
synced 2025-01-29 01:28:59 -05:00
feat: file preview
This commit is contained in:
parent
0a2b7b1243
commit
91a6b3f716
10 changed files with 188 additions and 28 deletions
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
14
frontend/package-lock.json
generated
14
frontend/package-lock.json
generated
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
13
frontend/src/components/core/CenterLoader.tsx
Normal file
13
frontend/src/components/core/CenterLoader.tsx
Normal 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;
|
|
@ -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>
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -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" />
|
||||||
|
|
|
@ -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 {
|
92
frontend/src/pages/share/[shareId]/preview/[fileId].tsx
Normal file
92
frontend/src/pages/share/[shareId]/preview/[fileId].tsx
Normal 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;
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
|
};
|
||||||
|
|
Loading…
Add table
Reference in a new issue