Skip to content

Commit

Permalink
feat(cloud-storage): cancel upload (#480)
Browse files Browse the repository at this point in the history
* feat(cloud-storage): cancel upload

* fix(cloud-storage): updateTotalUsage, cancel without fake id

* fix(cloud-storage): prevent upload too often

* refactor(cloud-storage): hide totalUsage = 0
  • Loading branch information
hyrious committed Apr 1, 2021
1 parent 3daa411 commit 58698f0
Show file tree
Hide file tree
Showing 3 changed files with 200 additions and 93 deletions.
10 changes: 10 additions & 0 deletions desktop/renderer-app/src/apiMiddleware/flatServer/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,13 @@ export interface ConvertFinishResult {}
export async function convertFinish(payload: ConvertFinishPayload): Promise<ConvertFinishResult> {
return await post("cloud-storage/convert/finish", payload);
}

export interface CancelUploadPayload {
fileUUIDs: string[];
}

export interface CancelUploadResult {}

export async function cancelUpload(payload: CancelUploadPayload): Promise<void> {
await post("cloud-storage/upload/cancel", payload);
}
217 changes: 132 additions & 85 deletions desktop/renderer-app/src/pages/CloudStoragePage/store.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
CloudStorageFile,
CloudStorageFileName,
CloudStorageStore as CloudStorageStoreBase,
CloudStorageUploadStatus,
} from "flat-components";
import { action, makeObservable } from "mobx";
import React, { ReactNode } from "react";
Expand All @@ -19,6 +20,7 @@ import {
convertStepToType,
download,
getFileUrl,
isFakeID,
pickFiles,
queryTask,
uploadManager,
Expand All @@ -27,12 +29,7 @@ import {
type FileMenusKey = "download" | "rename" | "delete";

export class CloudStorageStore extends CloudStorageStoreBase {
pulling = {
queue: [] as CloudFile[],
index: 0,
timer: NaN,
};

pulling = new Map<string, number>();
retryable = new Map<string, File>();

fileMenus = (
Expand All @@ -53,19 +50,35 @@ export class CloudStorageStore extends CloudStorageStoreBase {
this.setCompact(compact);
makeObservable(this, {
updateFiles: action,
updateTotalUsage: action,
updateFileName: action,
expandUploadPanel: action,
clearUploadStatusesMap: action,
deleteFileFromUploadStatusesMap: action,
onUploadInit: action,
onUploadEnd: action,
onUploadError: action,
onUploadProgress: action,
unselectAll: action,
});
}

unselectAll(): void {
this.selectedFileUUIDs = [];
}

expandUploadPanel(): void {
this.isUploadPanelExpand = true;
}

clearUploadStatusesMap(): void {
this.uploadStatusesMap.clear();
}

deleteFileFromUploadStatusesMap(fileUUID: string): void {
this.uploadStatusesMap.delete(fileUUID);
}

onUpload = async (): Promise<void> => {
this.expandUploadPanel();
const files = await pickFiles({
Expand All @@ -78,7 +91,7 @@ export class CloudStorageStore extends CloudStorageStoreBase {
}
};

onItemMenuClick = async (fileUUID: string, menuKey: React.Key): Promise<void> => {
onItemMenuClick = (fileUUID: string, menuKey: React.Key): void => {
const key = menuKey as FileMenusKey;
console.log("[cloud-storage] onItemMenuClick", fileUUID, key);
switch (key) {
Expand All @@ -92,9 +105,15 @@ export class CloudStorageStore extends CloudStorageStoreBase {
break;
}
case "delete": {
this.updateFiles({ files: this.files.filter(file => file.fileUUID !== fileUUID) });
await removeFiles({ fileUUIDs: [fileUUID] });
await this.refreshFiles();
Modal.confirm({
content: "确定删除所选课件?课件删除后不可恢复",
onOk: async () => {
this.updateFiles(this.files.filter(file => file.fileUUID !== fileUUID));
await removeFiles({ fileUUIDs: [fileUUID] });
await this.refreshFiles();
this.unselectAll();
},
});
break;
}
default:
Expand Down Expand Up @@ -139,22 +158,30 @@ export class CloudStorageStore extends CloudStorageStoreBase {
}
};

onBatchDelete = async (): Promise<void> => {
const fileUUIDs = this.selectedFileUUIDs;
console.log("[cloud-storage] onBatchDelete", fileUUIDs);
this.updateFiles({ files: this.files.filter(file => !fileUUIDs.includes(file.fileUUID)) });
await removeFiles({ fileUUIDs });
await this.refreshFiles();
onBatchDelete = (): void => {
Modal.confirm({
content: "确定删除所选课件?课件删除后不可恢复",
onOk: async () => {
const fileUUIDs = this.selectedFileUUIDs;
console.log("[cloud-storage] onBatchDelete", fileUUIDs);
this.updateFiles(this.files.filter(file => !fileUUIDs.includes(file.fileUUID)));
await removeFiles({ fileUUIDs });
await this.refreshFiles();
this.unselectAll();
},
});
};

isUploadNotFinished = ({ status }: CloudStorageUploadStatus): boolean => {
return status !== "success" && status !== "error";
};

onUploadPanelClose = (): void => {
for (const { status } of this.uploadStatusesMap.values()) {
if (status !== "success") {
message.warning("there are tasks not finished");
return;
}
if (Array.from(this.uploadStatusesMap.values()).some(this.isUploadNotFinished)) {
message.warning("there are tasks not finished");
} else {
this.clearUploadStatusesMap();
}
this.uploadStatusesMap.clear();
};

onUploadRetry = (fileUUID: string): void => {
Expand All @@ -168,26 +195,34 @@ export class CloudStorageStore extends CloudStorageStoreBase {
}
};

onUploadCancel = (fileUUID: string): void => {
onUploadCancel = async (fileUUID: string): Promise<void> => {
console.log("[cloud-storage] onUploadCancel", fileUUID);
console.log(`[cloud-storage] TODO: remove zombie task ${fileUUID}`);
uploadManager.clean([fileUUID]);
this.uploadStatusesMap.delete(fileUUID);
this.deleteFileFromUploadStatusesMap(fileUUID);
await this.refreshFiles();
};

setupUpload(file: File): void {
const task = uploadManager.upload(file);
const { fileUUID: fakeID } = task;
this.onUploadInit(fakeID, file);
task.onInit = fileUUID => {
console.log("[cloud-storage] start uploading", fileUUID, file.name, file.size);
this.deleteFileFromUploadStatusesMap(fakeID);
this.onUploadInit(fileUUID, file);
this.refreshFiles();
};
task.onError = error => {
console.error(error);
message.error(error.message);
if (task.fileUUID) {
if (!isFakeID(task.fileUUID)) {
this.onUploadError(task.fileUUID, file);
this.refreshFiles();
}
};
task.onCancel = () => {
this.refreshFiles();
};
task.onEnd = () => {
console.log("[cloud-storage] finish uploading", task.fileUUID);
this.onUploadEnd(task.fileUUID);
Expand Down Expand Up @@ -231,94 +266,106 @@ export class CloudStorageStore extends CloudStorageStoreBase {
});
}

updateFiles({ files, totalUsage }: { files: CloudStorageFile[]; totalUsage?: number }): void {
updateFiles(files: CloudStorageFile[]): void {
this.files = files;
if (totalUsage) {
this.totalUsage = totalUsage;
}
}

updateTotalUsage(totalUsage: number): void {
this.totalUsage = totalUsage;
}

async initialize(): Promise<void> {
await this.refreshFiles();
}

destroy(): void {
const { timer } = this.pulling;
if (!Number.isNaN(timer)) {
this.pulling.timer = NaN;
this.clearPullingTasks();
}

clearPullingTasks(): void {
for (const timer of this.pulling.values()) {
window.clearTimeout(timer);
}
this.pulling.clear();
}

async refreshFiles(): Promise<void> {
this.clearPullingTasks();
const { files, totalUsage } = await listFiles({ page: 1 });
const tempFiles = files.map(file => ({
...file,
convert: convertStepToType(file.convertStep),
}));
const { timer } = this.pulling;
if (!Number.isNaN(timer)) {
this.pulling.timer = NaN;
window.clearTimeout(timer);
}
const queue: CloudFile[] = [];
for (const file of tempFiles) {
if (file.fileName.endsWith(".pptx") || file.fileName.endsWith(".pdf")) {
switch (file.convertStep) {
case FileConvertStep.None: {
let shouldPull = false;
if (file.convertStep === FileConvertStep.None) {
try {
console.log("[cloud-storage] convert start", file.fileUUID, file.fileName);
const task = await convertStart({ fileUUID: file.fileUUID });
const { taskToken, taskUUID } = await convertStart({
fileUUID: file.fileUUID,
});
file.convertStep = FileConvertStep.Converting;
file.convert = "converting";
queue.push({ ...file, ...task });
break;
}
case FileConvertStep.Converting: {
queue.push(file);
break;
file.taskToken = taskToken;
file.taskUUID = taskUUID;
shouldPull = true;
} catch {
// ignore convert start failed
}
default:
} else if (file.convertStep === FileConvertStep.Converting) {
shouldPull = true;
}
if (shouldPull) {
this.setupPullingFile(file);
}
}
}
this.updateFiles({
files: tempFiles,
totalUsage,
});
this.pulling = {
queue,
index: 0,
timer: window.setTimeout(this.pullConvertSteps, 1000),
};
this.updateFiles(tempFiles);
this.updateTotalUsage(totalUsage === 0 ? NaN : totalUsage);
}

pullConvertSteps = async (): Promise<void> => {
const { queue, index } = this.pulling;
if (queue.length === 0) return;
const { fileName, fileUUID, taskUUID, taskToken } = queue[index];
// TODO: if queryTask failed because of network, retry?
const { status, progress } = await queryTask(
taskUUID,
taskToken,
fileName.endsWith(".pptx"),
);
console.log("[cloud-storage] convert", fileUUID, status, progress?.convertedPercentage);
switch (status) {
case "Fail":
case "Finished": {
try {
await convertFinish({ fileUUID });
} catch {
message.error(`${fileName} convert failed`);
} finally {
await this.refreshFiles();
}
break;
setupPullingFile({ fileName, fileUUID, taskUUID, taskToken }: CloudFile): void {
let task: () => Promise<void>;
const next = (): void => {
const timer = this.pulling.get(fileUUID);
if (timer) {
window.clearTimeout(timer);
}
default: {
this.pulling.index = (index + 1) % queue.length;
this.pulling.timer = window.setTimeout(this.pullConvertSteps, 1000);
this.pulling.set(fileUUID, window.setTimeout(task, 1000));
};
task = async (): Promise<void> => {
const { status, progress } = await queryTask(
taskUUID,
taskToken,
fileName.endsWith(".pptx"),
);
console.log(
"[cloud-storage] convert",
fileUUID,
fileName,
status,
progress?.convertedPercentage,
);
switch (status) {
case "Fail":
case "Finished": {
try {
await convertFinish({ fileUUID });
} catch (e) {
if (status === "Fail") {
message.error(`${fileName} convert failed`);
}
} finally {
await this.refreshFiles();
}
break;
}
default: {
next();
}
}
}
};
};
next();
}
}

0 comments on commit 58698f0

Please sign in to comment.