From 4da75eb68f675c744801fac73c51f26c42e6a537 Mon Sep 17 00:00:00 2001 From: Anton Shepilov Date: Fri, 6 Oct 2023 16:23:42 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=8C=9Fbasic=20status,=20while=20uploading?= =?UTF-8?q?=20directories=20(#216)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🌟 basic status, while uploading directories * 🐛 fix the infinite redirection when the token always expired --- .../src/services/documents/services/index.ts | 8 +- .../pending-file-row.tsx | 25 +++++- .../frontend/src/app/environment/version.ts | 4 +- .../src/app/features/auth/auth-service.ts | 5 +- .../internal-auth-provider-service.ts | 31 ++++--- .../console/api/console-api-client.ts | 6 +- .../drive/hooks/use-drive-actions.tsx | 6 +- .../features/drive/hooks/use-drive-upload.tsx | 44 +++------ .../files/services/file-upload-service.ts | 90 +++++++++++++++++-- .../src/app/features/files/types/file.ts | 8 +- .../app/features/files/utils/pending-files.ts | 10 +-- 11 files changed, 162 insertions(+), 75 deletions(-) diff --git a/tdrive/backend/node/src/services/documents/services/index.ts b/tdrive/backend/node/src/services/documents/services/index.ts index 724901a1..2bc22466 100644 --- a/tdrive/backend/node/src/services/documents/services/index.ts +++ b/tdrive/backend/node/src/services/documents/services/index.ts @@ -319,8 +319,11 @@ export class DocumentsService { driveItem.last_version_cache = driveItemVersion; await this.repository.save(driveItem); + + //TODO[ASH] update item size only for files, there is not need to do during direcotry creation await updateItemSize(driveItem.parent_id, this.repository, context); + //TODO[ASH] there is no need to notify websocket, until we implement user notification await this.notifyWebsocket(driveItem.parent_id, context); await globalResolver.platformServices.messageQueue.publish( @@ -459,6 +462,7 @@ export class DocumentsService { * deletes or moves to Trash a Drive Document and its children * * @param {string} id - the item id + * @param item * @param {DriveExecutionContext} context - the execution context * @returns {Promise} */ @@ -569,10 +573,10 @@ export class DocumentsService { } await updateItemSize(previousParentId, this.repository, context); - this.notifyWebsocket(previousParentId, context); + await this.notifyWebsocket(previousParentId, context); } - this.notifyWebsocket("trash", context); + await this.notifyWebsocket("trash", context); }; /** diff --git a/tdrive/frontend/src/app/components/file-uploads/pending-file-components/pending-file-row.tsx b/tdrive/frontend/src/app/components/file-uploads/pending-file-components/pending-file-row.tsx index 545fda1e..c7a5d645 100644 --- a/tdrive/frontend/src/app/components/file-uploads/pending-file-components/pending-file-row.tsx +++ b/tdrive/frontend/src/app/components/file-uploads/pending-file-components/pending-file-row.tsx @@ -67,7 +67,7 @@ export default ({ pendingFileState, pendingFile }: PropsType) => { }} > - {pendingFile?.originalFile.name ? ( + {pendingFile?.originalFile?.name ? ( { }} /> )} + {pendingFile?.label ? ( + + + {pendingFile?.label} + + + ) : ( +
+ )} ((resolve, reject) => { - this.signinIn = true; - this.logger.log("Sign in in console"); - ConsoleAPIClient.login( + this.signinIn = true; + this.logger.log("Sign in in console"); + try { + const access_token = await ConsoleAPIClient.login( { email: params.username, password: params.password, remember_me: params.remember_me, }, true, - ) - .then(accessToken => (accessToken ? resolve() : reject(new Error('Can not login')))) - .catch(err => { - this.logger.error('Error on login', err); - reject(new Error('Can not login')); - }) - .finally(() => { - this.signinIn = false; - }); - }); + ); + this.logger.debug("Got access token from server"); + await this.params?.onNewToken(access_token); + this.signinIn = true; + } catch (e) { + this.signinIn = false; + throw new Error('Can not login'); + } finally { + this.signinIn = false; + } + } async signOut(params: SignOutParameters): Promise { diff --git a/tdrive/frontend/src/app/features/console/api/console-api-client.ts b/tdrive/frontend/src/app/features/console/api/console-api-client.ts index de95b9c3..d5d8c1ab 100644 --- a/tdrive/frontend/src/app/features/console/api/console-api-client.ts +++ b/tdrive/frontend/src/app/features/console/api/console-api-client.ts @@ -31,8 +31,8 @@ class ConsoleAPIClient { logger = Logger.getLogger('ConsoleAPIClient'); - login(params: LoginParams, disableJWTAuthentication = false): Promise { - return Api.post( + login(params: LoginParams, disableJWTAuthentication = false): Promise { + return Api.post( '/internal/services/console/v1/login', { ...params, ...{ device: {} } }, undefined, @@ -90,7 +90,7 @@ class ConsoleAPIClient { response.access_token ? resolve(response.access_token) : reject(new Error('Can not get access token')); - }); + }, false, {disableJWTAuthentication: true}); }); } } diff --git a/tdrive/frontend/src/app/features/drive/hooks/use-drive-actions.tsx b/tdrive/frontend/src/app/features/drive/hooks/use-drive-actions.tsx index cd4dbdc8..c4a0dbac 100644 --- a/tdrive/frontend/src/app/features/drive/hooks/use-drive-actions.tsx +++ b/tdrive/frontend/src/app/features/drive/hooks/use-drive-actions.tsx @@ -10,7 +10,6 @@ import Languages from 'features/global/services/languages-service'; /** * Returns the children of a drive item - * @param id * @returns */ export const useDriveActions = () => { @@ -47,11 +46,12 @@ export const useDriveActions = () => { const create = useCallback( async (item: Partial, version: Partial) => { + if (!item || !version) throw new Error("All "); let driveFile = null; if (!item.company_id) item.company_id = companyId; try { driveFile = await DriveApiClient.create(companyId, { item, version }); - await refresh(item.parent_id!); + await refresh(driveFile.parent_id!); } catch (e) { ToasterService.error(Languages.t('hooks.use-drive-actions.unable_create_file')); } @@ -63,7 +63,7 @@ export const useDriveActions = () => { const download = useCallback( async (id: string, versionId?: string) => { try { - const url = await DriveApiClient.getDownloadUrl(companyId, id, versionId); + const url = DriveApiClient.getDownloadUrl(companyId, id, versionId); (window as any).open(url, '_blank').focus(); } catch (e) { ToasterService.error(Languages.t('hooks.use-drive-actions.unable_download_file')); diff --git a/tdrive/frontend/src/app/features/drive/hooks/use-drive-upload.tsx b/tdrive/frontend/src/app/features/drive/hooks/use-drive-upload.tsx index f41dcbf1..aecac709 100644 --- a/tdrive/frontend/src/app/features/drive/hooks/use-drive-upload.tsx +++ b/tdrive/frontend/src/app/features/drive/hooks/use-drive-upload.tsx @@ -3,14 +3,16 @@ import FileUploadService from '@features/files/services/file-upload-service'; import { ToasterService } from '@features/global/services/toaster-service'; import { DriveApiClient } from '../api-client/api-client'; import { useDriveActions } from './use-drive-actions'; +import Logger from '@features/global/framework/logger-service'; /** * Returns the children of a drive item - * @param id * @returns */ export const useDriveUpload = () => { - const { create } = useDriveActions(); + const { create, refresh } = useDriveActions(); + + const logger = Logger.getLogger('useDriveUpload') const uploadVersion = async (file: File, context: { companyId: string; id: string }) => { return new Promise(r => { @@ -45,47 +47,23 @@ export const useDriveUpload = () => { tree: FileTreeObject, context: { companyId: string; parentId: string }, ) => { - const filesPerParentId: { [key: string]: File[] } = {}; - // Create all directories - console.debug("Start creating directories ..."); - const createDirectories = async (tree: FileTreeObject['tree'], parentId: string) => { - for (const directory of Object.keys(tree)) { - if (tree[directory] instanceof File) { - if (!filesPerParentId[parentId]) filesPerParentId[parentId] = []; - filesPerParentId[parentId].push(tree[directory] as File); - } else { - console.debug(`Create directory ${directory}`); - const driveItem = await create( - { - company_id: context.companyId, - parent_id: parentId, - name: directory, - is_directory: true, - }, - {}, - ); - console.debug(`Directory ${directory} created`); - if (driveItem?.id) { - await createDirectories(tree[directory] as FileTreeObject['tree'], driveItem.id); - } else { - throw new Error('Could not create directory'); - } - } - } - }; - await createDirectories(tree.tree, context.parentId); - console.debug("All directories created"); + logger.debug("Start creating directories ..."); + const filesPerParentId = await FileUploadService.createDirectories(tree.tree, context); + await refresh(context.parentId); + logger.debug("All directories created"); // Upload files into directories + logger.debug("Start file uploading") for (const parentId of Object.keys(filesPerParentId)) { + logger.debug(`Upload files for directory ${parentId}`); await FileUploadService.upload(filesPerParentId[parentId], { context: { companyId: context.companyId, parentId: parentId, }, callback: (file, context) => { - console.log('created file: ', file); + logger.debug('created file: ', file); if (file) { create( { diff --git a/tdrive/frontend/src/app/features/files/services/file-upload-service.ts b/tdrive/frontend/src/app/features/files/services/file-upload-service.ts index 3b04c99a..ce6d1d1f 100644 --- a/tdrive/frontend/src/app/features/files/services/file-upload-service.ts +++ b/tdrive/frontend/src/app/features/files/services/file-upload-service.ts @@ -10,6 +10,8 @@ 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', @@ -20,6 +22,7 @@ class FileUploadService { private pendingFiles: PendingFileType[] = []; public currentTaskId = ''; private recoilHandler: Function = () => undefined; + private logger: Logger.Logger = Logger.getLogger('FileUploadService'); setRecoilHandler(handler: Function) { this.recoilHandler = handler; @@ -37,6 +40,69 @@ class FileUploadService { 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?: { @@ -47,7 +113,7 @@ class FileUploadService { const { companyId } = RouterServices.getStateFromRoute(); if (!fileList || !companyId) { - logger.log('FileList or companyId is undefined', [fileList, companyId]); + this.logger.log('FileList or companyId is undefined', [fileList, companyId]); return []; } @@ -69,6 +135,9 @@ class FileUploadService { originalFile: file, backendFile: null, resumable: null, + type: "file", + label: null, + pausable: true }; this.pendingFiles.push(pendingFile); @@ -108,7 +177,7 @@ class FileUploadService { pendingFile.resumable.on('fileProgress', (f: any) => { const bytesDelta = - (f.progress() - pendingFile.progress) * (pendingFile?.originalFile.size || 0); + (f.progress() - pendingFile.progress) * (pendingFile?.originalFile?.size || 0); const timeDelta = new Date().getTime() - pendingFile.lastProgress; // To avoid jumping time ? @@ -167,14 +236,17 @@ class FileUploadService { const fileToCancel = this.pendingFiles.filter(f => f.id === id)[0]; fileToCancel.status = 'cancel'; - fileToCancel.resumable.cancel(); - this.notify(); - if (fileToCancel.backendFile) - this.deleteOneFile({ - companyId: fileToCancel.backendFile.company_id, - fileId: fileToCancel.backendFile.id, - }); + 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); diff --git a/tdrive/frontend/src/app/features/files/types/file.ts b/tdrive/frontend/src/app/features/files/types/file.ts index bada26ad..db4ddca0 100644 --- a/tdrive/frontend/src/app/features/files/types/file.ts +++ b/tdrive/frontend/src/app/features/files/types/file.ts @@ -66,14 +66,20 @@ export type PendingFileRecoilType = { file: FileType | null; }; +/** + * It could be not only a file, but also a task with creating folders + */ export type PendingFileType = { + type: "file" | "folder" resumable: typeof Resumable | null; //Contain the resumable instance in charge of this file uploadTaskId: string; id: string; status: 'pending' | 'error' | 'success' | 'pause' | 'cancel'; progress: number; //Between 0 and 1 - originalFile: File; //Will be used to get filename, temporary thumbnail + originalFile: File | null; //Will be used to get filename, temporary thumbnail backendFile: FileType | null; //Will contain the final object returned by API lastProgress: number; speed: number; + label: string | null; + pausable: boolean; }; diff --git a/tdrive/frontend/src/app/features/files/utils/pending-files.ts b/tdrive/frontend/src/app/features/files/utils/pending-files.ts index 82d83279..8c372e3f 100644 --- a/tdrive/frontend/src/app/features/files/utils/pending-files.ts +++ b/tdrive/frontend/src/app/features/files/utils/pending-files.ts @@ -1,21 +1,21 @@ import { PendingFileRecoilType } from '@features/files/types/file'; export const isPendingFileStatusPause = (status: PendingFileRecoilType['status']): boolean => { - return status === 'pause' ? true : false; + return status === 'pause'; }; export const isPendingFileStatusError = (status: PendingFileRecoilType['status']): boolean => { - return status === 'error' ? true : false; + return status === 'error'; }; export const isPendingFileStatusCancel = (status: PendingFileRecoilType['status']): boolean => { - return status === 'cancel' ? true : false; + return status === 'cancel'; }; export const isPendingFileStatusPending = (status: PendingFileRecoilType['status']): boolean => { - return status === 'pending' ? true : false; + return status === 'pending'; }; export const isPendingFileStatusSuccess = (status: PendingFileRecoilType['status']): boolean => { - return status === 'success' ? true : false; + return status === 'success'; };