diff --git a/README.md b/README.md index 70eba064..a9a5e763 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,12 @@ Without docker: 1. Run `npm install` 2. Run `npm run build && npm run start` + +## Known issues / Limitations +Pingvin Share is currently in beta and there are issues and limitations that should be fixed in the future. +- `DownloadAll` generates the zip file on the client side. This takes alot of time. Because of that I temporarily limited this function to maximal 150 MB. +- If a user knows the share id, he can list and download the files directly from the Appwrite API even if the share is secured by a password or a visitor limit. + ## Contribute You're very welcome to contribute to Pingvin Share! diff --git a/package-lock.json b/package-lock.json index e0a11011..f65e183c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,9 @@ "axios": "^0.26.1", "cookie": "^0.5.0", "cookies-next": "^2.0.4", + "file-saver": "^2.0.5", "js-file-download": "^0.4.12", + "jszip": "^3.9.1", "next": "12.1.5", "next-pwa": "^5.5.2", "node-appwrite": "^5.1.0", @@ -30,6 +32,7 @@ }, "devDependencies": { "@types/cookie": "^0.5.0", + "@types/file-saver": "^2.0.5", "@types/node": "17.0.23", "@types/react": "18.0.4", "@types/react-dom": "18.0.0", @@ -2527,6 +2530,12 @@ "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==", "peer": true }, + "node_modules/@types/file-saver": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-zv9kNf3keYegP5oThGLaPk8E081DFDuwfqjtiTzm6PoxChdJ1raSuADf2YGCVIyrSynLrgc8JWv296s7Q7pQSQ==", + "dev": true + }, "node_modules/@types/glob": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", @@ -4455,6 +4464,11 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/file-saver": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==" + }, "node_modules/file-selector": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.4.0.tgz", @@ -4895,6 +4909,11 @@ "node": ">= 4" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=" + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -5364,6 +5383,44 @@ "node": ">=4.0" } }, + "node_modules/jszip": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.9.1.tgz", + "integrity": "sha512-H9A60xPqJ1CuC4Ka6qxzXZeU8aNmgOeP5IFqwJbQQwtu2EUYxota3LdsiZWplF7Wgd9tkAd0mdu36nceSaPuYw==", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "set-immediate-shim": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/language-subtag-registry": { "version": "0.3.21", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz", @@ -5400,6 +5457,14 @@ "node": ">= 0.8.0" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -5950,6 +6015,11 @@ "node": ">=4" } }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -6597,6 +6667,14 @@ "randombytes": "^2.1.0" } }, + "node_modules/set-immediate-shim": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", + "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -9469,6 +9547,12 @@ "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==", "peer": true }, + "@types/file-saver": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-zv9kNf3keYegP5oThGLaPk8E081DFDuwfqjtiTzm6PoxChdJ1raSuADf2YGCVIyrSynLrgc8JWv296s7Q7pQSQ==", + "dev": true + }, "@types/glob": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", @@ -10991,6 +11075,11 @@ "flat-cache": "^3.0.4" } }, + "file-saver": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==" + }, "file-selector": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.4.0.tgz", @@ -11314,6 +11403,11 @@ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==" }, + "immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=" + }, "import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -11648,6 +11742,46 @@ "object.assign": "^4.1.2" } }, + "jszip": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.9.1.tgz", + "integrity": "sha512-H9A60xPqJ1CuC4Ka6qxzXZeU8aNmgOeP5IFqwJbQQwtu2EUYxota3LdsiZWplF7Wgd9tkAd0mdu36nceSaPuYw==", + "requires": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "set-immediate-shim": "~1.0.1" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, "language-subtag-registry": { "version": "0.3.21", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz", @@ -11678,6 +11812,14 @@ "type-check": "~0.4.0" } }, + "lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "requires": { + "immediate": "~3.0.5" + } + }, "lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -12080,6 +12222,11 @@ "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", "dev": true }, + "pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, "parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -12536,6 +12683,11 @@ "randombytes": "^2.1.0" } }, + "set-immediate-shim": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", + "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=" + }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/package.json b/package.json index 91d1fcc0..edee07d2 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,9 @@ "axios": "^0.26.1", "cookie": "^0.5.0", "cookies-next": "^2.0.4", + "file-saver": "^2.0.5", "js-file-download": "^0.4.12", + "jszip": "^3.9.1", "next": "12.1.5", "next-pwa": "^5.5.2", "node-appwrite": "^5.1.0", @@ -33,6 +35,7 @@ }, "devDependencies": { "@types/cookie": "^0.5.0", + "@types/file-saver": "^2.0.5", "@types/node": "17.0.23", "@types/react": "18.0.4", "@types/react-dom": "18.0.0", diff --git a/src/components/share/DownloadAllButton.tsx b/src/components/share/DownloadAllButton.tsx new file mode 100644 index 00000000..74b260bf --- /dev/null +++ b/src/components/share/DownloadAllButton.tsx @@ -0,0 +1,67 @@ +import { Tooltip, Button } from "@mantine/core"; +import saveAs from "file-saver"; +import JSZip from "jszip"; +import { Dispatch, SetStateAction, useState } from "react"; +import { AppwriteFileWithPreview } from "../../types/File.type"; +import aw from "../../utils/appwrite.util"; + +const DownloadAllButton = ({ + shareId, + files, + setFiles, +}: { + shareId: string; + files: AppwriteFileWithPreview[]; + setFiles: Dispatch>; +}) => { + const [isLoading, setIsLoading] = useState(false); + const downloadAll = async () => { + setIsLoading(true); + var zip = new JSZip(); + for (let i = 0; i < files.length; i++) { + files[i].uploadingState = "inProgress"; + setFiles([...files]); + zip.file( + files[i].name, + await ( + await fetch( + aw.storage.getFileDownload(shareId, files[i].$id).toString() + ) + ).blob() + ); + files[i].uploadingState = "finished"; + setFiles([...files]); + } + zip.generateAsync({ type: "blob" }).then(function (content) { + setIsLoading(false); + saveAs(content, `${shareId}-pingvin-share.zip`); + }); + }; + const isFileTooBig = () => { + let shareSize = 0; + files.forEach((file) => (shareSize = +file.sizeOriginal)); + return 150000000 > shareSize; + }; + + if (!isFileTooBig()) + return ( + + + + ); + return ( + + ); +}; + +export default DownloadAllButton; diff --git a/src/components/share/FileList.tsx b/src/components/share/FileList.tsx index dff2054d..cdf2e0c5 100644 --- a/src/components/share/FileList.tsx +++ b/src/components/share/FileList.tsx @@ -1,7 +1,7 @@ -import { ActionIcon, Skeleton, Table } from "@mantine/core"; +import { ActionIcon, Loader, Skeleton, Table } from "@mantine/core"; import Image from "next/image"; import { useRouter } from "next/router"; -import { Download } from "tabler-icons-react"; +import { CircleCheck, Download } from "tabler-icons-react"; import { AppwriteFileWithPreview } from "../../types/File.type"; import aw from "../../utils/appwrite.util"; import { bytesToSize } from "../../utils/math/byteToSize.util"; @@ -50,14 +50,22 @@ const FileList = ({ {file.name} {bytesToSize(file.sizeOriginal)} - - router.push(aw.storage.getFileDownload(shareId, file.$id)) - } - > - - + {file.uploadingState ? ( + file.uploadingState != "finished" ? ( + + ) : ( + + ) + ) : ( + + router.push(aw.storage.getFileDownload(shareId, file.$id)) + } + > + + + )} )); diff --git a/src/pages/share/[shareId].tsx b/src/pages/share/[shareId].tsx index 31229253..b62ffa49 100644 --- a/src/pages/share/[shareId].tsx +++ b/src/pages/share/[shareId].tsx @@ -1,7 +1,9 @@ +import { Group } from "@mantine/core"; import { useModals } from "@mantine/modals"; import { useRouter } from "next/router"; 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 showShareNotFoundModal from "../../components/share/showShareNotFoundModal"; @@ -13,7 +15,7 @@ const Share = () => { const router = useRouter(); const modals = useModals(); const shareId = router.query.shareId as string; - const [shareList, setShareList] = useState([]); + const [fileList, setFileList] = useState([]); const submitPassword = async (password: string) => { await shareService.authenticateWithPassword(shareId, password).then(() => { @@ -25,7 +27,9 @@ const Share = () => { const getFiles = (password?: string) => shareService .get(shareId, password) - .then((files) => setShareList(files)) + .then((files) => { + setFileList(files); + }) .catch((e) => { const error = e.response.data.message; if (e.response.status == 404) { @@ -44,10 +48,17 @@ const Share = () => { return ( <> + + + ); diff --git a/src/types/File.type.ts b/src/types/File.type.ts index 0201ed1f..bda0a16f 100644 --- a/src/types/File.type.ts +++ b/src/types/File.type.ts @@ -3,5 +3,7 @@ import { Models } from "appwrite"; export type FileUpload = File & { uploadingState?: UploadState }; export type UploadState = "finished" | "inProgress" | undefined; -export type AppwriteFileWithPreview = Models.File & { preview: Buffer }; - +export interface AppwriteFileWithPreview extends Models.File { + uploadingState?: UploadState; + preview: Buffer; +}