1 import { useCallback, useRef } from 'react';
3 import { FILE_CHUNK_SIZE } from '@proton/shared/lib/drive/constants';
5 import type { TransferProgresses } from '../../../components/TransferManager/transfer';
6 import { TransferState } from '../../../components/TransferManager/transfer';
7 import { isTransferActive, isTransferFinalizing, isTransferPending, isTransferProgress } from '../../../utils/transfer';
8 import { MAX_BLOCKS_PER_UPLOAD } from '../constants';
9 import type { UploadFileControls, UploadFolderControls } from '../interface';
10 import type { FileUpload, UpdateCallback, UpdateFilter, UpdateState } from './interface';
12 export default function useUploadControl(
13 fileUploads: FileUpload[],
14 updateWithCallback: (idOrFilter: UpdateFilter, newState: UpdateState, callback: UpdateCallback) => void,
15 removeFromQueue: (idOrFilter: UpdateFilter, callback: UpdateCallback) => void,
16 clearQueue: () => void
18 // Controls keep references to ongoing uploads to have ability
19 // to pause or cancel them.
20 const controls = useRef<{ [id: string]: UploadFileControls | UploadFolderControls }>({});
21 const progresses = useRef<TransferProgresses>({});
23 const add = (id: string, uploadControls: UploadFileControls | UploadFolderControls) => {
24 controls.current[id] = uploadControls;
25 progresses.current[id] = 0;
28 const remove = (id: string) => {
29 delete controls.current[id];
30 delete progresses.current[id];
33 const updateProgress = (id: string, increment: number) => {
34 // Progress might be updated even when transfer is already finished and
35 // thus progress is not here anymore. In such case it is OK to simply
36 // ignore the call to not crash.
37 if (progresses.current[id] === undefined) {
40 progresses.current[id] += increment;
41 // Because increment can be float, some aritmetic operation can result
42 // in -0.0000000001 which would be then displayed as -0 after rounding.
43 if (progresses.current[id] < 0) {
44 progresses.current[id] = 0;
48 const getProgresses = () => ({ ...progresses.current });
50 const getProgress = (id: string) => progresses.current[id];
53 * calculateRemainingUploadBytes returns based on progresses of ongoing
54 * uploads how many data is planned to be uploaded to properly count the
55 * available space for next batch of files to be uploaded.
57 const calculateRemainingUploadBytes = (): number => {
58 return fileUploads.reduce((sum, upload) => {
59 if (!isTransferActive(upload) || !upload.file.size) {
62 // uploadedChunksSize counts only fully uploaded blocks. Fully
63 // uploaded blocks are counted into used space returned by API.
64 // The algorithm is not precise as file is uploaded in parallel,
65 // but this is what we can do without introducing complex
66 // computation. If better precision is needed, we need to keep
67 // track of each block, not the whole file.
68 const uploadedChunksSize =
69 progresses.current[upload.id] - (progresses.current[upload.id] % FILE_CHUNK_SIZE) || 0;
70 return sum + upload.file.size - uploadedChunksSize;
75 * calculateFileUploadLoad returns how many blocks are being currently
76 * uploaded by all ongoing uploads, considering into account the real
77 * state using the progresses.
79 const calculateFileUploadLoad = (): number => {
80 // Count both in-progress and finalizing transfers as the ones still
81 // running the worker and taking up some load. Without counting finalizing
82 // state and with the API being slow, we can keep around too many workers.
84 .filter((transfer) => isTransferProgress(transfer) || isTransferFinalizing(transfer))
85 .reduce((load, upload) => {
86 const remainingSize = (upload.file.size || 0) - (progresses.current[upload.id] || 0);
87 // Even if the file is empty, keep the minimum of blocks to 1,
88 // otherwise it would start too many threads.
89 const chunks = Math.max(Math.ceil(remainingSize / FILE_CHUNK_SIZE), 1);
90 const loadIncrease = Math.min(MAX_BLOCKS_PER_UPLOAD, chunks);
91 return load + loadIncrease;
95 const pauseUploads = useCallback(
96 (idOrFilter: UpdateFilter) => {
97 updateWithCallback(idOrFilter, TransferState.Paused, ({ id, state }) => {
98 if (isTransferProgress({ state }) || isTransferPending({ state })) {
99 (controls.current[id] as UploadFileControls)?.pause?.();
106 const resumeUploads = useCallback(
107 (idOrFilter: UpdateFilter) => {
110 ({ resumeState, parentId }) => {
111 // If the parent folder was created during the pause,
112 // go back to pending, not initializing state.
113 if (parentId && resumeState === TransferState.Initializing) {
114 return TransferState.Pending;
116 return resumeState || TransferState.Progress;
119 (controls.current[id] as UploadFileControls)?.resume?.();
126 const cancelUploads = useCallback(
127 (idOrFilter: UpdateFilter) => {
128 updateWithCallback(idOrFilter, TransferState.Canceled, ({ id }) => {
129 controls.current[id]?.cancel();
135 const removeUploads = useCallback(
136 (idOrFilter: UpdateFilter) => {
137 // We should never simply remove uploads, but cancel it first, so
138 // it does not continue on background without our knowledge.
139 cancelUploads(idOrFilter);
140 removeFromQueue(idOrFilter, ({ id }) => remove(id));
145 const clearUploads = useCallback(() => {
146 Object.entries(controls.current).map(([, uploadControls]) => uploadControls.cancel());
147 controls.current = {};
148 progresses.current = {};
158 calculateRemainingUploadBytes,
159 calculateFileUploadLoad,