4da75eb68f
* 🌟 basic status, while uploading directories * 🐛 fix the infinite redirection when the token always expired
343 lines
10 KiB
TypeScript
343 lines
10 KiB
TypeScript
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
/* eslint-disable @typescript-eslint/ban-types */
|
|
import { v1 as uuid } from 'uuid';
|
|
|
|
import JWTStorage from '@features/auth/jwt-storage-service';
|
|
import { FileType, PendingFileType } from '@features/files/types/file';
|
|
import Resumable from '@features/files/utils/resumable';
|
|
import Logger from '@features/global/framework/logger-service';
|
|
import RouterServices from '@features/router/services/router-service';
|
|
import _ from 'lodash';
|
|
import FileUploadAPIClient from '../api/file-upload-api-client';
|
|
import { isPendingFileStatusPending } from '../utils/pending-files';
|
|
import { FileTreeObject } from "components/uploads/file-tree-utils";
|
|
import { DriveApiClient } from "features/drive/api-client/api-client";
|
|
|
|
export enum Events {
|
|
ON_CHANGE = 'notify',
|
|
}
|
|
|
|
const logger = Logger.getLogger('Services/FileUploadService');
|
|
class FileUploadService {
|
|
private pendingFiles: PendingFileType[] = [];
|
|
public currentTaskId = '';
|
|
private recoilHandler: Function = () => undefined;
|
|
private logger: Logger.Logger = Logger.getLogger('FileUploadService');
|
|
|
|
setRecoilHandler(handler: Function) {
|
|
this.recoilHandler = handler;
|
|
}
|
|
|
|
notify() {
|
|
const updatedState = this.pendingFiles.map((f: PendingFileType) => {
|
|
return {
|
|
id: f.id,
|
|
status: f.status,
|
|
progress: f.progress,
|
|
file: f.backendFile,
|
|
};
|
|
});
|
|
this.recoilHandler(_.cloneDeep(updatedState));
|
|
}
|
|
|
|
public async createDirectories(root: FileTreeObject['tree'], context: { companyId: string; parentId: string }) {
|
|
// Create all directories
|
|
const filesPerParentId: { [key: string]: File[] } = {};
|
|
filesPerParentId[context.parentId] = []
|
|
|
|
const traverserTreeLevel = async (tree: FileTreeObject['tree'], parentId: string) => {
|
|
for (const directory of Object.keys(tree)) {
|
|
if (tree[directory] instanceof File) {
|
|
logger.trace(`${directory} is a file, save it for future upload`);
|
|
filesPerParentId[parentId].push(tree[directory] as File);
|
|
} else {
|
|
logger.debug(`Create directory ${directory}`);
|
|
|
|
const item = {
|
|
company_id: context.companyId,
|
|
parent_id: parentId,
|
|
name: directory,
|
|
is_directory: true,
|
|
};
|
|
|
|
if (!this.pendingFiles.some(f => isPendingFileStatusPending(f.status))) {
|
|
//New upload task when all previous task is finished
|
|
this.currentTaskId = uuid();
|
|
}
|
|
const pendingFile: PendingFileType = {
|
|
id: uuid(),
|
|
status: 'pending',
|
|
progress: 0,
|
|
lastProgress: new Date().getTime(),
|
|
speed: 0,
|
|
uploadTaskId: this.currentTaskId,
|
|
originalFile: null,
|
|
backendFile: null,
|
|
resumable: null,
|
|
label: directory,
|
|
type: "file",
|
|
pausable: false,
|
|
};
|
|
|
|
this.pendingFiles.push(pendingFile);
|
|
this.notify();
|
|
|
|
try {
|
|
const driveItem = await DriveApiClient.create(context.companyId, { item: item, version: {}});
|
|
this.logger.debug(`Directory ${directory} created`);
|
|
pendingFile.status = 'success';
|
|
this.notify();
|
|
if (driveItem?.id) {
|
|
filesPerParentId[driveItem.id] = []
|
|
await traverserTreeLevel(tree[directory] as FileTreeObject['tree'], driveItem.id);
|
|
}
|
|
} catch (e) {
|
|
this.logger.error(e);
|
|
throw new Error('Could not create directory');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
await traverserTreeLevel(root, context.parentId);
|
|
return filesPerParentId;
|
|
}
|
|
|
|
public async upload(
|
|
fileList: File[],
|
|
options?: {
|
|
context?: any;
|
|
callback?: (file: FileType | null, context: any) => void;
|
|
},
|
|
): Promise<PendingFileType[]> {
|
|
const { companyId } = RouterServices.getStateFromRoute();
|
|
|
|
if (!fileList || !companyId) {
|
|
this.logger.log('FileList or companyId is undefined', [fileList, companyId]);
|
|
return [];
|
|
}
|
|
|
|
if (!this.pendingFiles.some(f => isPendingFileStatusPending(f.status))) {
|
|
//New upload task when all previous task is finished
|
|
this.currentTaskId = uuid();
|
|
}
|
|
|
|
for (const file of fileList) {
|
|
if (!file) continue;
|
|
|
|
const pendingFile: PendingFileType = {
|
|
id: uuid(),
|
|
status: 'pending',
|
|
progress: 0,
|
|
lastProgress: new Date().getTime(),
|
|
speed: 0,
|
|
uploadTaskId: this.currentTaskId,
|
|
originalFile: file,
|
|
backendFile: null,
|
|
resumable: null,
|
|
type: "file",
|
|
label: null,
|
|
pausable: true
|
|
};
|
|
|
|
this.pendingFiles.push(pendingFile);
|
|
|
|
this.notify();
|
|
|
|
// First we create the file object
|
|
const resource = (
|
|
await FileUploadAPIClient.upload(file, { companyId, ...(options?.context || {}) })
|
|
)?.resource;
|
|
|
|
if (!resource) {
|
|
throw new Error('A server error occured');
|
|
}
|
|
|
|
pendingFile.backendFile = resource;
|
|
this.notify();
|
|
|
|
// Then we overwrite the file object with resumable
|
|
pendingFile.resumable = this.getResumableInstance({
|
|
target: FileUploadAPIClient.getRoute({
|
|
companyId,
|
|
fileId: pendingFile.backendFile.id,
|
|
fullApiRouteUrl: true,
|
|
}),
|
|
query: {
|
|
thumbnail_sync: 1,
|
|
},
|
|
headers: {
|
|
Authorization: JWTStorage.getAutorizationHeader(),
|
|
},
|
|
});
|
|
|
|
pendingFile.resumable.addFile(file);
|
|
|
|
pendingFile.resumable.on('fileAdded', () => pendingFile.resumable.upload());
|
|
|
|
pendingFile.resumable.on('fileProgress', (f: any) => {
|
|
const bytesDelta =
|
|
(f.progress() - pendingFile.progress) * (pendingFile?.originalFile?.size || 0);
|
|
const timeDelta = new Date().getTime() - pendingFile.lastProgress;
|
|
|
|
// To avoid jumping time ?
|
|
if (timeDelta > 1000) {
|
|
pendingFile.speed = bytesDelta / timeDelta;
|
|
}
|
|
|
|
pendingFile.backendFile = f;
|
|
pendingFile.lastProgress = new Date().getTime();
|
|
pendingFile.progress = f.progress();
|
|
this.notify();
|
|
});
|
|
|
|
pendingFile.resumable.on('fileSuccess', (_f: any, message: string) => {
|
|
try {
|
|
pendingFile.backendFile = JSON.parse(message).resource;
|
|
pendingFile.status = 'success';
|
|
console.log('fileSuccess', options?.callback);
|
|
options?.callback?.(pendingFile.backendFile, options?.context || {});
|
|
this.notify();
|
|
} catch (e) {
|
|
logger.error(`Error on fileSuccess Event`, e);
|
|
}
|
|
});
|
|
|
|
pendingFile.resumable.on('fileError', () => {
|
|
pendingFile.status = 'error';
|
|
pendingFile.resumable.cancel();
|
|
options?.callback?.(null, options?.context || {});
|
|
this.notify();
|
|
});
|
|
}
|
|
|
|
return this.pendingFiles.filter(f => f.uploadTaskId === this.currentTaskId);
|
|
}
|
|
|
|
public async getFile({
|
|
companyId,
|
|
fileId,
|
|
}: {
|
|
fileId: string;
|
|
companyId: string;
|
|
}): Promise<FileType> {
|
|
return _.cloneDeep((await FileUploadAPIClient.get({ fileId, companyId }))?.resource);
|
|
}
|
|
|
|
public getPendingFile(id: string): PendingFileType {
|
|
return this.pendingFiles.filter(f => f.id === id)[0];
|
|
}
|
|
|
|
public getPendingFileByBackendId(id: string): PendingFileType {
|
|
return this.pendingFiles.filter(f => f.backendFile?.id && f.backendFile.id === id)[0];
|
|
}
|
|
|
|
public cancel(id: string, timeout = 1000) {
|
|
const fileToCancel = this.pendingFiles.filter(f => f.id === id)[0];
|
|
|
|
fileToCancel.status = 'cancel';
|
|
|
|
if (fileToCancel.resumable) {
|
|
fileToCancel.resumable.cancel();
|
|
this.notify();
|
|
|
|
if (fileToCancel.backendFile)
|
|
this.deleteOneFile({
|
|
companyId: fileToCancel.backendFile.company_id,
|
|
fileId: fileToCancel.backendFile.id,
|
|
});
|
|
}
|
|
|
|
setTimeout(() => {
|
|
this.pendingFiles = this.pendingFiles.filter(f => f.id !== id);
|
|
this.notify();
|
|
}, timeout);
|
|
}
|
|
|
|
public retry(id: string) {
|
|
const fileToRetry = this.pendingFiles.filter(f => f.id === id)[0];
|
|
|
|
if (fileToRetry.status === 'error') {
|
|
fileToRetry.status = 'pending';
|
|
fileToRetry.resumable.upload();
|
|
|
|
this.notify();
|
|
}
|
|
}
|
|
|
|
public pauseOrResume(id: string) {
|
|
const fileToCancel = this.pendingFiles.filter(f => f.id === id)[0];
|
|
|
|
fileToCancel.status !== 'pause'
|
|
? (fileToCancel.status = 'pause')
|
|
: (fileToCancel.status = 'pending');
|
|
fileToCancel.status === 'pause'
|
|
? fileToCancel.resumable.pause()
|
|
: fileToCancel.resumable.upload();
|
|
|
|
this.notify();
|
|
}
|
|
|
|
private getResumableInstance({
|
|
target,
|
|
headers,
|
|
chunkSize,
|
|
testChunks,
|
|
simultaneousUploads,
|
|
maxChunkRetries,
|
|
query,
|
|
}: {
|
|
target: string;
|
|
headers: { Authorization: string };
|
|
chunkSize?: number;
|
|
testChunks?: number;
|
|
simultaneousUploads?: number;
|
|
maxChunkRetries?: number;
|
|
query?: { [key: string]: any };
|
|
}) {
|
|
return new Resumable({
|
|
target,
|
|
headers,
|
|
chunkSize: chunkSize || 50000000,
|
|
testChunks: testChunks || false,
|
|
simultaneousUploads: simultaneousUploads || 5,
|
|
maxChunkRetries: maxChunkRetries || 2,
|
|
query,
|
|
});
|
|
}
|
|
|
|
public async deleteOneFile({
|
|
companyId,
|
|
fileId,
|
|
}: {
|
|
companyId: string;
|
|
fileId: string;
|
|
}): Promise<void> {
|
|
const response = await FileUploadAPIClient.delete({ companyId, fileId });
|
|
|
|
if (response.status === 'success') {
|
|
this.pendingFiles = this.pendingFiles.filter(f => f.backendFile?.id !== fileId);
|
|
this.notify();
|
|
} else {
|
|
logger.error(`Error while processing delete for file`, fileId);
|
|
}
|
|
}
|
|
|
|
public download({ companyId, fileId }: { companyId: string; fileId: string }): Promise<Blob> {
|
|
return FileUploadAPIClient.download({
|
|
companyId: companyId,
|
|
fileId: fileId,
|
|
});
|
|
}
|
|
|
|
|
|
public getDownloadRoute({ companyId, fileId }: { companyId: string; fileId: string }): string {
|
|
return FileUploadAPIClient.getDownloadRoute({
|
|
companyId: companyId,
|
|
fileId: fileId,
|
|
});
|
|
}
|
|
}
|
|
|
|
export default new FileUploadService();
|