123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249 |
- import { compressData } from "../../data/encode";
- import { mutateElement } from "../../element/mutateElement";
- import { isInitializedImageElement } from "../../element/typeChecks";
- import {
- ExcalidrawElement,
- ExcalidrawImageElement,
- FileId,
- InitializedExcalidrawImageElement,
- } from "../../element/types";
- import { t } from "../../i18n";
- import {
- BinaryFileData,
- BinaryFileMetadata,
- ExcalidrawImperativeAPI,
- BinaryFiles,
- } from "../../types";
-
- export class FileManager {
- /** files being fetched */
- private fetchingFiles = new Map<ExcalidrawImageElement["fileId"], true>();
- /** files being saved */
- private savingFiles = new Map<ExcalidrawImageElement["fileId"], true>();
- /* files already saved to persistent storage */
- private savedFiles = new Map<ExcalidrawImageElement["fileId"], true>();
- private erroredFiles = new Map<ExcalidrawImageElement["fileId"], true>();
-
- private _getFiles;
- private _saveFiles;
-
- constructor({
- getFiles,
- saveFiles,
- }: {
- getFiles: (
- fileIds: FileId[],
- ) => Promise<{
- loadedFiles: BinaryFileData[];
- erroredFiles: Map<FileId, true>;
- }>;
- saveFiles: (data: {
- addedFiles: Map<FileId, BinaryFileData>;
- }) => Promise<{
- savedFiles: Map<FileId, true>;
- erroredFiles: Map<FileId, true>;
- }>;
- }) {
- this._getFiles = getFiles;
- this._saveFiles = saveFiles;
- }
-
- /**
- * returns whether file is already saved or being processed
- */
- isFileHandled = (id: FileId) => {
- return (
- this.savedFiles.has(id) ||
- this.fetchingFiles.has(id) ||
- this.savingFiles.has(id) ||
- this.erroredFiles.has(id)
- );
- };
-
- isFileSaved = (id: FileId) => {
- return this.savedFiles.has(id);
- };
-
- saveFiles = async ({
- elements,
- files,
- }: {
- elements: readonly ExcalidrawElement[];
- files: BinaryFiles;
- }) => {
- const addedFiles: Map<FileId, BinaryFileData> = new Map();
-
- for (const element of elements) {
- if (
- isInitializedImageElement(element) &&
- files[element.fileId] &&
- !this.isFileHandled(element.fileId)
- ) {
- addedFiles.set(element.fileId, files[element.fileId]);
- this.savingFiles.set(element.fileId, true);
- }
- }
-
- try {
- const { savedFiles, erroredFiles } = await this._saveFiles({
- addedFiles,
- });
-
- for (const [fileId] of savedFiles) {
- this.savedFiles.set(fileId, true);
- }
-
- return {
- savedFiles,
- erroredFiles,
- };
- } finally {
- for (const [fileId] of addedFiles) {
- this.savingFiles.delete(fileId);
- }
- }
- };
-
- getFiles = async (
- ids: FileId[],
- ): Promise<{
- loadedFiles: BinaryFileData[];
- erroredFiles: Map<FileId, true>;
- }> => {
- if (!ids.length) {
- return {
- loadedFiles: [],
- erroredFiles: new Map(),
- };
- }
- for (const id of ids) {
- this.fetchingFiles.set(id, true);
- }
-
- try {
- const { loadedFiles, erroredFiles } = await this._getFiles(ids);
-
- for (const file of loadedFiles) {
- this.savedFiles.set(file.id, true);
- }
- for (const [fileId] of erroredFiles) {
- this.erroredFiles.set(fileId, true);
- }
-
- return { loadedFiles, erroredFiles };
- } finally {
- for (const id of ids) {
- this.fetchingFiles.delete(id);
- }
- }
- };
-
- /** a file element prevents unload only if it's being saved regardless of
- * its `status`. This ensures that elements who for any reason haven't
- * beed set to `saved` status don't prevent unload in future sessions.
- * Technically we should prevent unload when the origin client haven't
- * yet saved the `status` update to storage, but that should be taken care
- * of during regular beforeUnload unsaved files check.
- */
- shouldPreventUnload = (elements: readonly ExcalidrawElement[]) => {
- return elements.some((element) => {
- return (
- isInitializedImageElement(element) &&
- !element.isDeleted &&
- this.savingFiles.has(element.fileId)
- );
- });
- };
-
- /**
- * helper to determine if image element status needs updating
- */
- shouldUpdateImageElementStatus = (
- element: ExcalidrawElement,
- ): element is InitializedExcalidrawImageElement => {
- return (
- isInitializedImageElement(element) &&
- this.isFileSaved(element.fileId) &&
- element.status === "pending"
- );
- };
-
- reset() {
- this.fetchingFiles.clear();
- this.savingFiles.clear();
- this.savedFiles.clear();
- this.erroredFiles.clear();
- }
- }
-
- export const encodeFilesForUpload = async ({
- files,
- maxBytes,
- encryptionKey,
- }: {
- files: Map<FileId, BinaryFileData>;
- maxBytes: number;
- encryptionKey: string;
- }) => {
- const processedFiles: {
- id: FileId;
- buffer: Uint8Array;
- }[] = [];
-
- for (const [id, fileData] of files) {
- const buffer = new TextEncoder().encode(fileData.dataURL);
-
- const encodedFile = await compressData<BinaryFileMetadata>(buffer, {
- encryptionKey,
- metadata: {
- id,
- mimeType: fileData.mimeType,
- created: Date.now(),
- },
- });
-
- if (buffer.byteLength > maxBytes) {
- throw new Error(
- t("errors.fileTooBig", {
- maxSize: `${Math.trunc(maxBytes / 1024 / 1024)}MB`,
- }),
- );
- }
-
- processedFiles.push({
- id,
- buffer: encodedFile,
- });
- }
-
- return processedFiles;
- };
-
- export const updateStaleImageStatuses = (params: {
- excalidrawAPI: ExcalidrawImperativeAPI;
- erroredFiles: Map<FileId, true>;
- elements: readonly ExcalidrawElement[];
- }) => {
- if (!params.erroredFiles.size) {
- return;
- }
- params.excalidrawAPI.updateScene({
- elements: params.excalidrawAPI
- .getSceneElementsIncludingDeleted()
- .map((element) => {
- if (
- isInitializedImageElement(element) &&
- params.erroredFiles.has(element.fileId)
- ) {
- return mutateElement(
- element,
- {
- status: "error",
- },
- false,
- );
- }
- return element;
- }),
- });
- };
|