FileManager.ts 5.9 KB

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