🌟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;
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<DocumentsMessageQueueRequest>(
@@ -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<void>}
*/
@@ -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);
};
/**
@@ -67,7 +67,7 @@ export default ({ pendingFileState, pendingFile }: PropsType) => {
}}
>
<Col className="small-left-margin" flex="auto" style={{ lineHeight: '16px' }}>
{pendingFile?.originalFile.name ? (
{pendingFile?.originalFile?.name ? (
<Row justify="start" align="middle" wrap={false}>
<Text
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
@@ -1,5 +1,5 @@
export default {
version: /* @VERSION */ '2023.Q3.011',
version_detail: /* @VERSION_DETAIL */ '2023.Q3.011',
version: /* @VERSION */ '2023.Q3.012',
version_detail: /* @VERSION_DETAIL */ '2023.Q3.012',
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';
type AccountType = 'remote' | 'internal';
export type LoginState = '' | 'app' | 'error' | 'signin' | 'logged_out' | 'logout';
type InitState = '' | 'initializing' | 'initialized';
@TdriveService('AuthService')
@@ -131,11 +130,13 @@ class AuthService {
onNewToken(token?: JWTDataType): void {
if (token) {
console.log("Save auth token to storage and cookie")
this.logger.info("Save auth token to storage and cookie")
JWT.updateJWT(token);
this.cookies.set(AuthService.AUTH_TOKEN_COOKIE, JWT.getJWT(), { path: "/" });
// TODO: Update the user from API?
// 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 initialized = false;
private signinIn = false;
private params?: InitParameters;
constructor(private configuration?: InternalConfiguration) {
super();
@@ -45,6 +46,7 @@ export default class InternalAuthProviderService
this.logger.warn('Already initialized');
return this;
}
this.params = params;
this.initialized = true;
params.onInitialized();
@@ -56,26 +58,27 @@ export default class InternalAuthProviderService
return Promise.reject('"username" and "password" are required');
}
return new Promise<void>((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<void> {
@@ -31,8 +31,8 @@ class ConsoleAPIClient {
logger = Logger.getLogger('ConsoleAPIClient');
login(params: LoginParams, disableJWTAuthentication = false): Promise<string> {
return Api.post<LoginParams, { access_token: string }>(
login(params: LoginParams, disableJWTAuthentication = false): Promise<JWTDataType> {
return Api.post<LoginParams, AccessTokenResponse>(
'/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});
});
}
}
@@ -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<DriveItem>, version: Partial<DriveItemVersion>) => {
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'));
@@ -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(
{
@@ -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);
@@ -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;
};
@@ -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';
};