🌟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:
@@ -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);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
+24
-1
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+17
-14
@@ -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';
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user