Merge branch 'MAILWEB-6067-improve-circular-dependencies-prevention' into 'main'
[ProtonMail-WebClient.git] / packages / drive-store / store / _uploads / initUploadFileWorker.ts
blob199d1eb7ccc23e18e6315edd678e7972acb391d0
1 import { TransferCancel } from '../../components/TransferManager/transfer';
2 import { sendErrorReport } from '../../utils/errorHandling';
3 import type {
4     FileKeys,
5     FileRequestBlock,
6     PhotoUpload,
7     ThumbnailRequestBlock,
8     UploadCallbacks,
9     UploadFileControls,
10     UploadFileProgressCallbacks,
11 } from './interface';
12 import { getMediaInfo } from './media';
13 import { mimeTypeFromFile } from './mimeTypeParser/mimeTypeParser';
14 import { UploadWorkerController } from './workerController';
16 type LogCallback = (message: string) => void;
18 class TransferRetry extends Error {
19     constructor(options: { message: string }) {
20         super(options.message);
21         this.name = 'TransferRetry';
22     }
25 export function initUploadFileWorker(
26     file: File,
27     isForPhotos: boolean,
28     {
29         initialize,
30         createFileRevision,
31         createBlockLinks,
32         getVerificationData,
33         finalize,
34         onError,
35         notifyVerificationError,
36     }: UploadCallbacks,
37     log: LogCallback
38 ): UploadFileControls {
39     const abortController = new AbortController();
40     let workerApi: UploadWorkerController;
42     // Start detecting mime type right away to have this information once the
43     // upload starts, so we can generate thumbnail as fast as possible without
44     // need to wait for creation of revision on API.
45     const mimeTypePromise = mimeTypeFromFile(file);
47     const start = async ({ onInit, onProgress, onNetworkError, onFinalize }: UploadFileProgressCallbacks = {}) => {
48         // Worker has a slight overhead about 40 ms. Let's start generating
49         // thumbnail a bit sooner.
50         const mediaInfoPromise = getMediaInfo(mimeTypePromise, file, isForPhotos);
52         return new Promise<void>((resolve, reject) => {
53             const worker = new Worker(
54                 /* webpackChunkName: "drive-worker" */
55                 /* webpackPrefetch: true */
56                 /* webpackPreload: true */
57                 new URL('./worker/worker.ts', import.meta.url)
58             );
60             workerApi = new UploadWorkerController(worker, log, {
61                 keysGenerated: (keys: FileKeys) => {
62                     mimeTypePromise
63                         .then(async (mimeType) => {
64                             return createFileRevision(abortController.signal, mimeType, keys).then(
65                                 async (fileRevision) => {
66                                     onInit?.(mimeType, fileRevision.fileName);
68                                     return Promise.all([
69                                         mediaInfoPromise,
70                                         getVerificationData(abortController.signal),
71                                     ]).then(async ([mediaInfo, verificationData]) => {
72                                         await workerApi.postStart(
73                                             file,
74                                             {
75                                                 mimeType,
76                                                 isForPhotos,
77                                                 media: {
78                                                     width: mediaInfo?.width,
79                                                     height: mediaInfo?.height,
80                                                     duration: mediaInfo?.duration,
81                                                 },
82                                                 thumbnails: mediaInfo?.thumbnails,
83                                             },
84                                             fileRevision.address.privateKey,
85                                             fileRevision.address.email,
86                                             fileRevision.privateKey,
87                                             fileRevision.sessionKey,
88                                             fileRevision.parentHashKey,
89                                             verificationData
90                                         );
91                                     });
92                                 }
93                             );
94                         })
95                         .catch(reject);
96                 },
97                 createBlocks: (fileBlocks: FileRequestBlock[], thumbnailBlocks?: ThumbnailRequestBlock[]) => {
98                     createBlockLinks(abortController.signal, fileBlocks, thumbnailBlocks)
99                         .then(({ fileLinks, thumbnailLinks }) => workerApi.postCreatedBlocks(fileLinks, thumbnailLinks))
100                         .catch(reject);
101                 },
102                 onProgress: (increment: number) => {
103                     onProgress?.(increment);
104                 },
105                 finalize: (signature: string, signatureAddress: string, xattr: string, photo?: PhotoUpload) => {
106                     onFinalize?.();
107                     finalize(signature, signatureAddress, xattr, photo).then(resolve).catch(reject);
108                 },
109                 onNetworkError: (error: Error) => {
110                     onNetworkError?.(error);
111                 },
112                 onError: (error: Error) => {
113                     reject(error);
114                 },
115                 notifySentry: (error: Error) => {
116                     sendErrorReport(error);
117                 },
118                 notifyVerificationError: (retryHelped: boolean) => {
119                     notifyVerificationError(retryHelped);
120                 },
121                 onCancel: () => {
122                     reject(new TransferCancel({ message: `Transfer canceled for ${file.name}` }));
123                 },
124                 onHeartbeatTimeout: () => {
125                     reject(new TransferRetry({ message: `Heartbeat timeout` }));
126                 },
127             });
129             initialize(abortController.signal)
130                 .then(async ({ addressPrivateKey, parentPrivateKey }) => {
131                     await workerApi.postGenerateKeys(addressPrivateKey, parentPrivateKey);
132                 })
133                 .catch(reject);
134         });
135     };
137     const pause = async () => {
138         workerApi?.postPause();
139     };
141     const resume = async () => {
142         workerApi?.postResume();
143     };
145     const cancel = async () => {
146         abortController.abort();
147         workerApi?.cancel();
148     };
150     return {
151         start: (progressCallbacks?: UploadFileProgressCallbacks) =>
152             start(progressCallbacks)
153                 .catch((err) => {
154                     abortController.abort();
155                     onError?.(err);
156                     throw err;
157                 })
158                 .finally(() => {
159                     workerApi?.postClose();
160                     // We give some time to the worker to `close()` itself, to safely erase the stored private keys.
161                     // We still forcefully terminate it after a few seconds, in case the worker is unexpectedly stuck
162                     // in a bad state, hence couldn't close itself.
163                     setTimeout(() => {
164                         workerApi?.terminate();
165                     }, 5000);
166                 }),
167         cancel,
168         pause,
169         resume,
170     };