Files
mtdb_movie/common/resources/client/uploads/uploader/file-upload-store.ts
maher 703f50a09d
Some checks failed
Build / run (push) Has been cancelled
first commit
2025-10-29 11:42:25 +01:00

196 lines
6.2 KiB
TypeScript
Executable File

import {create} from 'zustand';
import {immer} from 'zustand/middleware/immer';
import {Draft, enableMapSet} from 'immer';
import {UploadedFile} from '../uploaded-file';
import {UploadStrategy, UploadStrategyConfig} from './strategy/upload-strategy';
import {MessageDescriptor} from '../../i18n/message-descriptor';
import {FileEntry} from '../file-entry';
import {S3MultipartUpload} from './strategy/s3-multipart-upload';
import {Settings} from '../../core/settings/settings';
import {TusUpload} from './strategy/tus-upload';
import {ProgressTimeout} from './progress-timeout';
import {startUploading} from './start-uploading';
import {createUpload} from './create-file-upload';
enableMapSet();
export interface FileUpload {
file: UploadedFile;
percentage: number;
bytesUploaded: number;
status: 'pending' | 'inProgress' | 'aborted' | 'failed' | 'completed';
errorMessage?: string | MessageDescriptor | null;
entry?: FileEntry;
request?: UploadStrategy;
timer?: ProgressTimeout;
options: UploadStrategyConfig;
meta?: unknown;
}
interface State {
concurrency: number;
fileUploads: Map<string, FileUpload>;
// uploads with pending and inProgress status
activeUploadsCount: number;
completedUploadsCount: number;
}
const initialState: State = {
concurrency: 3,
fileUploads: new Map(),
activeUploadsCount: 0,
completedUploadsCount: 0,
};
interface Actions {
uploadMultiple: (
files: (File | UploadedFile)[] | FileList,
options?: Omit<
UploadStrategyConfig,
// progress would be called for each upload simultaneously
'onProgress' | 'showToastOnRestrictionFail'
>,
) => string[];
uploadSingle: (
file: File | UploadedFile,
options?: UploadStrategyConfig,
) => string;
clearInactive: () => void;
abortUpload: (id: string) => void;
updateFileUpload: (id: string, state: Partial<FileUpload>) => void;
getUpload: (id: string) => FileUpload | undefined;
runQueue: () => void;
reset: () => void;
}
export type FileUploadState = State & Actions;
interface StoreProps {
settings: Settings;
}
export const createFileUploadStore = ({settings}: StoreProps) =>
create<FileUploadState>()(
immer((set, get) => {
return {
...initialState,
reset: () => {
set(initialState);
},
getUpload: uploadId => {
return get().fileUploads.get(uploadId);
},
clearInactive: () => {
set(state => {
state.fileUploads.forEach((upload, key) => {
if (upload.status !== 'inProgress') {
state.fileUploads.delete(key);
}
});
});
get().runQueue();
},
abortUpload: id => {
const upload = get().fileUploads.get(id);
if (upload) {
upload.request?.abort();
get().updateFileUpload(id, {status: 'aborted', percentage: 0});
get().runQueue();
}
},
updateFileUpload: (id, newUploadState) => {
set(state => {
const fileUpload = state.fileUploads.get(id);
if (fileUpload) {
state.fileUploads.set(id, {
...fileUpload,
...newUploadState,
});
// only need to update inProgress count if status of the uploads in queue change
if ('status' in newUploadState) {
updateTotals(state);
}
}
});
},
uploadSingle: (file, userOptions) => {
const upload = createUpload(file, userOptions);
const fileUploads = new Map(get().fileUploads);
fileUploads.set(upload.file.id, upload);
set(state => {
updateTotals(state);
state.fileUploads = fileUploads;
});
get().runQueue();
return upload.file.id;
},
uploadMultiple: (files, options) => {
// create file upload items from specified files
const uploads = new Map<string, FileUpload>(get().fileUploads);
[...files].forEach(file => {
const upload = createUpload(file, options);
uploads.set(upload.file.id, upload);
});
// set state only once, there might be thousands of files, don't want to trigger a rerender for each one
set(state => {
updateTotals(state);
state.fileUploads = uploads;
});
get().runQueue();
return [...uploads.keys()];
},
runQueue: async () => {
const uploads = [...get().fileUploads.values()];
const activeUploads = uploads.filter(u => u.status === 'inProgress');
let concurrency = get().concurrency;
if (
activeUploads.filter(
activeUpload =>
// only upload one file from folder at a time to avoid creating duplicate folders
activeUpload.file.relativePath ||
// only allow one s3 multipart upload at a time, it will already upload multiple parts in parallel
activeUpload.request instanceof S3MultipartUpload ||
// only allow one tus upload if file is larger than chunk size, tus will have parallel uploads already in that case
(activeUpload.request instanceof TusUpload &&
settings.uploads.chunk_size &&
activeUpload.file.size > settings.uploads.chunk_size),
).length
) {
concurrency = 1;
}
if (activeUploads.length < concurrency) {
//const pendingUploads = uploads.filter(u => u.status === 'pending');
//const next = pendingUploads.find(a => !!a.request);
const next = uploads.find(u => u.status === 'pending');
if (next) {
await startUploading(next, get());
}
}
},
};
}),
);
const updateTotals = (state: Draft<FileUploadState>) => {
state.completedUploadsCount = [...state.fileUploads.values()].filter(
u => u.status === 'completed',
).length;
state.activeUploadsCount = [...state.fileUploads.values()].filter(
u => u.status === 'inProgress' || u.status === 'pending',
).length;
};