You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

FileManager.ts 6.0KB

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