mirror of
				https://github.com/excalidraw/excalidraw.git
				synced 2025-11-04 04:44:31 +01:00 
			
		
		
		
	* feat: decouple package deps and introduce yarn workspaces * update root directory * fix * fix scripts * fix lint * update path in scripts * remove yarn.lock files from packages * ignore workspace * dummy * dummy * remove comment check * revert workflow changes * ignore ws when installing gh actions * remove log * update path * fix * fix typo
		
			
				
	
	
		
			243 lines
		
	
	
		
			6.1 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			243 lines
		
	
	
		
			6.1 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
import { compressData } from "../../packages/excalidraw/data/encode";
 | 
						|
import { newElementWith } from "../../packages/excalidraw/element/mutateElement";
 | 
						|
import { isInitializedImageElement } from "../../packages/excalidraw/element/typeChecks";
 | 
						|
import {
 | 
						|
  ExcalidrawElement,
 | 
						|
  ExcalidrawImageElement,
 | 
						|
  FileId,
 | 
						|
  InitializedExcalidrawImageElement,
 | 
						|
} from "../../packages/excalidraw/element/types";
 | 
						|
import { t } from "../../packages/excalidraw/i18n";
 | 
						|
import {
 | 
						|
  BinaryFileData,
 | 
						|
  BinaryFileMetadata,
 | 
						|
  ExcalidrawImperativeAPI,
 | 
						|
  BinaryFiles,
 | 
						|
} from "../../packages/excalidraw/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(),
 | 
						|
        lastRetrieved: 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 newElementWith(element, {
 | 
						|
            status: "error",
 | 
						|
          });
 | 
						|
        }
 | 
						|
        return element;
 | 
						|
      }),
 | 
						|
  });
 | 
						|
};
 |