🌟basic status, while uploading directories (#216)

* 🌟 basic status, while uploading directories
* 🐛 fix the infinite redirection when the token always expired
This commit is contained in:
Anton Shepilov
2023-10-06 16:23:42 +02:00
committed by GitHub
parent 422bd6cbf6
commit 4da75eb68f
11 changed files with 162 additions and 75 deletions
@@ -319,8 +319,11 @@ export class DocumentsService {
driveItem.last_version_cache = driveItemVersion; driveItem.last_version_cache = driveItemVersion;
await this.repository.save(driveItem); 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); 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 this.notifyWebsocket(driveItem.parent_id, context);
await globalResolver.platformServices.messageQueue.publish<DocumentsMessageQueueRequest>( await globalResolver.platformServices.messageQueue.publish<DocumentsMessageQueueRequest>(
@@ -459,6 +462,7 @@ export class DocumentsService {
* deletes or moves to Trash a Drive Document and its children * deletes or moves to Trash a Drive Document and its children
* *
* @param {string} id - the item id * @param {string} id - the item id
* @param item
* @param {DriveExecutionContext} context - the execution context * @param {DriveExecutionContext} context - the execution context
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
@@ -569,10 +573,10 @@ export class DocumentsService {
} }
await updateItemSize(previousParentId, this.repository, context); await updateItemSize(previousParentId, this.repository, context);
this.notifyWebsocket(previousParentId, context); await this.notifyWebsocket(previousParentId, context);
} }
this.notifyWebsocket("trash", context); await this.notifyWebsocket("trash", context);
}; };
/** /**
@@ -67,7 +67,7 @@ export default ({ pendingFileState, pendingFile }: PropsType) => {
}} }}
> >
<Col className="small-left-margin" flex="auto" style={{ lineHeight: '16px' }}> <Col className="small-left-margin" flex="auto" style={{ lineHeight: '16px' }}>
{pendingFile?.originalFile.name ? ( {pendingFile?.originalFile?.name ? (
<Row justify="start" align="middle" wrap={false}> <Row justify="start" align="middle" wrap={false}>
<Text <Text
ellipsis ellipsis
@@ -96,6 +96,29 @@ export default ({ pendingFileState, pendingFile }: PropsType) => {
}} }}
/> />
)} )}
{pendingFile?.label ? (
<Row justify="start" align="middle" wrap={false}>
<Text
ellipsis
style={{
maxWidth: isPendingFileStatusPause(pendingFile.status) ? 130 : 160,
verticalAlign: 'middle',
}}
>
{pendingFile?.label}
</Text>
</Row>
) : (
<div
style={{
marginTop: 8,
height: 8,
maxWidth: 160,
borderRadius: 8,
backgroundColor: 'var(--grey-background)',
}}
/>
)}
</Col> </Col>
<Col <Col
@@ -1,5 +1,5 @@
export default { export default {
version: /* @VERSION */ '2023.Q3.011', version: /* @VERSION */ '2023.Q3.012',
version_detail: /* @VERSION_DETAIL */ '2023.Q3.011', version_detail: /* @VERSION_DETAIL */ '2023.Q3.012',
version_name: /* @VERSION_NAME */ 'Ghost-Dog', version_name: /* @VERSION_NAME */ 'Ghost-Dog',
}; };
@@ -22,7 +22,6 @@ import Globals from '@features/global/services/globals-tdrive-app-service';
import { Cookies } from 'react-cookie'; import { Cookies } from 'react-cookie';
type AccountType = 'remote' | 'internal'; type AccountType = 'remote' | 'internal';
export type LoginState = '' | 'app' | 'error' | 'signin' | 'logged_out' | 'logout';
type InitState = '' | 'initializing' | 'initialized'; type InitState = '' | 'initializing' | 'initialized';
@TdriveService('AuthService') @TdriveService('AuthService')
@@ -131,11 +130,13 @@ class AuthService {
onNewToken(token?: JWTDataType): void { onNewToken(token?: JWTDataType): void {
if (token) { if (token) {
console.log("Save auth token to storage and cookie") this.logger.info("Save auth token to storage and cookie")
JWT.updateJWT(token); JWT.updateJWT(token);
this.cookies.set(AuthService.AUTH_TOKEN_COOKIE, JWT.getJWT(), { path: "/" }); this.cookies.set(AuthService.AUTH_TOKEN_COOKIE, JWT.getJWT(), { path: "/" });
// TODO: Update the user from API? // TODO: Update the user from API?
// this.updateUser(); // this.updateUser();
} else {
this.logger.warn("Try to initialize storage with empty access token")
} }
} }
@@ -33,6 +33,7 @@ export default class InternalAuthProviderService
private logger: Logger.Logger; private logger: Logger.Logger;
private initialized = false; private initialized = false;
private signinIn = false; private signinIn = false;
private params?: InitParameters;
constructor(private configuration?: InternalConfiguration) { constructor(private configuration?: InternalConfiguration) {
super(); super();
@@ -45,6 +46,7 @@ export default class InternalAuthProviderService
this.logger.warn('Already initialized'); this.logger.warn('Already initialized');
return this; return this;
} }
this.params = params;
this.initialized = true; this.initialized = true;
params.onInitialized(); params.onInitialized();
@@ -56,26 +58,27 @@ export default class InternalAuthProviderService
return Promise.reject('"username" and "password" are required'); return Promise.reject('"username" and "password" are required');
} }
return new Promise<void>((resolve, reject) => { this.signinIn = true;
this.signinIn = true; this.logger.log("Sign in in console");
this.logger.log("Sign in in console"); try {
ConsoleAPIClient.login( const access_token = await ConsoleAPIClient.login(
{ {
email: params.username, email: params.username,
password: params.password, password: params.password,
remember_me: params.remember_me, remember_me: params.remember_me,
}, },
true, true,
) );
.then(accessToken => (accessToken ? resolve() : reject(new Error('Can not login')))) this.logger.debug("Got access token from server");
.catch(err => { await this.params?.onNewToken(access_token);
this.logger.error('Error on login', err); this.signinIn = true;
reject(new Error('Can not login')); } catch (e) {
}) this.signinIn = false;
.finally(() => { throw new Error('Can not login');
this.signinIn = false; } finally {
}); this.signinIn = false;
}); }
} }
async signOut(params: SignOutParameters): Promise<void> { async signOut(params: SignOutParameters): Promise<void> {
@@ -31,8 +31,8 @@ class ConsoleAPIClient {
logger = Logger.getLogger('ConsoleAPIClient'); logger = Logger.getLogger('ConsoleAPIClient');
login(params: LoginParams, disableJWTAuthentication = false): Promise<string> { login(params: LoginParams, disableJWTAuthentication = false): Promise<JWTDataType> {
return Api.post<LoginParams, { access_token: string }>( return Api.post<LoginParams, AccessTokenResponse>(
'/internal/services/console/v1/login', '/internal/services/console/v1/login',
{ ...params, ...{ device: {} } }, { ...params, ...{ device: {} } },
undefined, undefined,
@@ -90,7 +90,7 @@ class ConsoleAPIClient {
response.access_token response.access_token
? resolve(response.access_token) ? resolve(response.access_token)
: reject(new Error('Can not get access token')); : reject(new Error('Can not get access token'));
}); }, false, {disableJWTAuthentication: true});
}); });
} }
} }
@@ -10,7 +10,6 @@ import Languages from 'features/global/services/languages-service';
/** /**
* Returns the children of a drive item * Returns the children of a drive item
* @param id
* @returns * @returns
*/ */
export const useDriveActions = () => { export const useDriveActions = () => {
@@ -47,11 +46,12 @@ export const useDriveActions = () => {
const create = useCallback( const create = useCallback(
async (item: Partial<DriveItem>, version: Partial<DriveItemVersion>) => { async (item: Partial<DriveItem>, version: Partial<DriveItemVersion>) => {
if (!item || !version) throw new Error("All ");
let driveFile = null; let driveFile = null;
if (!item.company_id) item.company_id = companyId; if (!item.company_id) item.company_id = companyId;
try { try {
driveFile = await DriveApiClient.create(companyId, { item, version }); driveFile = await DriveApiClient.create(companyId, { item, version });
await refresh(item.parent_id!); await refresh(driveFile.parent_id!);
} catch (e) { } catch (e) {
ToasterService.error(Languages.t('hooks.use-drive-actions.unable_create_file')); ToasterService.error(Languages.t('hooks.use-drive-actions.unable_create_file'));
} }
@@ -63,7 +63,7 @@ export const useDriveActions = () => {
const download = useCallback( const download = useCallback(
async (id: string, versionId?: string) => { async (id: string, versionId?: string) => {
try { try {
const url = await DriveApiClient.getDownloadUrl(companyId, id, versionId); const url = DriveApiClient.getDownloadUrl(companyId, id, versionId);
(window as any).open(url, '_blank').focus(); (window as any).open(url, '_blank').focus();
} catch (e) { } catch (e) {
ToasterService.error(Languages.t('hooks.use-drive-actions.unable_download_file')); ToasterService.error(Languages.t('hooks.use-drive-actions.unable_download_file'));
@@ -3,14 +3,16 @@ import FileUploadService from '@features/files/services/file-upload-service';
import { ToasterService } from '@features/global/services/toaster-service'; import { ToasterService } from '@features/global/services/toaster-service';
import { DriveApiClient } from '../api-client/api-client'; import { DriveApiClient } from '../api-client/api-client';
import { useDriveActions } from './use-drive-actions'; import { useDriveActions } from './use-drive-actions';
import Logger from '@features/global/framework/logger-service';
/** /**
* Returns the children of a drive item * Returns the children of a drive item
* @param id
* @returns * @returns
*/ */
export const useDriveUpload = () => { 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 }) => { const uploadVersion = async (file: File, context: { companyId: string; id: string }) => {
return new Promise(r => { return new Promise(r => {
@@ -45,47 +47,23 @@ export const useDriveUpload = () => {
tree: FileTreeObject, tree: FileTreeObject,
context: { companyId: string; parentId: string }, context: { companyId: string; parentId: string },
) => { ) => {
const filesPerParentId: { [key: string]: File[] } = {};
// Create all directories // Create all directories
console.debug("Start creating directories ..."); logger.debug("Start creating directories ...");
const createDirectories = async (tree: FileTreeObject['tree'], parentId: string) => { const filesPerParentId = await FileUploadService.createDirectories(tree.tree, context);
for (const directory of Object.keys(tree)) { await refresh(context.parentId);
if (tree[directory] instanceof File) { logger.debug("All directories created");
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");
// Upload files into directories // Upload files into directories
logger.debug("Start file uploading")
for (const parentId of Object.keys(filesPerParentId)) { for (const parentId of Object.keys(filesPerParentId)) {
logger.debug(`Upload files for directory ${parentId}`);
await FileUploadService.upload(filesPerParentId[parentId], { await FileUploadService.upload(filesPerParentId[parentId], {
context: { context: {
companyId: context.companyId, companyId: context.companyId,
parentId: parentId, parentId: parentId,
}, },
callback: (file, context) => { callback: (file, context) => {
console.log('created file: ', file); logger.debug('created file: ', file);
if (file) { if (file) {
create( create(
{ {
@@ -10,6 +10,8 @@ import RouterServices from '@features/router/services/router-service';
import _ from 'lodash'; import _ from 'lodash';
import FileUploadAPIClient from '../api/file-upload-api-client'; import FileUploadAPIClient from '../api/file-upload-api-client';
import { isPendingFileStatusPending } from '../utils/pending-files'; 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 { export enum Events {
ON_CHANGE = 'notify', ON_CHANGE = 'notify',
@@ -20,6 +22,7 @@ class FileUploadService {
private pendingFiles: PendingFileType[] = []; private pendingFiles: PendingFileType[] = [];
public currentTaskId = ''; public currentTaskId = '';
private recoilHandler: Function = () => undefined; private recoilHandler: Function = () => undefined;
private logger: Logger.Logger = Logger.getLogger('FileUploadService');
setRecoilHandler(handler: Function) { setRecoilHandler(handler: Function) {
this.recoilHandler = handler; this.recoilHandler = handler;
@@ -37,6 +40,69 @@ class FileUploadService {
this.recoilHandler(_.cloneDeep(updatedState)); 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( public async upload(
fileList: File[], fileList: File[],
options?: { options?: {
@@ -47,7 +113,7 @@ class FileUploadService {
const { companyId } = RouterServices.getStateFromRoute(); const { companyId } = RouterServices.getStateFromRoute();
if (!fileList || !companyId) { if (!fileList || !companyId) {
logger.log('FileList or companyId is undefined', [fileList, companyId]); this.logger.log('FileList or companyId is undefined', [fileList, companyId]);
return []; return [];
} }
@@ -69,6 +135,9 @@ class FileUploadService {
originalFile: file, originalFile: file,
backendFile: null, backendFile: null,
resumable: null, resumable: null,
type: "file",
label: null,
pausable: true
}; };
this.pendingFiles.push(pendingFile); this.pendingFiles.push(pendingFile);
@@ -108,7 +177,7 @@ class FileUploadService {
pendingFile.resumable.on('fileProgress', (f: any) => { pendingFile.resumable.on('fileProgress', (f: any) => {
const bytesDelta = 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; const timeDelta = new Date().getTime() - pendingFile.lastProgress;
// To avoid jumping time ? // To avoid jumping time ?
@@ -167,14 +236,17 @@ class FileUploadService {
const fileToCancel = this.pendingFiles.filter(f => f.id === id)[0]; const fileToCancel = this.pendingFiles.filter(f => f.id === id)[0];
fileToCancel.status = 'cancel'; fileToCancel.status = 'cancel';
fileToCancel.resumable.cancel();
this.notify();
if (fileToCancel.backendFile) if (fileToCancel.resumable) {
this.deleteOneFile({ fileToCancel.resumable.cancel();
companyId: fileToCancel.backendFile.company_id, this.notify();
fileId: fileToCancel.backendFile.id,
}); if (fileToCancel.backendFile)
this.deleteOneFile({
companyId: fileToCancel.backendFile.company_id,
fileId: fileToCancel.backendFile.id,
});
}
setTimeout(() => { setTimeout(() => {
this.pendingFiles = this.pendingFiles.filter(f => f.id !== id); this.pendingFiles = this.pendingFiles.filter(f => f.id !== id);
@@ -66,14 +66,20 @@ export type PendingFileRecoilType = {
file: FileType | null; file: FileType | null;
}; };
/**
* It could be not only a file, but also a task with creating folders
*/
export type PendingFileType = { export type PendingFileType = {
type: "file" | "folder"
resumable: typeof Resumable | null; //Contain the resumable instance in charge of this file resumable: typeof Resumable | null; //Contain the resumable instance in charge of this file
uploadTaskId: string; uploadTaskId: string;
id: string; id: string;
status: 'pending' | 'error' | 'success' | 'pause' | 'cancel'; status: 'pending' | 'error' | 'success' | 'pause' | 'cancel';
progress: number; //Between 0 and 1 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 backendFile: FileType | null; //Will contain the final object returned by API
lastProgress: number; lastProgress: number;
speed: number; speed: number;
label: string | null;
pausable: boolean;
}; };
@@ -1,21 +1,21 @@
import { PendingFileRecoilType } from '@features/files/types/file'; import { PendingFileRecoilType } from '@features/files/types/file';
export const isPendingFileStatusPause = (status: PendingFileRecoilType['status']): boolean => { export const isPendingFileStatusPause = (status: PendingFileRecoilType['status']): boolean => {
return status === 'pause' ? true : false; return status === 'pause';
}; };
export const isPendingFileStatusError = (status: PendingFileRecoilType['status']): boolean => { export const isPendingFileStatusError = (status: PendingFileRecoilType['status']): boolean => {
return status === 'error' ? true : false; return status === 'error';
}; };
export const isPendingFileStatusCancel = (status: PendingFileRecoilType['status']): boolean => { export const isPendingFileStatusCancel = (status: PendingFileRecoilType['status']): boolean => {
return status === 'cancel' ? true : false; return status === 'cancel';
}; };
export const isPendingFileStatusPending = (status: PendingFileRecoilType['status']): boolean => { export const isPendingFileStatusPending = (status: PendingFileRecoilType['status']): boolean => {
return status === 'pending' ? true : false; return status === 'pending';
}; };
export const isPendingFileStatusSuccess = (status: PendingFileRecoilType['status']): boolean => { export const isPendingFileStatusSuccess = (status: PendingFileRecoilType['status']): boolean => {
return status === 'success' ? true : false; return status === 'success';
}; };