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
7 changed files with 265 additions and 16 deletions

View File

@@ -50,6 +50,12 @@ Without docker:
1. Run `npm install` 1. Run `npm install`
2. Run `npm run build && npm run start` 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 ## Contribute
You're very welcome to contribute to Pingvin Share! You're very welcome to contribute to Pingvin Share!

152
package-lock.json generated
View File

@@ -19,7 +19,9 @@
"axios": "^0.26.1", "axios": "^0.26.1",
"cookie": "^0.5.0", "cookie": "^0.5.0",
"cookies-next": "^2.0.4", "cookies-next": "^2.0.4",
"file-saver": "^2.0.5",
"js-file-download": "^0.4.12", "js-file-download": "^0.4.12",
"jszip": "^3.9.1",
"next": "12.1.5", "next": "12.1.5",
"next-pwa": "^5.5.2", "next-pwa": "^5.5.2",
"node-appwrite": "^5.1.0", "node-appwrite": "^5.1.0",
@@ -30,6 +32,7 @@
}, },
"devDependencies": { "devDependencies": {
"@types/cookie": "^0.5.0", "@types/cookie": "^0.5.0",
"@types/file-saver": "^2.0.5",
"@types/node": "17.0.23", "@types/node": "17.0.23",
"@types/react": "18.0.4", "@types/react": "18.0.4",
"@types/react-dom": "18.0.0", "@types/react-dom": "18.0.0",
@@ -2527,6 +2530,12 @@
"integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==", "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==",
"peer": true "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": { "node_modules/@types/glob": {
"version": "7.2.0", "version": "7.2.0",
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz",
@@ -4455,6 +4464,11 @@
"node": "^10.12.0 || >=12.0.0" "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": { "node_modules/file-selector": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.4.0.tgz", "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.4.0.tgz",
@@ -4895,6 +4909,11 @@
"node": ">= 4" "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": { "node_modules/import-fresh": {
"version": "3.3.0", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@@ -5364,6 +5383,44 @@
"node": ">=4.0" "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": { "node_modules/language-subtag-registry": {
"version": "0.3.21", "version": "0.3.21",
"resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz",
@@ -5400,6 +5457,14 @@
"node": ">= 0.8.0" "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": { "node_modules/lines-and-columns": {
"version": "1.2.4", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
@@ -5950,6 +6015,11 @@
"node": ">=4" "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": { "node_modules/parent-module": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -6597,6 +6667,14 @@
"randombytes": "^2.1.0" "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": { "node_modules/shebang-command": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -9469,6 +9547,12 @@
"integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==", "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==",
"peer": true "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": { "@types/glob": {
"version": "7.2.0", "version": "7.2.0",
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz",
@@ -10991,6 +11075,11 @@
"flat-cache": "^3.0.4" "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": { "file-selector": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.4.0.tgz", "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", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz",
"integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==" "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": { "import-fresh": {
"version": "3.3.0", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@@ -11648,6 +11742,46 @@
"object.assign": "^4.1.2" "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": { "language-subtag-registry": {
"version": "0.3.21", "version": "0.3.21",
"resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz",
@@ -11678,6 +11812,14 @@
"type-check": "~0.4.0" "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": { "lines-and-columns": {
"version": "1.2.4", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
@@ -12080,6 +12222,11 @@
"integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=",
"dev": true "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": { "parent-module": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -12536,6 +12683,11 @@
"randombytes": "^2.1.0" "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": { "shebang-command": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",

View File

@@ -22,7 +22,9 @@
"axios": "^0.26.1", "axios": "^0.26.1",
"cookie": "^0.5.0", "cookie": "^0.5.0",
"cookies-next": "^2.0.4", "cookies-next": "^2.0.4",
"file-saver": "^2.0.5",
"js-file-download": "^0.4.12", "js-file-download": "^0.4.12",
"jszip": "^3.9.1",
"next": "12.1.5", "next": "12.1.5",
"next-pwa": "^5.5.2", "next-pwa": "^5.5.2",
"node-appwrite": "^5.1.0", "node-appwrite": "^5.1.0",
@@ -33,6 +35,7 @@
}, },
"devDependencies": { "devDependencies": {
"@types/cookie": "^0.5.0", "@types/cookie": "^0.5.0",
"@types/file-saver": "^2.0.5",
"@types/node": "17.0.23", "@types/node": "17.0.23",
"@types/react": "18.0.4", "@types/react": "18.0.4",
"@types/react-dom": "18.0.0", "@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 Image from "next/image";
import { useRouter } from "next/router"; 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 { AppwriteFileWithPreview } from "../../types/File.type";
import aw from "../../utils/appwrite.util"; import aw from "../../utils/appwrite.util";
import { bytesToSize } from "../../utils/math/byteToSize.util"; import { bytesToSize } from "../../utils/math/byteToSize.util";
@@ -50,6 +50,13 @@ const FileList = ({
<td>{file.name}</td> <td>{file.name}</td>
<td>{bytesToSize(file.sizeOriginal)}</td> <td>{bytesToSize(file.sizeOriginal)}</td>
<td> <td>
{file.uploadingState ? (
file.uploadingState != "finished" ? (
<Loader size={22} />
) : (
<CircleCheck color="green" size={22} />
)
) : (
<ActionIcon <ActionIcon
size={25} size={25}
onClick={() => onClick={() =>
@@ -58,6 +65,7 @@ const FileList = ({
> >
<Download /> <Download />
</ActionIcon> </ActionIcon>
)}
</td> </td>
</tr> </tr>
)); ));

View File

@@ -1,7 +1,9 @@
import { Group } from "@mantine/core";
import { useModals } from "@mantine/modals"; import { useModals } from "@mantine/modals";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
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 FileList from "../../components/share/FileList"; import FileList from "../../components/share/FileList";
import showEnterPasswordModal from "../../components/share/showEnterPasswordModal"; import showEnterPasswordModal from "../../components/share/showEnterPasswordModal";
import showShareNotFoundModal from "../../components/share/showShareNotFoundModal"; import showShareNotFoundModal from "../../components/share/showShareNotFoundModal";
@@ -13,7 +15,7 @@ const Share = () => {
const router = useRouter(); const router = useRouter();
const modals = useModals(); const modals = useModals();
const shareId = router.query.shareId as string; const shareId = router.query.shareId as string;
const [shareList, setShareList] = useState<AppwriteFileWithPreview[]>([]); const [fileList, setFileList] = useState<AppwriteFileWithPreview[]>([]);
const submitPassword = async (password: string) => { const submitPassword = async (password: string) => {
await shareService.authenticateWithPassword(shareId, password).then(() => { await shareService.authenticateWithPassword(shareId, password).then(() => {
@@ -25,7 +27,9 @@ const Share = () => {
const getFiles = (password?: string) => const getFiles = (password?: string) =>
shareService shareService
.get(shareId, password) .get(shareId, password)
.then((files) => setShareList(files)) .then((files) => {
setFileList(files);
})
.catch((e) => { .catch((e) => {
const error = e.response.data.message; const error = e.response.data.message;
if (e.response.status == 404) { if (e.response.status == 404) {
@@ -44,10 +48,17 @@ const Share = () => {
return ( return (
<> <>
<Meta title={`Share ${shareId}`} /> <Meta title={`Share ${shareId}`} />
<FileList <Group position="right">
files={shareList} <DownloadAllButton
shareId={shareId} shareId={shareId}
isLoading={shareList.length == 0} files={fileList}
setFiles={setFileList}
/>
</Group>
<FileList
files={fileList}
shareId={shareId}
isLoading={fileList.length == 0}
/> />
</> </>
); );

View File

@@ -3,5 +3,7 @@ import { Models } from "appwrite";
export type FileUpload = File & { uploadingState?: UploadState }; export type FileUpload = File & { uploadingState?: UploadState };
export type UploadState = "finished" | "inProgress" | undefined; export type UploadState = "finished" | "inProgress" | undefined;
export type AppwriteFileWithPreview = Models.File & { preview: Buffer }; export interface AppwriteFileWithPreview extends Models.File {
uploadingState?: UploadState;
preview: Buffer;
}