FileManager.ts 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. import { StoreAction } from "../../packages/excalidraw";
  2. import { compressData } from "../../packages/excalidraw/data/encode";
  3. import { newElementWith } from "../../packages/excalidraw/element/mutateElement";
  4. import { isInitializedImageElement } from "../../packages/excalidraw/element/typeChecks";
  5. import type {
  6. ExcalidrawElement,
  7. ExcalidrawImageElement,
  8. FileId,
  9. InitializedExcalidrawImageElement,
  10. } from "../../packages/excalidraw/element/types";
  11. import { t } from "../../packages/excalidraw/i18n";
  12. import type {
  13. BinaryFileData,
  14. BinaryFileMetadata,
  15. ExcalidrawImperativeAPI,
  16. BinaryFiles,
  17. } from "../../packages/excalidraw/types";
  18. export class FileManager {
  19. /** files being fetched */
  20. private fetchingFiles = new Map<ExcalidrawImageElement["fileId"], true>();
  21. /** files being saved */
  22. private savingFiles = new Map<ExcalidrawImageElement["fileId"], true>();
  23. /* files already saved to persistent storage */
  24. private savedFiles = new Map<ExcalidrawImageElement["fileId"], true>();
  25. private erroredFiles = new Map<ExcalidrawImageElement["fileId"], true>();
  26. private _getFiles;
  27. private _saveFiles;
  28. constructor({
  29. getFiles,
  30. saveFiles,
  31. }: {
  32. getFiles: (fileIds: FileId[]) => Promise<{
  33. loadedFiles: BinaryFileData[];
  34. erroredFiles: Map<FileId, true>;
  35. }>;
  36. saveFiles: (data: { addedFiles: Map<FileId, BinaryFileData> }) => Promise<{
  37. savedFiles: Map<FileId, true>;
  38. erroredFiles: Map<FileId, true>;
  39. }>;
  40. }) {
  41. this._getFiles = getFiles;
  42. this._saveFiles = saveFiles;
  43. }
  44. /**
  45. * returns whether file is already saved or being processed
  46. */
  47. isFileHandled = (id: FileId) => {
  48. return (
  49. this.savedFiles.has(id) ||
  50. this.fetchingFiles.has(id) ||
  51. this.savingFiles.has(id) ||
  52. this.erroredFiles.has(id)
  53. );
  54. };
  55. isFileSaved = (id: FileId) => {
  56. return this.savedFiles.has(id);
  57. };
  58. saveFiles = async ({
  59. elements,
  60. files,
  61. }: {
  62. elements: readonly ExcalidrawElement[];
  63. files: BinaryFiles;
  64. }) => {
  65. const addedFiles: Map<FileId, BinaryFileData> = new Map();
  66. for (const element of elements) {
  67. if (
  68. isInitializedImageElement(element) &&
  69. files[element.fileId] &&
  70. !this.isFileHandled(element.fileId)
  71. ) {
  72. addedFiles.set(element.fileId, files[element.fileId]);
  73. this.savingFiles.set(element.fileId, true);
  74. }
  75. }
  76. try {
  77. const { savedFiles, erroredFiles } = await this._saveFiles({
  78. addedFiles,
  79. });
  80. for (const [fileId] of savedFiles) {
  81. this.savedFiles.set(fileId, true);
  82. }
  83. return {
  84. savedFiles,
  85. erroredFiles,
  86. };
  87. } finally {
  88. for (const [fileId] of addedFiles) {
  89. this.savingFiles.delete(fileId);
  90. }
  91. }
  92. };
  93. getFiles = async (
  94. ids: FileId[],
  95. ): Promise<{
  96. loadedFiles: BinaryFileData[];
  97. erroredFiles: Map<FileId, true>;
  98. }> => {
  99. if (!ids.length) {
  100. return {
  101. loadedFiles: [],
  102. erroredFiles: new Map(),
  103. };
  104. }
  105. for (const id of ids) {
  106. this.fetchingFiles.set(id, true);
  107. }
  108. try {
  109. const { loadedFiles, erroredFiles } = await this._getFiles(ids);
  110. for (const file of loadedFiles) {
  111. this.savedFiles.set(file.id, true);
  112. }
  113. for (const [fileId] of erroredFiles) {
  114. this.erroredFiles.set(fileId, true);
  115. }
  116. return { loadedFiles, erroredFiles };
  117. } finally {
  118. for (const id of ids) {
  119. this.fetchingFiles.delete(id);
  120. }
  121. }
  122. };
  123. /** a file element prevents unload only if it's being saved regardless of
  124. * its `status`. This ensures that elements who for any reason haven't
  125. * beed set to `saved` status don't prevent unload in future sessions.
  126. * Technically we should prevent unload when the origin client haven't
  127. * yet saved the `status` update to storage, but that should be taken care
  128. * of during regular beforeUnload unsaved files check.
  129. */
  130. shouldPreventUnload = (elements: readonly ExcalidrawElement[]) => {
  131. return elements.some((element) => {
  132. return (
  133. isInitializedImageElement(element) &&
  134. !element.isDeleted &&
  135. this.savingFiles.has(element.fileId)
  136. );
  137. });
  138. };
  139. /**
  140. * helper to determine if image element status needs updating
  141. */
  142. shouldUpdateImageElementStatus = (
  143. element: ExcalidrawElement,
  144. ): element is InitializedExcalidrawImageElement => {
  145. return (
  146. isInitializedImageElement(element) &&
  147. this.isFileSaved(element.fileId) &&
  148. element.status === "pending"
  149. );
  150. };
  151. reset() {
  152. this.fetchingFiles.clear();
  153. this.savingFiles.clear();
  154. this.savedFiles.clear();
  155. this.erroredFiles.clear();
  156. }
  157. }
  158. export const encodeFilesForUpload = async ({
  159. files,
  160. maxBytes,
  161. encryptionKey,
  162. }: {
  163. files: Map<FileId, BinaryFileData>;
  164. maxBytes: number;
  165. encryptionKey: string;
  166. }) => {
  167. const processedFiles: {
  168. id: FileId;
  169. buffer: Uint8Array;
  170. }[] = [];
  171. for (const [id, fileData] of files) {
  172. const buffer = new TextEncoder().encode(fileData.dataURL);
  173. const encodedFile = await compressData<BinaryFileMetadata>(buffer, {
  174. encryptionKey,
  175. metadata: {
  176. id,
  177. mimeType: fileData.mimeType,
  178. created: Date.now(),
  179. lastRetrieved: Date.now(),
  180. },
  181. });
  182. if (buffer.byteLength > maxBytes) {
  183. throw new Error(
  184. t("errors.fileTooBig", {
  185. maxSize: `${Math.trunc(maxBytes / 1024 / 1024)}MB`,
  186. }),
  187. );
  188. }
  189. processedFiles.push({
  190. id,
  191. buffer: encodedFile,
  192. });
  193. }
  194. return processedFiles;
  195. };
  196. export const updateStaleImageStatuses = (params: {
  197. excalidrawAPI: ExcalidrawImperativeAPI;
  198. erroredFiles: Map<FileId, true>;
  199. elements: readonly ExcalidrawElement[];
  200. }) => {
  201. if (!params.erroredFiles.size) {
  202. return;
  203. }
  204. params.excalidrawAPI.updateScene({
  205. elements: params.excalidrawAPI
  206. .getSceneElementsIncludingDeleted()
  207. .map((element) => {
  208. if (
  209. isInitializedImageElement(element) &&
  210. params.erroredFiles.has(element.fileId)
  211. ) {
  212. return newElementWith(element, {
  213. status: "error",
  214. });
  215. }
  216. return element;
  217. }),
  218. storeAction: StoreAction.UPDATE,
  219. });
  220. };