1 import { useCallback, useMemo, useState } from 'react';
3 import { c } from 'ttag';
5 import { DS_STORE } from '@proton/shared/lib/drive/constants';
6 import generateUID from '@proton/utils/generateUID';
8 import { TransferState } from '../../../components/TransferManager/transfer';
12 isTransferInitializing,
14 } from '../../../utils/transfer';
15 import type { UploadFileItem, UploadFileList, UploadFolderItem } from '../interface';
28 import { UploadConflictError, UploadUserError } from './interface';
30 type LogCallback = (id: string, message: string) => void;
32 export default function useUploadQueue(log: LogCallback) {
33 const [queue, setQueue] = useState<UploadQueue[]>([]);
35 const fileUploads = useMemo((): FileUpload[] => {
36 const f = ({ files, folders }: { files: FileUpload[]; folders: FolderUpload[] }): FileUpload[] => {
37 return [...files, ...folders.flatMap(f)];
39 return queue.flatMap(f);
42 const folderUploads = useMemo((): FolderUpload[] => {
43 const f = ({ folders }: { folders: FolderUpload[] }): FolderUpload[] => {
44 return [...folders, ...folders.flatMap(f)];
46 return queue.flatMap(f);
49 const allUploads = useMemo((): (FileUpload | FolderUpload)[] => {
50 return [...fileUploads, ...folderUploads];
51 }, [fileUploads, folderUploads]);
53 const hasUploads = useMemo((): boolean => {
54 return allUploads.length > 0;
57 const { nextFileUpload, nextFolderUpload } = useMemo(() => {
58 let nextFileUpload: FileUploadReady | undefined;
59 let nextFolderUpload: FolderUploadReady | undefined;
61 const conflictingUpload = allUploads.some(isTransferConflict);
62 if (conflictingUpload) {
63 return { nextFileUpload, nextFolderUpload };
66 nextFileUpload = fileUploads.find((file) => isTransferPending(file) && file.parentId) as FileUploadReady;
67 nextFolderUpload = folderUploads.find(
68 (folder) => isTransferPending(folder) && folder.parentId
69 ) as FolderUploadReady;
70 return { nextFileUpload, nextFolderUpload };
71 }, [allUploads, fileUploads, folderUploads]);
73 const add = useCallback(
78 isForPhotos: boolean = false,
79 isSharedWithMe: boolean = false
81 return new Promise((resolve, reject) => {
83 const errors: Error[] = [];
84 const conflictErrors: UploadConflictError[] = [];
86 const queueItem = queue.find((item) => item.shareId === shareId && item.linkId === parentId) || {
92 for (const item of list) {
93 if ((item as UploadFileItem).file?.name === DS_STORE) {
97 addItemToQueue(log, shareId, queueItem, item, isForPhotos, isSharedWithMe);
99 if ((err as Error).name === 'UploadConflictError') {
100 conflictErrors.push(err);
107 ...queue.filter((item) => item.shareId !== shareId || item.linkId !== parentId),
111 if (conflictErrors.length > 0) {
112 errors.push(new UploadConflictError(conflictErrors[0].filename, conflictErrors.length - 1));
114 if (errors.length > 0) {
126 const update = useCallback(
128 idOrFilter: UpdateFilter,
129 newStateOrCallback: UpdateState,
130 { mimeType, name, error, folderId, originalIsDraft, originalIsFolder }: UpdateData = {},
131 callback?: UpdateCallback
133 const filter = convertFilterToFunction(idOrFilter);
134 const newStateCallback = convertNewStateToFunction(newStateOrCallback);
135 const updateFileOrFolder = <T extends FileUpload | FolderUpload>(item: T) => {
137 const newState = newStateCallback(item);
138 // If pause is set twice, prefer resumeState set already before
139 // to not be locked in paused state forever.
140 item.resumeState = newState === TransferState.Paused ? item.resumeState || item.state : undefined;
141 item.state = newState;
143 item.meta.mimeType = mimeType;
146 item.meta.filename = name;
148 if (originalIsDraft) {
149 item.originalIsDraft = originalIsDraft;
151 if (originalIsFolder) {
152 item.originalIsFolder = originalIsFolder;
156 item.numberOfErrors++;
158 log(item.id, `Updated queue (state: ${newState}, error: ${error || ''})`);
160 const updateFile = (file: FileUpload): FileUpload => {
162 updateFileOrFolder(file);
166 const updateFolder = (folder: FolderUpload): FolderUpload => {
167 if (filter(folder)) {
168 // When parent folder is canceled, all childs would hang up
169 // in initializing state - therefore we need to cancel
170 // recursively all children.
171 if (newStateCallback(folder) === TransferState.Canceled) {
172 folder = recursiveCancel(log, folder);
174 updateFileOrFolder(folder);
176 folder.linkId = folderId;
177 folder.files = folder.files.map((file) => {
180 `Updated child (state: ${
181 file.state === TransferState.Initializing ? TransferState.Pending : file.state
187 state: file.state === TransferState.Initializing ? TransferState.Pending : file.state,
190 folder.folders = folder.folders.map((folder) => {
193 `Updated child (state: ${
194 folder.state === TransferState.Initializing ? TransferState.Pending : folder.state
201 folder.state === TransferState.Initializing ? TransferState.Pending : folder.state,
206 folder.files = folder.files.map(updateFile);
207 folder.folders = folder.folders.map(updateFolder);
208 // When any child is restarted after parent folder is canceled,
209 // the child would hang up in initializing state - therefore we
210 // need to restart also all canceled parents of that child.
211 if (folder.state === TransferState.Canceled && hasInitializingUpload(folder)) {
212 folder.state = folder.parentId ? TransferState.Pending : TransferState.Initializing;
216 setQueue((queue) => [
217 ...queue.map((item) => {
218 item.files = item.files.map(updateFile);
219 item.folders = item.folders.map(updateFolder);
227 const updateState = useCallback(
228 (idOrFilter: UpdateFilter, newStateOrCallback: UpdateState) => {
229 update(idOrFilter, newStateOrCallback);
234 const updateWithData = useCallback(
235 (idOrFilter: UpdateFilter, newStateOrCallback: UpdateState, data: UpdateData = {}) => {
236 update(idOrFilter, newStateOrCallback, data);
241 const updateWithCallback = useCallback(
242 (idOrFilter: UpdateFilter, newStateOrCallback: UpdateState, callback: UpdateCallback) => {
243 update(idOrFilter, newStateOrCallback, {}, callback);
248 const remove = useCallback((idOrFilter: UpdateFilter, callback?: UpdateCallback) => {
249 const filter = convertFilterToFunction(idOrFilter);
250 const invertFilter: UpdateFilter = (item) => !filter(item);
252 setQueue((queue) => {
254 const recursiveCallback = (item: FolderUpload) => {
256 item.files.forEach((value) => callback(value));
257 item.folders.forEach(recursiveCallback);
259 const doCallback = (item: UploadQueue | FolderUpload) => {
260 item.files.filter(filter).forEach((value) => callback(value));
261 item.folders.filter(filter).forEach(recursiveCallback);
262 item.folders.forEach(doCallback);
264 queue.forEach(doCallback);
267 const doFilter = <T extends UploadQueue | FolderUpload>(item: T): T => {
268 item.files = item.files.filter(invertFilter);
269 item.folders = item.folders.filter(invertFilter).map(doFilter);
272 return [...queue.map(doFilter)];
276 const clear = useCallback(() => {
296 export function convertFilterToFunction(filterOrId: UpdateFilter) {
297 return typeof filterOrId === 'function' ? filterOrId : ({ id }: UpdateCallbackParams) => id === filterOrId;
300 function convertNewStateToFunction(newStateOrCallback: UpdateState) {
301 return typeof newStateOrCallback === 'function' ? newStateOrCallback : () => newStateOrCallback;
304 export function addItemToQueue(
307 newQueue: UploadQueue,
308 item: UploadFileItem | UploadFolderItem,
309 isForPhotos: boolean = false,
310 isSharedWithMe: boolean = false
312 const name = (item as UploadFileItem).file ? (item as UploadFileItem).file.name : (item as UploadFolderItem).folder;
314 throw new UploadUserError(c('Notification').t`File or folder is missing a name`);
317 const part = findUploadQueueFolder(newQueue, item.path);
318 if (isNameAlreadyUploading(part, name)) {
319 throw new UploadConflictError(name);
322 const id = generateUID();
323 const state = part.linkId ? TransferState.Pending : TransferState.Initializing;
324 const generalAttributes = {
327 parentId: part.linkId,
329 startDate: new Date(),
333 if ((item as UploadFileItem).file) {
334 const fileItem = item as UploadFileItem;
336 ...generalAttributes,
340 size: fileItem.file.size,
341 mimeType: fileItem.file.type,
347 `Added file to the queue (state: ${state}, parent: ${part.id}, type: ${fileItem.file.type}, size: ${fileItem.file.size} bytes)`
350 const folderItem = item as UploadFolderItem;
352 ...generalAttributes,
353 name: folderItem.folder,
354 modificationTime: folderItem.modificationTime,
358 filename: folderItem.folder,
364 log(id, `Added folder to the queue (state: ${state}, parent: ${part.id})`);
368 function findUploadQueueFolder(
369 part: UploadQueue | FolderUpload,
371 ): (UploadQueue & { id: string }) | FolderUpload {
372 if (path.length === 0) {
379 const nextStep = path[0];
380 const sortedMatchingFolders = part.folders
381 // Find all folders with the same name. This can happen in situation
382 // when user uploads folder and after its done, user uploads it again.
383 .filter(({ name }) => name === nextStep)
384 // Sort it by date, the latest one is at the beginning of the array.
385 // We want to add new uploads to the latest folder, not the one which
386 // was already finished before.
387 .sort((a, b) => b.startDate.getTime() - a.startDate.getTime());
388 // Folders can have the same startDate (mostly in unit test, probably not
389 // in real world), but lets explicitely always prefer non finished folder
391 const folder = sortedMatchingFolders.find((folder) => !isTransferFinished(folder)) || sortedMatchingFolders[0];
393 return findUploadQueueFolder(folder, path.slice(1));
396 throw new Error('Wrong file or folder structure');
399 function isNameAlreadyUploading(part: UploadQueue | FolderUpload, name: string): boolean {
400 const recursiveIsNotFinished = (upload: FolderUpload): boolean => {
402 !isTransferFinished(upload) ||
403 upload.files.some((upload) => !isTransferFinished(upload)) ||
404 upload.folders.some(recursiveIsNotFinished)
408 part.files.filter((upload) => !isTransferFinished(upload)).some(({ file }) => file.name === name) ||
409 part.folders.filter(recursiveIsNotFinished).some((folder) => folder.name === name)
413 function recursiveCancel(log: LogCallback, folder: FolderUpload): FolderUpload {
416 files: folder.files.map((file) => {
417 log(file.id, 'Canceled by parent');
420 state: TransferState.Canceled,
423 folders: folder.folders
425 log(folder.id, 'Canceled by parent');
428 state: TransferState.Canceled,
431 .map((subfolder) => recursiveCancel(log, subfolder)),
435 function hasInitializingUpload(folder: FolderUpload): boolean {
437 folder.files.some(isTransferInitializing) ||
438 folder.folders.some(isTransferInitializing) ||
439 folder.folders.some(hasInitializingUpload)