Merge branch 'feat/rbf-wording' into 'main'
[ProtonMail-WebClient.git] / packages / drive-store / store / _uploads / UploadProvider / useUploadFolder.ts
blobdf1b7618d8569db15d5219bca8b64db62d5d2998
1 import { c } from 'ttag';
3 import { TransferCancel } from '../../../components/TransferManager/transfer';
4 import useQueuedFunction from '../../../hooks/util/useQueuedFunction';
5 import { isErrorDueToNameConflict } from '../../../utils/isErrorDueToNameConflict';
6 import { useLinkActions, useLinksActions } from '../../_links';
7 import type { UploadFolderControls } from '../interface';
8 import { TransferConflictStrategy } from '../interface';
9 import type { ConflictStrategyHandler } from './interface';
10 import useUploadHelper from './useUploadHelper';
12 type LogCallback = (message: string) => void;
14 interface Folder {
15     isNewFolder: boolean;
16     folderId: string;
17     folderName: string;
20 export default function useUploadFolder() {
21     const queuedFunction = useQueuedFunction();
22     const { createFolder } = useLinkActions();
23     const { deleteChildrenLinks } = useLinksActions();
24     const { findAvailableName, getLinkByName } = useUploadHelper();
26     const createEmptyFolder = async (
27         abortSignal: AbortSignal,
28         shareId: string,
29         parentId: string,
30         folderName: string,
31         modificationTime?: Date
32     ): Promise<Folder> => {
33         const folderId = await createFolder(abortSignal, shareId, parentId, folderName, modificationTime);
34         return {
35             folderId,
36             isNewFolder: true,
37             folderName,
38         };
39     };
41     const getFolder = async (
42         abortSignal: AbortSignal,
43         shareId: string,
44         parentId: string,
45         folderName: string
46     ): Promise<Folder> => {
47         const link = await getLinkByName(abortSignal, shareId, parentId, folderName);
48         if (!link) {
49             throw Error(c('Error').t`The original folder not found`);
50         }
51         if (link.isFile) {
52             throw Error(c('Error').t`File cannot be merged with folder`);
53         }
54         checkSignal(abortSignal, folderName);
55         return {
56             folderId: link.linkId,
57             isNewFolder: false,
58             folderName,
59         };
60     };
62     const replaceDraft = async (
63         abortSignal: AbortSignal,
64         shareId: string,
65         parentId: string,
66         linkId: string,
67         folderName: string,
68         modificationTime?: Date
69     ) => {
70         await deleteChildrenLinks(abortSignal, shareId, parentId, [linkId]);
71         return createEmptyFolder(abortSignal, shareId, parentId, folderName, modificationTime);
72     };
74     const handleNameConflict = async (
75         abortSignal: AbortSignal,
76         {
77             shareId,
78             parentId,
79             folderName,
80             modificationTime,
81             getFolderConflictStrategy,
82             log,
83         }: {
84             shareId: string;
85             parentId: string;
86             folderName: string;
87             modificationTime: Date | undefined;
88             getFolderConflictStrategy: ConflictStrategyHandler;
89             log: LogCallback;
90         },
91         { filename, draftLinkId }: { filename: string; draftLinkId?: string }
92     ) => {
93         log(`Name conflict`);
94         const link = await getLinkByName(abortSignal, shareId, parentId, folderName);
95         const originalIsFolder = link ? !link.isFile : false;
97         checkSignal(abortSignal, folderName);
98         const conflictStrategy = await getFolderConflictStrategy(abortSignal, !!draftLinkId, originalIsFolder);
99         log(`Conflict resolved with: ${conflictStrategy}`);
100         if (conflictStrategy === TransferConflictStrategy.Rename) {
101             log(`Creating new folder`);
102             return createEmptyFolder(abortSignal, shareId, parentId, filename, modificationTime);
103         }
104         if (conflictStrategy === TransferConflictStrategy.Replace) {
105             if (draftLinkId) {
106                 log(`Replacing draft`);
107                 return replaceDraft(abortSignal, shareId, parentId, draftLinkId, folderName, modificationTime);
108             }
109             log(`Merging folders`);
110             return getFolder(abortSignal, shareId, parentId, folderName);
111         }
112         if (conflictStrategy === TransferConflictStrategy.Skip) {
113             throw new TransferCancel({ message: c('Info').t`Transfer skipped for folder "${folderName}"` });
114         }
115         throw new Error(`Unknown conflict strategy: ${conflictStrategy}`);
116     };
118     const prepareFolderOptimistically = (
119         abortSignal: AbortSignal,
120         shareId: string,
121         parentId: string,
122         folderName: string,
123         modificationTime: Date | undefined,
124         getFolderConflictStrategy: ConflictStrategyHandler,
125         log: LogCallback
126     ): Promise<Folder> => {
127         const lowercaseName = folderName.toLowerCase();
129         return queuedFunction(`upload_empty_folder:${lowercaseName}`, async () => {
130             log(`Creating new folder`);
131             return createEmptyFolder(abortSignal, shareId, parentId, folderName, modificationTime).catch(
132                 async (err) => {
133                     if (isErrorDueToNameConflict(err)) {
134                         const {
135                             filename: newName,
136                             draftLinkId,
137                             clientUid,
138                         } = await findAvailableName(abortSignal, {
139                             shareId,
140                             parentLinkId: parentId,
141                             filename: folderName,
142                         });
143                         checkSignal(abortSignal, folderName);
144                         // Automatically replace file - previous draft was uploaded
145                         // by the same client.
146                         if (draftLinkId && clientUid) {
147                             log(`Automatically replacing draft`);
148                             return replaceDraft(abortSignal, shareId, parentId, draftLinkId, newName, modificationTime);
149                         }
151                         return handleNameConflict(
152                             abortSignal,
153                             {
154                                 shareId,
155                                 parentId,
156                                 folderName,
157                                 modificationTime,
158                                 getFolderConflictStrategy,
159                                 log,
160                             },
161                             {
162                                 filename: newName,
163                                 draftLinkId,
164                             }
165                         );
166                     }
167                     throw err;
168                 }
169             );
170         })();
171     };
173     const initFolderUpload = (
174         shareId: string,
175         parentId: string,
176         folderName: string,
177         modificationTime: Date | undefined,
178         getFolderConflictStrategy: ConflictStrategyHandler,
179         log: LogCallback
180     ): UploadFolderControls => {
181         const abortController = new AbortController();
182         return {
183             start: () => {
184                 return prepareFolderOptimistically(
185                     abortController.signal,
186                     shareId,
187                     parentId,
188                     folderName,
189                     modificationTime,
190                     getFolderConflictStrategy,
191                     log
192                 );
193             },
194             cancel: () => {
195                 abortController.abort();
196             },
197         };
198     };
200     return {
201         initFolderUpload,
202     };
205 function checkSignal(abortSignal: AbortSignal, name: string) {
206     if (abortSignal.aborted) {
207         throw new TransferCancel({ message: c('Info').t`Transfer canceled for folder "${name}"` });
208     }