Merge branch 'comment-composer' into 'main'
[ProtonMail-WebClient.git] / packages / drive-store / store / _uploads / UploadProvider / uploadClientUid.ts
blob0257820be64b9adedfe6179dd0d1a6176439b816
1 import { captureMessage } from '@proton/shared/lib/helpers/sentry';
2 import { generateProtonWebUID } from '@proton/shared/lib/helpers/uid';
4 import { sendErrorReport } from '../../../utils/errorHandling';
6 enum UploadingState {
7     // File is being uploading - do not automatically replace file.
8     Uploading = 'uploading',
9     // File upload either failed or user closed the tab before upload
10     // finish - automatic replace is safe.
11     Failed = 'failed',
14 const KEY_PREFIX = 'upload-client-uid';
16 /**
17  * generateClientUid generates new client UID and two callbacks.
18  * One to be called when file is uploaded - that will remove the UID
19  * from local storage as it is not needed anymore.
20  * Second to be called when file failed to be uplaoded - that will
21  * change the state of the uploading UID in local storage to indicate
22  * it is safe to automatically replace the file draft.
23  * Note it will automatically set failed state when page is unloaded.
24  */
25 export function generateClientUid(clientUid?: string) {
26     // When the file is being replaced, we want to reuse the same UID
27     // so it is properly removed from the local storage after successfull
28     // upload.
29     if (!clientUid) {
30         clientUid = generateProtonWebUID();
31     }
32     const key = getStorageKey(clientUid);
34     // LocalStorage can fail when setting values, which will fail the
35     // transfer. We can ignore this.
36     //
37     // The worst case scenario would be not knowing the state, but it's
38     // better to at least upload the file than fail here.
39     try {
40         localStorage.setItem(key, UploadingState.Uploading);
41     } catch (e) {
42         handleLocalStorageError(e);
43     }
45     const uploadFailed = () => {
46         try {
47             localStorage.setItem(key, UploadingState.Failed);
48         } catch (e) {
49             handleLocalStorageError(e);
50         }
51         removeEventListener('unload', uploadFailed);
52     };
54     const uploadFinished = () => {
55         localStorage.removeItem(key);
56         removeEventListener('unload', uploadFailed);
57     };
59     // If file is not finished and page is closed, we consider it
60     // as failed upload which is safe to automatically replace with
61     // next upload attempt by the user.
62     addEventListener('unload', uploadFailed);
64     return {
65         clientUid,
66         uploadFailed,
67         uploadFinished,
68     };
71 /**
72  * isClientUidAvailable returns true only if the client UID is known
73  * by the client and file failed to be uploaded, that is we are sure
74  * it is safe to automatically replace.
75  * If the file is still being uploaded or is not known by the client,
76  * user needs to be notified about it and asked what to do.
77  */
78 export function isClientUidAvailable(clientUid: string): boolean {
79     const key = getStorageKey(clientUid);
80     const result = localStorage.getItem(key);
81     return result === UploadingState.Failed;
84 /**
85  * getStorageKey generates key to be used for local storage.
86  * Key should be unique enough to not be conflict with anything else.
87  */
88 function getStorageKey(uid: string) {
89     return `${KEY_PREFIX}-${uid}`;
92 function handleLocalStorageError(err: unknown) {
93     // When upload succeeds, the keys are removed. If user uploaded many files
94     // and all failed, it could fill the whole storage. If we cannot fit more
95     // keys anymore, lets simply remove them to keep the local storage
96     // functional. The worst case is we ask user to replace the draft.
97     if (isQuotaExceededError(err)) {
98         captureMessage('The local storage is full, deleting old failed upload statuses');
99         clearAllUploadUIDsFromLocalStorage();
100     } else {
101         sendErrorReport(err);
102     }
105 function isQuotaExceededError(err: unknown): boolean {
106     return (
107         err instanceof DOMException &&
108         // Most browsers
109         (err.code === 22 ||
110             err.name === 'QuotaExceededError' ||
111             // Firefox
112             err.code === 1014 ||
113             err.name === 'NS_ERROR_DOM_QUOTA_REACHED')
114     );
117 function clearAllUploadUIDsFromLocalStorage() {
118     Object.keys(localStorage).forEach((key) => {
119         if (key.startsWith(KEY_PREFIX)) {
120             localStorage.removeItem(key);
121         }
122     });