0
Fork 0
mirror of https://github.com/stonith404/pingvin-share.git synced 2025-02-19 01:55:48 -05:00

Add function to download all files as a zip

This commit is contained in:
Elias Schneider 2022-04-30 23:30:23 +02:00
parent 7ddce593f9
commit b070f17d67
No known key found for this signature in database
GPG key ID: D5EC1C72D93244FD
7 changed files with 265 additions and 16 deletions

View file

@ -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!

152
package-lock.json generated
View file

@ -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",

View file

@ -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",

View file

@ -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<SetStateAction<AppwriteFileWithPreview[]>>;
}) => {
const [isLoading, setIsLoading] = useState<boolean>(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 (
<Tooltip
wrapLines
position="bottom"
width={220}
withArrow
label="Only available if your share is smaller than 150 MB."
>
<Button variant="outline" onClick={downloadAll} disabled>
Download all
</Button>
</Tooltip>
);
return (
<Button variant="outline" loading={isLoading} onClick={downloadAll}>
Download all
</Button>
);
};
export default DownloadAllButton;

View file

@ -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 = ({
<td>{file.name}</td>
<td>{bytesToSize(file.sizeOriginal)}</td>
<td>
<ActionIcon
size={25}
onClick={() =>
router.push(aw.storage.getFileDownload(shareId, file.$id))
}
>
<Download />
</ActionIcon>
{file.uploadingState ? (
file.uploadingState != "finished" ? (
<Loader size={22} />
) : (
<CircleCheck color="green" size={22} />
)
) : (
<ActionIcon
size={25}
onClick={() =>
router.push(aw.storage.getFileDownload(shareId, file.$id))
}
>
<Download />
</ActionIcon>
)}
</td>
</tr>
));

View file

@ -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<AppwriteFileWithPreview[]>([]);
const [fileList, setFileList] = useState<AppwriteFileWithPreview[]>([]);
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 (
<>
<Meta title={`Share ${shareId}`} />
<Group position="right">
<DownloadAllButton
shareId={shareId}
files={fileList}
setFiles={setFileList}
/>
</Group>
<FileList
files={shareList}
files={fileList}
shareId={shareId}
isLoading={shareList.length == 0}
isLoading={fileList.length == 0}
/>
</>
);

View file

@ -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;
}