feat(client/worker): support web work in frontend with fallback (#31)
* feat(client/web): add upload mgr * feat(uploader): move uploader to worker * feat(upload_mgr): use native worker for uploading * fix(upload_mgr): fix uploading stop not working * chore(client/web): cleanups * test(upload.worker): add unit test for upload.worker * feat(worker): add foreground upload worker * chore(uploader): turn down the speedup
This commit is contained in:
parent
67c07cc81f
commit
31e4850344
27 changed files with 1192 additions and 265 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -7,6 +7,7 @@
|
||||||
**/yarn-error
|
**/yarn-error
|
||||||
**/public/static/**/*.js
|
**/public/static/**/*.js
|
||||||
**/public/index.html
|
**/public/index.html
|
||||||
|
**/**/*.d.ts.map
|
||||||
|
|
||||||
# backend
|
# backend
|
||||||
**/*/quickshare.db
|
**/*/quickshare.db
|
||||||
|
|
12
package.json
12
package.json
|
@ -5,12 +5,12 @@
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev:setup": "yarn && yarn dev:copy-immutable && yarn dev:copy-react-dom && yarn dev:copy-react",
|
"dev:setup": "yarn && yarn dev:copy-immutable && yarn dev:copy-react-dom && yarn dev:copy-react",
|
||||||
"dev:copy-immutable": "cp node_modules/immutable/dist/immutable.min.js public/static/js",
|
"dev:copy-immutable": "cp node_modules/immutable/dist/immutable.min.js public/static/js/",
|
||||||
"dev:copy-react-dom": "cp node_modules/react-dom/umd/react-dom.development.js public/static/js",
|
"dev:copy-react-dom": "cp node_modules/react-dom/umd/react-dom.development.js public/static/js/",
|
||||||
"dev:copy-react": "cp node_modules/react/umd/react.development.js public/static/js",
|
"dev:copy-react": "cp node_modules/react/umd/react.development.js public/static/js/",
|
||||||
"prod:setup": "yarn && yarn prod:copy-immutable && yarn prod:copy-react-dom && yarn prod:copy-react",
|
"prod:setup": "yarn && yarn prod:copy-immutable && yarn prod:copy-react-dom && yarn prod:copy-react",
|
||||||
"prod:copy-immutable": "cp node_modules/immutable/dist/immutable.min.js public/static/js",
|
"prod:copy-immutable": "cp node_modules/immutable/dist/immutable.min.js public/static/js/",
|
||||||
"prod:copy-react-dom": "cp node_modules/react-dom/umd/react-dom.production.min.js public/static/js",
|
"prod:copy-react-dom": "cp node_modules/react-dom/umd/react-dom.production.min.js public/static/js/",
|
||||||
"prod:copy-react": "cp node_modules/react/umd/react.production.min.js public/static/js"
|
"prod:copy-react": "cp node_modules/react/umd/react.production.min.js public/static/js/"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,7 @@
|
||||||
"@types/deep-diff": "^1.0.0",
|
"@types/deep-diff": "^1.0.0",
|
||||||
"@types/jest": "^24.0.12",
|
"@types/jest": "^24.0.12",
|
||||||
"assert": "^2.0.0",
|
"assert": "^2.0.0",
|
||||||
|
"babel-loader": "^8.2.2",
|
||||||
"css-loader": "^2.1.1",
|
"css-loader": "^2.1.1",
|
||||||
"deep-diff": "^1.0.2",
|
"deep-diff": "^1.0.2",
|
||||||
"html-webpack-plugin": "^4.0.0-beta.5",
|
"html-webpack-plugin": "^4.0.0-beta.5",
|
||||||
|
@ -38,14 +39,16 @@
|
||||||
"terser-webpack-plugin": "^1.3.0",
|
"terser-webpack-plugin": "^1.3.0",
|
||||||
"ts-jest": "^26.4.4",
|
"ts-jest": "^26.4.4",
|
||||||
"ts-loader": "^6.0.0",
|
"ts-loader": "^6.0.0",
|
||||||
|
"ts-mockito": "^2.6.1",
|
||||||
"ts-node": "^8.2.0",
|
"ts-node": "^8.2.0",
|
||||||
"tslint": "^5.16.0",
|
"tslint": "^5.16.0",
|
||||||
"typescript": "^3.4.3",
|
"typescript": "^4.1.3",
|
||||||
"uglifyjs-webpack-plugin": "^2.1.3",
|
"uglifyjs-webpack-plugin": "^2.1.3",
|
||||||
"webpack": "^5.0.0-rc.6",
|
"webpack": "^5.0.0-rc.6",
|
||||||
"webpack-bundle-analyzer": "^3.3.2",
|
"webpack-bundle-analyzer": "^3.3.2",
|
||||||
"webpack-cli": "^4.2.0",
|
"webpack-cli": "^4.2.0",
|
||||||
"webpack-merge": "^4.2.1"
|
"webpack-merge": "^4.2.1",
|
||||||
|
"worker-loader": "^3.0.7"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/axios": "^0.14.0",
|
"@types/axios": "^0.14.0",
|
||||||
|
@ -62,7 +65,8 @@
|
||||||
"react-copy-to-clipboard": "^5.0.1",
|
"react-copy-to-clipboard": "^5.0.1",
|
||||||
"react-dom": "^16.8.6",
|
"react-dom": "^16.8.6",
|
||||||
"react-svg": "^8.0.6",
|
"react-svg": "^8.0.6",
|
||||||
"throttle-debounce": "^2.1.0"
|
"throttle-debounce": "^2.1.0",
|
||||||
|
"worker-loader": "^3.0.7"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"testMatch": [
|
"testMatch": [
|
||||||
|
|
|
@ -1,14 +1,36 @@
|
||||||
import {
|
import {
|
||||||
BaseClient,
|
BaseClient,
|
||||||
|
FatalErrResp,
|
||||||
Response,
|
Response,
|
||||||
UploadStatusResp,
|
UploadStatusResp,
|
||||||
ListResp,
|
ListResp,
|
||||||
|
ListUploadingsResp,
|
||||||
} from "./";
|
} from "./";
|
||||||
|
|
||||||
const filePathQuery = "fp";
|
const filePathQuery = "fp";
|
||||||
const listDirQuery = "dp";
|
const listDirQuery = "dp";
|
||||||
// TODO: get timeout from server
|
// TODO: get timeout from server
|
||||||
|
|
||||||
|
function translateResp(resp: Response<any>): Response<any> {
|
||||||
|
if (resp.status === 500) {
|
||||||
|
if (
|
||||||
|
(resp.data == null || resp.data === "") ||
|
||||||
|
(
|
||||||
|
resp.data.error != null &&
|
||||||
|
!resp.data.error.includes("fail to lock the file") &&
|
||||||
|
!resp.data.error.includes("offset != uploaded") &&
|
||||||
|
!resp.data.error.includes("i/o timeout")
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return FatalErrResp(resp.statusText);
|
||||||
|
}
|
||||||
|
} else if (resp.status === 404) {
|
||||||
|
return FatalErrResp(resp.statusText);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
export class FilesClient extends BaseClient {
|
export class FilesClient extends BaseClient {
|
||||||
constructor(url: string) {
|
constructor(url: string) {
|
||||||
super(url);
|
super(url);
|
||||||
|
@ -79,6 +101,12 @@ export class FilesClient extends BaseClient {
|
||||||
content,
|
content,
|
||||||
offset,
|
offset,
|
||||||
},
|
},
|
||||||
|
})
|
||||||
|
.then((resp) => {
|
||||||
|
return translateResp(resp);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
return FatalErrResp(`unknow uploadStatus error ${e.toString()}`);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -89,6 +117,12 @@ export class FilesClient extends BaseClient {
|
||||||
params: {
|
params: {
|
||||||
[filePathQuery]: filePath,
|
[filePathQuery]: filePath,
|
||||||
},
|
},
|
||||||
|
})
|
||||||
|
.then((resp) => {
|
||||||
|
return translateResp(resp);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
return FatalErrResp(`unknow uploadStatus error ${e.toString()}`);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -101,4 +135,21 @@ export class FilesClient extends BaseClient {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
listUploadings = (): Promise<Response<ListUploadingsResp>> => {
|
||||||
|
return this.do({
|
||||||
|
method: "get",
|
||||||
|
url: `${this.url}/v1/fs/uploadings`,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
deleteUploading = (filePath: string): Promise<Response> => {
|
||||||
|
return this.do({
|
||||||
|
method: "delete",
|
||||||
|
url: `${this.url}/v1/fs/uploadings`,
|
||||||
|
params: {
|
||||||
|
[filePathQuery]: filePath,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import {
|
||||||
Response,
|
Response,
|
||||||
UploadStatusResp,
|
UploadStatusResp,
|
||||||
ListResp,
|
ListResp,
|
||||||
|
ListUploadingsResp,
|
||||||
} from "./";
|
} from "./";
|
||||||
|
|
||||||
export class FilesClient {
|
export class FilesClient {
|
||||||
|
@ -18,6 +19,8 @@ export class FilesClient {
|
||||||
private uploadStatusMockResps: Array<Promise<Response<UploadStatusResp>>>;
|
private uploadStatusMockResps: Array<Promise<Response<UploadStatusResp>>>;
|
||||||
private uploadStatusMockRespID: number = 0;
|
private uploadStatusMockRespID: number = 0;
|
||||||
private listMockResp: Promise<Response<ListResp>>;
|
private listMockResp: Promise<Response<ListResp>>;
|
||||||
|
private listUploadingsMockResp: Promise<Response<ListUploadingsResp>>;
|
||||||
|
private deleteUploadingMockResp: Promise<Response>;
|
||||||
|
|
||||||
constructor(url: string) {
|
constructor(url: string) {
|
||||||
this.url = url;
|
this.url = url;
|
||||||
|
@ -55,6 +58,14 @@ export class FilesClient {
|
||||||
this.listMockResp = resp;
|
this.listMockResp = resp;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
listUploadingsMock = (resp: Promise<Response<ListUploadingsResp>>) => {
|
||||||
|
this.listUploadingsMockResp = resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteUploadingMock = (resp: Promise<Response>) => {
|
||||||
|
this.deleteUploadingMockResp = resp;
|
||||||
|
}
|
||||||
|
|
||||||
create = (filePath: string, fileSize: number): Promise<Response> => {
|
create = (filePath: string, fileSize: number): Promise<Response> => {
|
||||||
if (this.createMockRespID < this.createMockResps.length) {
|
if (this.createMockRespID < this.createMockResps.length) {
|
||||||
return this.createMockResps[this.createMockRespID++];
|
return this.createMockResps[this.createMockRespID++];
|
||||||
|
@ -99,4 +110,12 @@ export class FilesClient {
|
||||||
list = (dirPath: string): Promise<Response<ListResp>> => {
|
list = (dirPath: string): Promise<Response<ListResp>> => {
|
||||||
return this.listMockResp;
|
return this.listMockResp;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
listUploadings = (): Promise<Response<ListUploadingsResp>> => {
|
||||||
|
return this.listUploadingsMockResp;
|
||||||
|
};
|
||||||
|
|
||||||
|
deleteUploading = (filePath:string): Promise<Response<Response>> => {
|
||||||
|
return this.deleteUploadingMockResp;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,11 +20,21 @@ export interface ListResp {
|
||||||
metadatas: MetadataResp[];
|
metadatas: MetadataResp[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UploadInfo {
|
||||||
|
realFilePath: string;
|
||||||
|
size: number;
|
||||||
|
uploaded: number; // TODO: use string instead
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ListUploadingsResp {
|
||||||
|
uploadInfos: UploadInfo[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface IUsersClient {
|
export interface IUsersClient {
|
||||||
login: (user: string, pwd: string) => Promise<Response>
|
login: (user: string, pwd: string) => Promise<Response>;
|
||||||
logout: () => Promise<Response>
|
logout: () => Promise<Response>;
|
||||||
isAuthed: () => Promise<Response>
|
isAuthed: () => Promise<Response>;
|
||||||
setPwd: (oldPwd: string, newPwd: string) => Promise<Response>
|
setPwd: (oldPwd: string, newPwd: string) => Promise<Response>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IFilesClient {
|
export interface IFilesClient {
|
||||||
|
@ -40,6 +50,8 @@ export interface IFilesClient {
|
||||||
) => Promise<Response<UploadStatusResp>>;
|
) => Promise<Response<UploadStatusResp>>;
|
||||||
uploadStatus: (filePath: string) => Promise<Response<UploadStatusResp>>;
|
uploadStatus: (filePath: string) => Promise<Response<UploadStatusResp>>;
|
||||||
list: (dirPath: string) => Promise<Response<ListResp>>;
|
list: (dirPath: string) => Promise<Response<ListResp>>;
|
||||||
|
listUploadings: () => Promise<Response<ListUploadingsResp>>;
|
||||||
|
deleteUploading: (filePath: string) => Promise<Response>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Response<T = any> {
|
export interface Response<T = any> {
|
||||||
|
@ -61,7 +73,7 @@ export const EmptyBodyResp: Response<any> = {
|
||||||
statusText: "Empty Response Body",
|
statusText: "Empty Response Body",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const UnknownErrResp = (errMsg: string): Response<any> => {
|
export const FatalErrResp = (errMsg: string): Response<any> => {
|
||||||
return {
|
return {
|
||||||
status: 600,
|
status: 600,
|
||||||
data: {},
|
data: {},
|
||||||
|
@ -69,6 +81,10 @@ export const UnknownErrResp = (errMsg: string): Response<any> => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isFatalErr = (resp: Response<any>): boolean => {
|
||||||
|
return resp.status === 600;
|
||||||
|
};
|
||||||
|
|
||||||
export class BaseClient {
|
export class BaseClient {
|
||||||
protected url: string;
|
protected url: string;
|
||||||
|
|
||||||
|
@ -95,11 +111,15 @@ export class BaseClient {
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
const errMsg = e.toString();
|
const errMsg = e.toString();
|
||||||
|
|
||||||
if (errMsg.includes("ERR_EMPTY")) {
|
if (errMsg.includes("ERR_EMPTY")) {
|
||||||
// this means connection is eliminated by server because of timeout.
|
// this means connection is eliminated by server, it may be caused by timeout.
|
||||||
resolve(EmptyBodyResp);
|
resolve(EmptyBodyResp);
|
||||||
|
} else if (e.response != null) {
|
||||||
|
resolve(e.response);
|
||||||
} else {
|
} else {
|
||||||
resolve(UnknownErrResp(errMsg));
|
// TODO: check e.request to get more friendly error message
|
||||||
|
resolve(FatalErrResp(errMsg));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,151 +0,0 @@
|
||||||
import { IFilesClient } from "../client";
|
|
||||||
import { FilesClient } from "../client/files";
|
|
||||||
import { Response, UnknownErrResp, UploadStatusResp } from "./";
|
|
||||||
|
|
||||||
// TODO: get settings from server
|
|
||||||
const defaultChunkLen = 1024 * 1024 * 30; // 15MB/s
|
|
||||||
const speedDownRatio = 0.5;
|
|
||||||
const speedUpRatio = 1.1;
|
|
||||||
const retryLimit = 4;
|
|
||||||
|
|
||||||
export class FileUploader {
|
|
||||||
private reader = new FileReader();
|
|
||||||
private client: IFilesClient = new FilesClient("");
|
|
||||||
private chunkLen: number = defaultChunkLen;
|
|
||||||
private file: File;
|
|
||||||
private offset: number;
|
|
||||||
private filePath: string;
|
|
||||||
private errMsg: string | null;
|
|
||||||
private progressCb: (filePath: string, progress: number) => void;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
file: File,
|
|
||||||
filePath: string,
|
|
||||||
progressCb?: (filePath: string, progress: number) => void
|
|
||||||
) {
|
|
||||||
this.file = file;
|
|
||||||
this.filePath = filePath;
|
|
||||||
this.progressCb = progressCb;
|
|
||||||
this.offset = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
err = (): string | null => {
|
|
||||||
return this.errMsg;
|
|
||||||
};
|
|
||||||
|
|
||||||
setClient = (client: IFilesClient) => {
|
|
||||||
this.client = client;
|
|
||||||
};
|
|
||||||
|
|
||||||
create = async (filePath: string, fileSize: number): Promise<Response> => {
|
|
||||||
return this.client.create(filePath, fileSize);
|
|
||||||
};
|
|
||||||
|
|
||||||
uploadChunk = async (
|
|
||||||
filePath: string,
|
|
||||||
base64Chunk: string,
|
|
||||||
offset: number
|
|
||||||
): Promise<Response<UploadStatusResp>> => {
|
|
||||||
return this.client.uploadChunk(filePath, base64Chunk, offset);
|
|
||||||
};
|
|
||||||
|
|
||||||
uploadStatus = async (
|
|
||||||
filePath: string
|
|
||||||
): Promise<Response<UploadStatusResp>> => {
|
|
||||||
return this.client.uploadStatus(filePath);
|
|
||||||
};
|
|
||||||
|
|
||||||
start = async (): Promise<boolean> => {
|
|
||||||
let resp: Response;
|
|
||||||
for (let i = 0; i < retryLimit; i++) {
|
|
||||||
resp = await this.create(this.filePath, this.file.size);
|
|
||||||
if (resp.status === 200) {
|
|
||||||
return await this.upload();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.errMsg = `failed to create ${this.filePath}: status=${resp.statusText}`;
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
upload = async (): Promise<boolean> => {
|
|
||||||
while (
|
|
||||||
this.chunkLen > 0 &&
|
|
||||||
this.offset >= 0 &&
|
|
||||||
this.offset < this.file.size
|
|
||||||
) {
|
|
||||||
const uploadPromise = new Promise<Response<UploadStatusResp>>(
|
|
||||||
(resolve: (resp: Response<UploadStatusResp>) => void) => {
|
|
||||||
this.reader.onerror = (ev: ProgressEvent<FileReader>) => {
|
|
||||||
resolve(UnknownErrResp(this.reader.error.toString()));
|
|
||||||
};
|
|
||||||
|
|
||||||
this.reader.onloadend = (ev: ProgressEvent<FileReader>) => {
|
|
||||||
const dataURL = ev.target.result as string; // readAsDataURL
|
|
||||||
const base64Chunk = dataURL.slice(dataURL.indexOf(",") + 1);
|
|
||||||
this.uploadChunk(this.filePath, base64Chunk, this.offset)
|
|
||||||
.then((resp: Response<UploadStatusResp>) => {
|
|
||||||
resolve(resp);
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
resolve(UnknownErrResp(e.toString()));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const chunkRightPos =
|
|
||||||
this.offset + this.chunkLen > this.file.size
|
|
||||||
? this.file.size
|
|
||||||
: this.offset + this.chunkLen;
|
|
||||||
const blob = this.file.slice(this.offset, chunkRightPos);
|
|
||||||
this.reader.readAsDataURL(blob);
|
|
||||||
|
|
||||||
const uploadResp = await uploadPromise;
|
|
||||||
|
|
||||||
if (uploadResp.status === 200 && uploadResp.data != null) {
|
|
||||||
this.offset = uploadResp.data.uploaded;
|
|
||||||
this.chunkLen = Math.ceil(this.chunkLen * speedUpRatio);
|
|
||||||
} else {
|
|
||||||
this.errMsg = uploadResp.statusText;
|
|
||||||
this.chunkLen = Math.ceil(this.chunkLen * speedDownRatio);
|
|
||||||
|
|
||||||
let uploadStatusResp: Response<UploadStatusResp> = undefined;
|
|
||||||
try {
|
|
||||||
uploadStatusResp = await this.uploadStatus(this.filePath);
|
|
||||||
} catch (e) {
|
|
||||||
if (uploadStatusResp == null) {
|
|
||||||
this.errMsg = `${this.errMsg}; unknown error: empty uploadStatus response`;
|
|
||||||
break;
|
|
||||||
} else if (uploadStatusResp.status === 500) {
|
|
||||||
if (
|
|
||||||
!uploadStatusResp.statusText.includes("fail to lock the file") &&
|
|
||||||
uploadStatusResp.statusText !== ""
|
|
||||||
) {
|
|
||||||
this.errMsg = `${this.errMsg}; unknown error: ${uploadStatusResp.statusText}`;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else if (uploadStatusResp.status === 600) {
|
|
||||||
this.errMsg = `${this.errMsg}; unknown error: ${uploadStatusResp.statusText}`;
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
// ignore error and retry
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (uploadStatusResp.status === 200) {
|
|
||||||
this.offset = uploadStatusResp.data.uploaded;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.progressCb != null) {
|
|
||||||
this.progressCb(this.filePath, Math.ceil(this.offset / this.file.size));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.chunkLen === 0) {
|
|
||||||
this.errMsg = "network is bad, please retry later.";
|
|
||||||
}
|
|
||||||
return this.offset === this.file.size;
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,9 +1,15 @@
|
||||||
import { init } from "../core_state";
|
import { mock, instance } from "ts-mockito";
|
||||||
|
|
||||||
|
import { initWithWorker } from "../core_state";
|
||||||
import { Updater } from "../auth_pane";
|
import { Updater } from "../auth_pane";
|
||||||
import { MockUsersClient } from "../../client/users_mock";
|
import { MockUsersClient } from "../../client/users_mock";
|
||||||
import { Response } from "../../client";
|
import { Response } from "../../client";
|
||||||
|
import { MockWorker } from "../../worker/interface";
|
||||||
|
|
||||||
describe("AuthPane", () => {
|
describe("AuthPane", () => {
|
||||||
|
const mockWorkerClass = mock(MockWorker);
|
||||||
|
const mockWorker = instance(mockWorkerClass);
|
||||||
|
|
||||||
const makePromise = (ret: any): Promise<any> => {
|
const makePromise = (ret: any): Promise<any> => {
|
||||||
return new Promise<any>((resolve) => {
|
return new Promise<any>((resolve) => {
|
||||||
resolve(ret);
|
resolve(ret);
|
||||||
|
@ -44,7 +50,7 @@ describe("AuthPane", () => {
|
||||||
client.isAuthedMock(makeNumberResponse(tc.isAuthedStatus));
|
client.isAuthedMock(makeNumberResponse(tc.isAuthedStatus));
|
||||||
client.setPwdMock(makeNumberResponse(tc.setPwdStatus));
|
client.setPwdMock(makeNumberResponse(tc.setPwdStatus));
|
||||||
|
|
||||||
const coreState = init();
|
const coreState = initWithWorker(mockWorker);
|
||||||
Updater.setClient(client);
|
Updater.setClient(client);
|
||||||
Updater.init(coreState.panel.authPane);
|
Updater.init(coreState.panel.authPane);
|
||||||
await Updater.initIsAuthed();
|
await Updater.initIsAuthed();
|
||||||
|
|
|
@ -1,13 +1,18 @@
|
||||||
import { List, Map } from "immutable";
|
import { List, Map } from "immutable";
|
||||||
|
import { mock, instance } from "ts-mockito";
|
||||||
|
|
||||||
import { init } from "../core_state";
|
import { initWithWorker } from "../core_state";
|
||||||
import { makePromise, makeNumberResponse } from "../../test/helpers";
|
import { makePromise, makeNumberResponse } from "../../test/helpers";
|
||||||
import { Updater } from "../browser";
|
import { Updater } from "../browser";
|
||||||
import { MockUsersClient } from "../../client/users_mock";
|
import { MockUsersClient } from "../../client/users_mock";
|
||||||
import { FilesClient } from "../../client/files_mock";
|
import { FilesClient } from "../../client/files_mock";
|
||||||
import { MetadataResp } from "../../client";
|
import { MetadataResp } from "../../client";
|
||||||
|
import { MockWorker } from "../../worker/interface";
|
||||||
|
|
||||||
describe("Browser", () => {
|
describe("Browser", () => {
|
||||||
|
const mockWorkerClass = mock(MockWorker);
|
||||||
|
const mockWorker = instance(mockWorkerClass);
|
||||||
|
|
||||||
test("Updater: setPwd", async () => {
|
test("Updater: setPwd", async () => {
|
||||||
const tests = [
|
const tests = [
|
||||||
{
|
{
|
||||||
|
@ -43,7 +48,7 @@ describe("Browser", () => {
|
||||||
filesClient.listMock(makePromise(tc.listResp));
|
filesClient.listMock(makePromise(tc.listResp));
|
||||||
Updater.setClients(usersClient, filesClient);
|
Updater.setClients(usersClient, filesClient);
|
||||||
|
|
||||||
const coreState = init();
|
const coreState = initWithWorker(mockWorker);
|
||||||
Updater.init(coreState.panel.browser);
|
Updater.init(coreState.panel.browser);
|
||||||
await Updater.setItems(List<string>(tc.filePath.split("/")));
|
await Updater.setItems(List<string>(tc.filePath.split("/")));
|
||||||
const newState = Updater.setBrowser(coreState);
|
const newState = Updater.setBrowser(coreState);
|
||||||
|
@ -105,7 +110,7 @@ describe("Browser", () => {
|
||||||
filesClient.deleteMock(makeNumberResponse(200));
|
filesClient.deleteMock(makeNumberResponse(200));
|
||||||
Updater.setClients(usersClient, filesClient);
|
Updater.setClients(usersClient, filesClient);
|
||||||
|
|
||||||
const coreState = init();
|
const coreState = initWithWorker(mockWorker);
|
||||||
Updater.init(coreState.panel.browser);
|
Updater.init(coreState.panel.browser);
|
||||||
await Updater.delete(
|
await Updater.delete(
|
||||||
List<string>(tc.dirPath.split("/")),
|
List<string>(tc.dirPath.split("/")),
|
||||||
|
@ -166,7 +171,7 @@ describe("Browser", () => {
|
||||||
filesClient.moveMock(makeNumberResponse(200));
|
filesClient.moveMock(makeNumberResponse(200));
|
||||||
Updater.setClients(usersClient, filesClient);
|
Updater.setClients(usersClient, filesClient);
|
||||||
|
|
||||||
const coreState = init();
|
const coreState = initWithWorker(mockWorker);
|
||||||
Updater.init(coreState.panel.browser);
|
Updater.init(coreState.panel.browser);
|
||||||
await Updater.moveHere(
|
await Updater.moveHere(
|
||||||
tc.dirPath1,
|
tc.dirPath1,
|
||||||
|
|
|
@ -1,13 +1,20 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as ReactDOM from "react-dom";
|
import * as ReactDOM from "react-dom";
|
||||||
import { List, Map } from "immutable";
|
import { List, Map } from "immutable";
|
||||||
import * as Filesize from "filesize";
|
import FileSize from "filesize";
|
||||||
|
|
||||||
import { ICoreState } from "./core_state";
|
import { ICoreState } from "./core_state";
|
||||||
import { IUsersClient, IFilesClient, MetadataResp } from "../client";
|
import {
|
||||||
|
IUsersClient,
|
||||||
|
IFilesClient,
|
||||||
|
MetadataResp,
|
||||||
|
UploadInfo,
|
||||||
|
} from "../client";
|
||||||
import { FilesClient } from "../client/files";
|
import { FilesClient } from "../client/files";
|
||||||
import { UsersClient } from "../client/users";
|
import { UsersClient } from "../client/users";
|
||||||
import { FileUploader } from "../client/uploader";
|
import { UploadMgr } from "../worker/upload_mgr";
|
||||||
|
import { UploadEntry } from "../worker/interface";
|
||||||
|
// import { FileUploader } from "../worker/uploader";
|
||||||
|
|
||||||
export const uploadCheckCycle = 1000;
|
export const uploadCheckCycle = 1000;
|
||||||
|
|
||||||
|
@ -22,6 +29,7 @@ export interface Item {
|
||||||
export interface Props {
|
export interface Props {
|
||||||
dirPath: List<string>;
|
dirPath: List<string>;
|
||||||
items: List<MetadataResp>;
|
items: List<MetadataResp>;
|
||||||
|
uploadings: List<UploadInfo>;
|
||||||
|
|
||||||
uploadFiles: List<File>;
|
uploadFiles: List<File>;
|
||||||
uploadValue: string;
|
uploadValue: string;
|
||||||
|
@ -46,9 +54,23 @@ export class Updater {
|
||||||
Updater.filesClient = filesClient;
|
Updater.filesClient = filesClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static setUploadings = (infos: Map<string, UploadEntry>) => {
|
||||||
|
Updater.props.uploadings = List<UploadInfo>(
|
||||||
|
infos.valueSeq().map(
|
||||||
|
(v: UploadEntry): UploadInfo => {
|
||||||
|
return {
|
||||||
|
realFilePath: v.filePath,
|
||||||
|
size: v.size,
|
||||||
|
uploaded: v.uploaded,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
static setItems = async (dirParts: List<string>): Promise<void> => {
|
static setItems = async (dirParts: List<string>): Promise<void> => {
|
||||||
let dirPath = dirParts.join("/");
|
const dirPath = dirParts.join("/");
|
||||||
let listResp = await Updater.filesClient.list(dirPath);
|
const listResp = await Updater.filesClient.list(dirPath);
|
||||||
|
|
||||||
Updater.props.dirPath = dirParts;
|
Updater.props.dirPath = dirParts;
|
||||||
Updater.props.items =
|
Updater.props.items =
|
||||||
|
@ -57,6 +79,26 @@ export class Updater {
|
||||||
: Updater.props.items;
|
: Updater.props.items;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static refreshUploadings = async (): Promise<boolean> => {
|
||||||
|
const luResp = await Updater.filesClient.listUploadings();
|
||||||
|
|
||||||
|
Updater.props.uploadings =
|
||||||
|
luResp.status === 200
|
||||||
|
? List<UploadInfo>(luResp.data.uploadInfos)
|
||||||
|
: Updater.props.uploadings;
|
||||||
|
return luResp.status === 200;
|
||||||
|
};
|
||||||
|
|
||||||
|
static deleteUploading = async (filePath: string): Promise<boolean> => {
|
||||||
|
UploadMgr.delete(filePath);
|
||||||
|
const resp = await Updater.filesClient.deleteUploading(filePath);
|
||||||
|
return resp.status === 200;
|
||||||
|
};
|
||||||
|
|
||||||
|
static stopUploading = (filePath: string) => {
|
||||||
|
UploadMgr.stop(filePath);
|
||||||
|
};
|
||||||
|
|
||||||
static mkDir = async (dirPath: string): Promise<void> => {
|
static mkDir = async (dirPath: string): Promise<void> => {
|
||||||
let resp = await Updater.filesClient.mkdir(dirPath);
|
let resp = await Updater.filesClient.mkdir(dirPath);
|
||||||
if (resp.status !== 200) {
|
if (resp.status !== 200) {
|
||||||
|
@ -120,16 +162,11 @@ export class Updater {
|
||||||
};
|
};
|
||||||
|
|
||||||
static addUploadFiles = (fileList: FileList, len: number) => {
|
static addUploadFiles = (fileList: FileList, len: number) => {
|
||||||
let newUploads = List<File>([]);
|
|
||||||
for (let i = 0; i < len; i++) {
|
for (let i = 0; i < len; i++) {
|
||||||
newUploads = newUploads.push(fileList.item(i));
|
// do not wait for the promise
|
||||||
|
UploadMgr.add(fileList[i], fileList[i].name);
|
||||||
}
|
}
|
||||||
|
Updater.setUploadings(UploadMgr.list());
|
||||||
Updater.props.uploadFiles = Updater.props.uploadFiles.concat(newUploads);
|
|
||||||
};
|
|
||||||
|
|
||||||
static setUploadFiles = (uploadFiles: List<File>) => {
|
|
||||||
Updater.props.uploadFiles = uploadFiles;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static setBrowser = (prevState: ICoreState): ICoreState => {
|
static setBrowser = (prevState: ICoreState): ICoreState => {
|
||||||
|
@ -154,7 +191,6 @@ export class Browser extends React.Component<Props, State, {}> {
|
||||||
private uploadInput: Element | Text;
|
private uploadInput: Element | Text;
|
||||||
private assignInput: (input: Element) => void;
|
private assignInput: (input: Element) => void;
|
||||||
private onClickUpload: () => void;
|
private onClickUpload: () => void;
|
||||||
private uploading: boolean;
|
|
||||||
|
|
||||||
constructor(p: Props) {
|
constructor(p: Props) {
|
||||||
super(p);
|
super(p);
|
||||||
|
@ -176,46 +212,21 @@ export class Browser extends React.Component<Props, State, {}> {
|
||||||
this.uploadInput = ReactDOM.findDOMNode(input);
|
this.uploadInput = ReactDOM.findDOMNode(input);
|
||||||
};
|
};
|
||||||
this.onClickUpload = () => {
|
this.onClickUpload = () => {
|
||||||
|
// TODO: check if the re-upload file is same as previous upload
|
||||||
const uploadInput = this.uploadInput as HTMLButtonElement;
|
const uploadInput = this.uploadInput as HTMLButtonElement;
|
||||||
uploadInput.click();
|
uploadInput.click();
|
||||||
};
|
};
|
||||||
|
|
||||||
Updater.setItems(p.dirPath).then(() => {
|
UploadMgr.setStatusCb(this.updateProgress);
|
||||||
|
Updater.setItems(p.dirPath)
|
||||||
|
.then(() => {
|
||||||
|
return Updater.refreshUploadings();
|
||||||
|
})
|
||||||
|
.then((_: boolean) => {
|
||||||
this.update(Updater.setBrowser);
|
this.update(Updater.setBrowser);
|
||||||
});
|
});
|
||||||
|
|
||||||
setInterval(this.pollUploads, uploadCheckCycle);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pollUploads = () => {
|
|
||||||
if (this.props.uploadFiles.size > 0 && !this.uploading) {
|
|
||||||
this.uploading = true;
|
|
||||||
const file = this.props.uploadFiles.get(0);
|
|
||||||
Updater.setUploadFiles(this.props.uploadFiles.slice(1));
|
|
||||||
this.update(Updater.setBrowser);
|
|
||||||
|
|
||||||
const uploader = new FileUploader(
|
|
||||||
file,
|
|
||||||
`${this.props.dirPath.join("/")}/${file.name}`,
|
|
||||||
this.updateProgress
|
|
||||||
);
|
|
||||||
|
|
||||||
uploader.start().then((ok: boolean) => {
|
|
||||||
Updater.setItems(this.props.dirPath).then(() => {
|
|
||||||
this.update(Updater.setBrowser);
|
|
||||||
});
|
|
||||||
if (!ok) {
|
|
||||||
alert(`upload failed: ${uploader.err()}`);
|
|
||||||
}
|
|
||||||
this.uploading = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
updateProgress = (filePath: string, progress: number) => {
|
|
||||||
// update uploading progress in the core state
|
|
||||||
};
|
|
||||||
|
|
||||||
showPane = () => {
|
showPane = () => {
|
||||||
this.setState({ show: !this.state.show });
|
this.setState({ show: !this.state.show });
|
||||||
};
|
};
|
||||||
|
@ -231,12 +242,6 @@ export class Browser extends React.Component<Props, State, {}> {
|
||||||
onInputChange = (ev: React.ChangeEvent<HTMLInputElement>) => {
|
onInputChange = (ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
this.setState({ inputValue: ev.target.value });
|
this.setState({ inputValue: ev.target.value });
|
||||||
};
|
};
|
||||||
|
|
||||||
addUploadFile = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
Updater.addUploadFiles(event.target.files, event.target.files.length);
|
|
||||||
this.update(Updater.setBrowser);
|
|
||||||
};
|
|
||||||
|
|
||||||
select = (itemName: string) => {
|
select = (itemName: string) => {
|
||||||
const selectedItems = this.state.selectedItems.has(itemName)
|
const selectedItems = this.state.selectedItems.has(itemName)
|
||||||
? this.state.selectedItems.delete(itemName)
|
? this.state.selectedItems.delete(itemName)
|
||||||
|
@ -248,6 +253,18 @@ export class Browser extends React.Component<Props, State, {}> {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
addUploadFile = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
Updater.addUploadFiles(event.target.files, event.target.files.length);
|
||||||
|
this.update(Updater.setBrowser);
|
||||||
|
};
|
||||||
|
|
||||||
|
updateProgress = (infos: Map<string, UploadEntry>) => {
|
||||||
|
Updater.setUploadings(infos);
|
||||||
|
Updater.setItems(this.props.dirPath).then(() => {
|
||||||
|
this.update(Updater.setBrowser);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
onMkDir = () => {
|
onMkDir = () => {
|
||||||
Updater.mkDir(this.state.inputValue)
|
Updater.mkDir(this.state.inputValue)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
@ -259,6 +276,24 @@ export class Browser extends React.Component<Props, State, {}> {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
deleteUploading = (filePath: string) => {
|
||||||
|
Updater.deleteUploading(filePath)
|
||||||
|
.then((ok: boolean) => {
|
||||||
|
if (!ok) {
|
||||||
|
alert(`Failed to delete uploading ${filePath}`);
|
||||||
|
}
|
||||||
|
return Updater.refreshUploadings();
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.update(Updater.setBrowser);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
stopUploading = (filePath: string) => {
|
||||||
|
Updater.stopUploading(filePath);
|
||||||
|
this.update(Updater.setBrowser);
|
||||||
|
};
|
||||||
|
|
||||||
delete = () => {
|
delete = () => {
|
||||||
if (this.props.dirPath.join("/") !== this.state.selectedSrc) {
|
if (this.props.dirPath.join("/") !== this.state.selectedSrc) {
|
||||||
alert("please select file or folder to delete at first");
|
alert("please select file or folder to delete at first");
|
||||||
|
@ -460,7 +495,10 @@ export class Browser extends React.Component<Props, State, {}> {
|
||||||
: `${dirPath}/${item.name}`;
|
: `${dirPath}/${item.name}`;
|
||||||
|
|
||||||
return item.isDir ? (
|
return item.isDir ? (
|
||||||
<tr key={item.name} className={`${isSelected ? "white0-bg selected" : ""}`}>
|
<tr
|
||||||
|
key={item.name}
|
||||||
|
className={`${isSelected ? "white0-bg selected" : ""}`}
|
||||||
|
>
|
||||||
<td className="padding-l-l" style={{ width: "3rem" }}>
|
<td className="padding-l-l" style={{ width: "3rem" }}>
|
||||||
<span className="dot yellow0-bg"></span>
|
<span className="dot yellow0-bg"></span>
|
||||||
</td>
|
</td>
|
||||||
|
@ -485,7 +523,10 @@ export class Browser extends React.Component<Props, State, {}> {
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
) : (
|
) : (
|
||||||
<tr key={item.name} className={`${isSelected ? "white0-bg selected" : ""}`}>
|
<tr
|
||||||
|
key={item.name}
|
||||||
|
className={`${isSelected ? "white0-bg selected" : ""}`}
|
||||||
|
>
|
||||||
<td className="padding-l-l" style={{ width: "3rem" }}>
|
<td className="padding-l-l" style={{ width: "3rem" }}>
|
||||||
<span className="dot green0-bg"></span>
|
<span className="dot green0-bg"></span>
|
||||||
</td>
|
</td>
|
||||||
|
@ -498,7 +539,7 @@ export class Browser extends React.Component<Props, State, {}> {
|
||||||
{item.name}
|
{item.name}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>{Filesize(item.size, {round: 0})}</td>
|
<td>{FileSize(item.size, { round: 0 })}</td>
|
||||||
<td>{item.modTime.slice(0, item.modTime.indexOf("T"))}</td>
|
<td>{item.modTime.slice(0, item.modTime.indexOf("T"))}</td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
|
@ -514,6 +555,38 @@ export class Browser extends React.Component<Props, State, {}> {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const uploadingList = this.props.uploadings.map((uploading: UploadInfo) => {
|
||||||
|
const pathParts = uploading.realFilePath.split("/");
|
||||||
|
const fileName = pathParts[pathParts.length - 1];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr key={fileName}>
|
||||||
|
<td className="padding-l-l" style={{ width: "3rem" }}>
|
||||||
|
<span className="dot blue0-bg"></span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span className="item-name pointer">{fileName}</span>
|
||||||
|
</td>
|
||||||
|
<td>{FileSize(uploading.uploaded, { round: 0 })}</td>
|
||||||
|
<td>{FileSize(uploading.size, { round: 0 })}</td>
|
||||||
|
<td>
|
||||||
|
<button
|
||||||
|
onClick={() => this.stopUploading(uploading.realFilePath)}
|
||||||
|
className="white-font margin-m"
|
||||||
|
>
|
||||||
|
Stop
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => this.deleteUploading(uploading.realFilePath)}
|
||||||
|
className="white-font margin-m"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div id="op-bar" className="op-bar">
|
<div id="op-bar" className="op-bar">
|
||||||
|
@ -522,6 +595,31 @@ export class Browser extends React.Component<Props, State, {}> {
|
||||||
|
|
||||||
<div id="item-list" className="">
|
<div id="item-list" className="">
|
||||||
<div className="margin-b-l">{breadcrumb}</div>
|
<div className="margin-b-l">{breadcrumb}</div>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead style={{ fontWeight: "bold" }}>
|
||||||
|
<tr>
|
||||||
|
<td className="padding-l-l" style={{ width: "3rem" }}>
|
||||||
|
<span className="dot black-bg"></span>
|
||||||
|
</td>
|
||||||
|
<td>Name</td>
|
||||||
|
<td>Uploaded</td>
|
||||||
|
<td>Size</td>
|
||||||
|
<td>Action</td>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>{uploadingList}</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
<thead style={{ fontWeight: "bold" }}>
|
<thead style={{ fontWeight: "bold" }}>
|
||||||
<tr>
|
<tr>
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
import { List } from "immutable";
|
import { List } from "immutable";
|
||||||
|
|
||||||
|
import BgWorker from "../worker/upload.bg.worker";
|
||||||
|
import { FgWorker } from "../worker/upload.fgworker";
|
||||||
|
|
||||||
import { Props as PanelProps } from "./panel";
|
import { Props as PanelProps } from "./panel";
|
||||||
import { Item } from "./browser";
|
import { Item } from "./browser";
|
||||||
|
import { UploadInfo } from "../client";
|
||||||
|
import { UploadMgr, IWorker } from "../worker/upload_mgr";
|
||||||
|
|
||||||
export interface IContext {
|
export interface IContext {
|
||||||
update: (targetStatePatch: any) => void;
|
update: (targetStatePatch: any) => void;
|
||||||
|
@ -13,7 +17,23 @@ export interface ICoreState {
|
||||||
panel: PanelProps;
|
panel: PanelProps;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function initWithWorker(worker: IWorker): ICoreState {
|
||||||
|
UploadMgr.init(worker);
|
||||||
|
return initState();
|
||||||
|
}
|
||||||
|
|
||||||
export function init(): ICoreState {
|
export function init(): ICoreState {
|
||||||
|
const scripts = Array.from(document.querySelectorAll("script"));
|
||||||
|
if (!Worker) {
|
||||||
|
alert("web worker is not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
const worker = new BgWorker();
|
||||||
|
UploadMgr.init(worker);
|
||||||
|
return initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initState(): ICoreState {
|
||||||
return {
|
return {
|
||||||
ctx: null,
|
ctx: null,
|
||||||
panel: {
|
panel: {
|
||||||
|
@ -24,6 +44,7 @@ export function init(): ICoreState {
|
||||||
browser: {
|
browser: {
|
||||||
dirPath: List<string>(["."]),
|
dirPath: List<string>(["."]),
|
||||||
items: List<Item>([]),
|
items: List<Item>([]),
|
||||||
|
uploadings: List<UploadInfo>([]),
|
||||||
uploadValue: "",
|
uploadValue: "",
|
||||||
uploadFiles: List<File>([]),
|
uploadFiles: List<File>([]),
|
||||||
},
|
},
|
||||||
|
|
|
@ -56,6 +56,7 @@ export class Panel extends React.Component<Props, State, {}> {
|
||||||
<Browser
|
<Browser
|
||||||
dirPath={this.props.browser.dirPath}
|
dirPath={this.props.browser.dirPath}
|
||||||
items={this.props.browser.items}
|
items={this.props.browser.items}
|
||||||
|
uploadings={this.props.browser.uploadings}
|
||||||
update={this.update}
|
update={this.update}
|
||||||
uploadFiles={this.props.browser.uploadFiles}
|
uploadFiles={this.props.browser.uploadFiles}
|
||||||
uploadValue={this.props.browser.uploadValue}
|
uploadValue={this.props.browser.uploadValue}
|
||||||
|
|
7
src/client/web/src/typings/custom.d.ts
vendored
Normal file
7
src/client/web/src/typings/custom.d.ts
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
declare module "worker-loader!*" {
|
||||||
|
class UploadWorker extends Worker {
|
||||||
|
constructor();
|
||||||
|
}
|
||||||
|
|
||||||
|
export = UploadWorker;
|
||||||
|
}
|
97
src/client/web/src/worker/__test__/upload.worker.test.ts
Normal file
97
src/client/web/src/worker/__test__/upload.worker.test.ts
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
import { mock, instance, when } from "ts-mockito";
|
||||||
|
|
||||||
|
import { UploadWorker } from "../upload.baseworker";
|
||||||
|
import { FileUploader } from "../uploader";
|
||||||
|
import { FileWorkerResp, UploadEntry, syncReqKind } from "../interface";
|
||||||
|
|
||||||
|
describe("upload.worker", () => {
|
||||||
|
const content = ["123456"];
|
||||||
|
const filePath = "mock/file";
|
||||||
|
const blob = new Blob(content);
|
||||||
|
const fileSize = blob.size;
|
||||||
|
const file = new File(content, filePath);
|
||||||
|
|
||||||
|
const makeEntry = (filePath: string, runnable: boolean): UploadEntry => {
|
||||||
|
return {
|
||||||
|
file: file,
|
||||||
|
filePath,
|
||||||
|
size: fileSize,
|
||||||
|
uploaded: 0,
|
||||||
|
runnable,
|
||||||
|
err: "",
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
test("onMsg:syncReqKind: filter list and start uploading correct file", async () => {
|
||||||
|
const mockUploaderClass = mock(FileUploader);
|
||||||
|
when(mockUploaderClass.start()).thenCall(
|
||||||
|
(): Promise<boolean> => {
|
||||||
|
return new Promise((resolve) => resolve(true));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
when(mockUploaderClass.stop()).thenCall(() => {});
|
||||||
|
|
||||||
|
let currentUploader: FileUploader = undefined;
|
||||||
|
let uploaderFile: File = undefined;
|
||||||
|
let uploaderFilePath: string = undefined;
|
||||||
|
let uploaderStopFilePath: string = undefined;
|
||||||
|
|
||||||
|
interface TestCase {
|
||||||
|
infos: Array<UploadEntry>;
|
||||||
|
expectedUploadingFile: string;
|
||||||
|
expectedUploaderStartInput: string;
|
||||||
|
currentFilePath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tcs: Array<TestCase> = [
|
||||||
|
{
|
||||||
|
infos: [makeEntry("file1", true), makeEntry("file2", true)],
|
||||||
|
expectedUploadingFile: "file1",
|
||||||
|
expectedUploaderStartInput: "file1",
|
||||||
|
currentFilePath: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
infos: [makeEntry("file1", false), makeEntry("file2", true)],
|
||||||
|
expectedUploadingFile: "file2",
|
||||||
|
expectedUploaderStartInput: "file2",
|
||||||
|
currentFilePath: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
infos: [makeEntry("file1", true), makeEntry("file0", true)],
|
||||||
|
expectedUploadingFile: "file1",
|
||||||
|
expectedUploaderStartInput: "file1",
|
||||||
|
currentFilePath: "file0",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (let i = 0; i < tcs.length; i++) {
|
||||||
|
const uploadWorker = new UploadWorker();
|
||||||
|
uploadWorker.sendEvent = (_: FileWorkerResp) => {};
|
||||||
|
uploadWorker.makeUploader = (
|
||||||
|
file: File,
|
||||||
|
filePath: string
|
||||||
|
): FileUploader => {
|
||||||
|
uploaderFile = file;
|
||||||
|
uploaderFilePath = filePath;
|
||||||
|
currentUploader = instance(mockUploaderClass);
|
||||||
|
return currentUploader;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (tcs[i].currentFilePath !== "") {
|
||||||
|
uploadWorker.setFilePath(tcs[i].currentFilePath);
|
||||||
|
}
|
||||||
|
const req = {
|
||||||
|
kind: syncReqKind,
|
||||||
|
infos: tcs[i].infos,
|
||||||
|
};
|
||||||
|
|
||||||
|
uploadWorker.onMsg(
|
||||||
|
new MessageEvent("worker", {
|
||||||
|
data: req,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(uploadWorker.getFilePath()).toEqual(tcs[i].expectedUploadingFile);
|
||||||
|
expect(uploaderFilePath).toEqual(tcs[i].expectedUploaderStartInput);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
141
src/client/web/src/worker/__test__/upload_mgr.test.ts
Normal file
141
src/client/web/src/worker/__test__/upload_mgr.test.ts
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
import { Map } from "immutable";
|
||||||
|
import { mock, instance, when, anything } from "ts-mockito";
|
||||||
|
|
||||||
|
import { FilesClient } from "../../client/files_mock";
|
||||||
|
import { makePromise } from "../../test/helpers";
|
||||||
|
|
||||||
|
import { UploadMgr } from "../upload_mgr";
|
||||||
|
|
||||||
|
import {
|
||||||
|
FileWorkerReq,
|
||||||
|
UploadEntry,
|
||||||
|
uploadInfoKind,
|
||||||
|
syncReqKind,
|
||||||
|
SyncReq,
|
||||||
|
} from "../interface";
|
||||||
|
|
||||||
|
function arraytoMap(infos: Array<UploadEntry>): Map<string, UploadEntry> {
|
||||||
|
let map = Map<string, UploadEntry>();
|
||||||
|
infos.forEach((info) => {
|
||||||
|
map = map.set(info.filePath, info);
|
||||||
|
});
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
const delay = (ms: number): Promise<void> => {
|
||||||
|
return new Promise<void>((resolve) => {
|
||||||
|
setTimeout(resolve, ms);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("UploadMgr", () => {
|
||||||
|
const content = ["123456"];
|
||||||
|
const filePath = "mock/file";
|
||||||
|
const blob = new Blob(content);
|
||||||
|
const fileSize = blob.size;
|
||||||
|
const file = new File(content, filePath);
|
||||||
|
const makeInfo = (filePath: string, runnable: boolean): UploadEntry => {
|
||||||
|
return {
|
||||||
|
file: file,
|
||||||
|
filePath: filePath,
|
||||||
|
size: fileSize,
|
||||||
|
uploaded: 0,
|
||||||
|
runnable,
|
||||||
|
err: "",
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
test("test init and respHandler: pick up tasks and remove them after done", async () => {
|
||||||
|
interface TestCase {
|
||||||
|
inputInfos: Array<UploadEntry>;
|
||||||
|
expectedInfos: Array<UploadEntry>;
|
||||||
|
}
|
||||||
|
|
||||||
|
class MockWorker {
|
||||||
|
constructor() {}
|
||||||
|
onmessage = (event: MessageEvent): void => {};
|
||||||
|
postMessage = (req: FileWorkerReq): void => {
|
||||||
|
switch (req.kind) {
|
||||||
|
case syncReqKind:
|
||||||
|
const syncReq = req as SyncReq;
|
||||||
|
// find the first qualified task
|
||||||
|
const infoArray = syncReq.infos;
|
||||||
|
for (let i = 0; i < infoArray.length; i++) {
|
||||||
|
if (
|
||||||
|
infoArray[i].runnable &&
|
||||||
|
infoArray[i].uploaded < infoArray[i].size
|
||||||
|
) {
|
||||||
|
this.onmessage(
|
||||||
|
new MessageEvent("worker", {
|
||||||
|
data: {
|
||||||
|
kind: uploadInfoKind,
|
||||||
|
filePath: infoArray[i].filePath,
|
||||||
|
uploaded: infoArray[i].size,
|
||||||
|
runnable: true,
|
||||||
|
err: "",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw Error(
|
||||||
|
`unknown worker request ${req.kind} ${req.kind === syncReqKind}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const tcs: Array<TestCase> = [
|
||||||
|
{
|
||||||
|
inputInfos: [
|
||||||
|
makeInfo("path1/file1", true),
|
||||||
|
makeInfo("path2/file1", true),
|
||||||
|
],
|
||||||
|
expectedInfos: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
inputInfos: [
|
||||||
|
makeInfo("path1/file1", true),
|
||||||
|
makeInfo("path2/file1", false),
|
||||||
|
],
|
||||||
|
expectedInfos: [makeInfo("path2/file1", false)],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
inputInfos: [
|
||||||
|
makeInfo("path1/file1", false),
|
||||||
|
makeInfo("path2/file1", true),
|
||||||
|
],
|
||||||
|
expectedInfos: [
|
||||||
|
makeInfo("path1/file1", false),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const worker = new MockWorker();
|
||||||
|
UploadMgr.setCycle(100);
|
||||||
|
|
||||||
|
for (let i = 0; i < tcs.length; i++) {
|
||||||
|
const infoMap = arraytoMap(tcs[i].inputInfos);
|
||||||
|
UploadMgr._setInfos(infoMap);
|
||||||
|
|
||||||
|
UploadMgr.init(worker);
|
||||||
|
// polling needs several rounds to finish all the tasks
|
||||||
|
await delay(tcs.length * UploadMgr.getCycle() + 1000);
|
||||||
|
// TODO: find a better way to wait
|
||||||
|
const gotInfos = UploadMgr.list();
|
||||||
|
|
||||||
|
const expectedInfoMap = arraytoMap(tcs[i].expectedInfos);
|
||||||
|
gotInfos.keySeq().forEach((filePath) => {
|
||||||
|
expect(gotInfos.get(filePath)).toEqual(expectedInfoMap.get(filePath));
|
||||||
|
});
|
||||||
|
expectedInfoMap.keySeq().forEach((filePath) => {
|
||||||
|
expect(expectedInfoMap.get(filePath)).toEqual(gotInfos.get(filePath));
|
||||||
|
});
|
||||||
|
|
||||||
|
UploadMgr.destory();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,5 +1,5 @@
|
||||||
import { FileUploader } from "../uploader";
|
import { FileUploader } from "../uploader";
|
||||||
import { FilesClient } from "../files_mock";
|
import { FilesClient } from "../../client/files_mock";
|
||||||
import { makePromise } from "../../test/helpers";
|
import { makePromise } from "../../test/helpers";
|
||||||
|
|
||||||
describe("Uploader", () => {
|
describe("Uploader", () => {
|
||||||
|
@ -37,12 +37,14 @@ describe("Uploader", () => {
|
||||||
status: number;
|
status: number;
|
||||||
uploaded: number;
|
uploaded: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TestCase {
|
interface TestCase {
|
||||||
createResps: Array<number>;
|
createResps: Array<number>;
|
||||||
uploadChunkResps: Array<any>;
|
uploadChunkResps: Array<any>;
|
||||||
uploadStatusResps: Array<any>;
|
uploadStatusResps: Array<any>;
|
||||||
result: boolean;
|
result: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
test("test start and upload method", async () => {
|
test("test start and upload method", async () => {
|
||||||
const testCases: Array<TestCase> = [
|
const testCases: Array<TestCase> = [
|
||||||
{
|
{
|
||||||
|
@ -83,7 +85,14 @@ describe("Uploader", () => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// fail twice
|
// fail twice
|
||||||
createResps: [500, 500, 500, 200],
|
createResps: [500, 500],
|
||||||
|
uploadChunkResps: [],
|
||||||
|
uploadStatusResps: [],
|
||||||
|
result: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// fail twice
|
||||||
|
createResps: [500, 200],
|
||||||
uploadChunkResps: [
|
uploadChunkResps: [
|
||||||
{ status: 200, uploaded: 0 },
|
{ status: 200, uploaded: 0 },
|
||||||
{ status: 500, uploaded: 1 },
|
{ status: 500, uploaded: 1 },
|
||||||
|
@ -115,7 +124,13 @@ describe("Uploader", () => {
|
||||||
|
|
||||||
for (let i = 0; i < testCases.length; i++) {
|
for (let i = 0; i < testCases.length; i++) {
|
||||||
const tc = testCases[i];
|
const tc = testCases[i];
|
||||||
const uploader = new FileUploader(file, filePath);
|
const mockCb = (
|
||||||
|
filePath: string,
|
||||||
|
uploaded: number,
|
||||||
|
done: boolean,
|
||||||
|
err: string
|
||||||
|
):void => {};
|
||||||
|
const uploader = new FileUploader(file, filePath, mockCb);
|
||||||
const mockClient = new FilesClient("");
|
const mockClient = new FilesClient("");
|
||||||
|
|
||||||
const createResps = tc.createResps.map((resp) => makeCreateResp(resp));
|
const createResps = tc.createResps.map((resp) => makeCreateResp(resp));
|
49
src/client/web/src/worker/interface.ts
Normal file
49
src/client/web/src/worker/interface.ts
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
export interface UploadEntry {
|
||||||
|
file: File;
|
||||||
|
filePath: string;
|
||||||
|
size: number;
|
||||||
|
uploaded: number;
|
||||||
|
runnable: boolean;
|
||||||
|
err: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type eventKind = SyncReqKind | ErrKind | UploadInfoKind;
|
||||||
|
export interface WorkerEvent {
|
||||||
|
kind: eventKind;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SyncReqKind = "worker.req.sync";
|
||||||
|
export const syncReqKind: SyncReqKind = "worker.req.sync";
|
||||||
|
|
||||||
|
export interface SyncReq extends WorkerEvent {
|
||||||
|
kind: SyncReqKind;
|
||||||
|
infos: Array<UploadEntry>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FileWorkerReq = SyncReq;
|
||||||
|
|
||||||
|
export type ErrKind = "worker.resp.err";
|
||||||
|
export const errKind: ErrKind = "worker.resp.err";
|
||||||
|
export interface ErrResp extends WorkerEvent {
|
||||||
|
kind: ErrKind;
|
||||||
|
err: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// caller should combine uploaded and done to see if the upload is successfully finished
|
||||||
|
export type UploadInfoKind = "worker.resp.info";
|
||||||
|
export const uploadInfoKind: UploadInfoKind = "worker.resp.info";
|
||||||
|
export interface UploadInfoResp extends WorkerEvent {
|
||||||
|
kind: UploadInfoKind;
|
||||||
|
filePath: string;
|
||||||
|
uploaded: number;
|
||||||
|
runnable: boolean;
|
||||||
|
err: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FileWorkerResp = ErrResp | UploadInfoResp;
|
||||||
|
|
||||||
|
export class MockWorker {
|
||||||
|
constructor() {}
|
||||||
|
onmessage = (event: MessageEvent): void => {};
|
||||||
|
postMessage = (event: FileWorkerReq): void => {};
|
||||||
|
}
|
93
src/client/web/src/worker/upload.baseworker.ts
Normal file
93
src/client/web/src/worker/upload.baseworker.ts
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
import { FileUploader } from "./uploader";
|
||||||
|
import {
|
||||||
|
FileWorkerReq,
|
||||||
|
syncReqKind,
|
||||||
|
SyncReq,
|
||||||
|
errKind,
|
||||||
|
ErrResp,
|
||||||
|
uploadInfoKind,
|
||||||
|
UploadInfoResp,
|
||||||
|
FileWorkerResp,
|
||||||
|
} from "./interface";
|
||||||
|
|
||||||
|
export class UploadWorker {
|
||||||
|
private file: File = undefined;
|
||||||
|
private filePath: string = undefined;
|
||||||
|
private uploader: FileUploader = undefined;
|
||||||
|
sendEvent = (resp: FileWorkerResp):void => {
|
||||||
|
// TODO: make this abstract
|
||||||
|
throw new Error("not implemented");
|
||||||
|
};
|
||||||
|
makeUploader = (file: File, filePath: string): FileUploader => {
|
||||||
|
return new FileUploader(file, filePath, this.onCb);
|
||||||
|
};
|
||||||
|
startUploader = (file: File, filePath: string) => {
|
||||||
|
this.file = file;
|
||||||
|
this.filePath = filePath;
|
||||||
|
this.uploader = this.makeUploader(file, filePath);
|
||||||
|
this.uploader.start();
|
||||||
|
};
|
||||||
|
stopUploader = () => {
|
||||||
|
if (this.uploader != null) {
|
||||||
|
this.uploader.stop();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
getFilePath = (): string => {
|
||||||
|
return this.filePath;
|
||||||
|
};
|
||||||
|
|
||||||
|
setFilePath = (fp: string) => {
|
||||||
|
this.filePath = fp;
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
onMsg = (event: MessageEvent) => {
|
||||||
|
const req = event.data as FileWorkerReq;
|
||||||
|
switch (req.kind) {
|
||||||
|
case syncReqKind:
|
||||||
|
// find the first qualified task
|
||||||
|
const syncReq = req as SyncReq;
|
||||||
|
const infoArray = syncReq.infos;
|
||||||
|
for (let i = 0; i < infoArray.length; i++) {
|
||||||
|
if (
|
||||||
|
infoArray[i].runnable &&
|
||||||
|
infoArray[i].uploaded < infoArray[i].size
|
||||||
|
) {
|
||||||
|
if (infoArray[i].filePath !== this.filePath) {
|
||||||
|
this.stopUploader();
|
||||||
|
this.startUploader(infoArray[i].file, infoArray[i].filePath);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.log(`unknown worker request(${JSON.stringify(req)})`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onError = (ev: ErrorEvent) => {
|
||||||
|
const errResp: ErrResp = {
|
||||||
|
kind: errKind,
|
||||||
|
err: ev.error,
|
||||||
|
};
|
||||||
|
this.sendEvent(errResp);
|
||||||
|
};
|
||||||
|
|
||||||
|
onCb = (
|
||||||
|
filePath: string,
|
||||||
|
uploaded: number,
|
||||||
|
runnable: boolean,
|
||||||
|
err: string
|
||||||
|
): void => {
|
||||||
|
const uploadInfoResp: UploadInfoResp = {
|
||||||
|
kind: uploadInfoKind,
|
||||||
|
filePath,
|
||||||
|
uploaded,
|
||||||
|
runnable,
|
||||||
|
err,
|
||||||
|
};
|
||||||
|
this.sendEvent(uploadInfoResp);
|
||||||
|
};
|
||||||
|
}
|
20
src/client/web/src/worker/upload.bg.worker.ts
Normal file
20
src/client/web/src/worker/upload.bg.worker.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { UploadWorker } from "./upload.baseworker";
|
||||||
|
import { FileWorkerResp } from "./interface";
|
||||||
|
|
||||||
|
const ctx: Worker = self as any;
|
||||||
|
|
||||||
|
class BgWorker extends UploadWorker {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
sendEvent = (resp: FileWorkerResp): void => {
|
||||||
|
ctx.postMessage(resp);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const worker = new BgWorker();
|
||||||
|
ctx.addEventListener("message", worker.onMsg);
|
||||||
|
ctx.addEventListener("error", worker.onError);
|
||||||
|
|
||||||
|
export default null as any;
|
27
src/client/web/src/worker/upload.fgworker.ts
Normal file
27
src/client/web/src/worker/upload.fgworker.ts
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import { UploadWorker } from "./upload.baseworker";
|
||||||
|
import { FileWorkerReq, FileWorkerResp } from "./interface";
|
||||||
|
|
||||||
|
export class FgWorker extends UploadWorker {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
// provide interfaces for non-worker mode
|
||||||
|
onmessage = (event: MessageEvent): void => {};
|
||||||
|
|
||||||
|
sendEvent = (resp: FileWorkerResp) => {
|
||||||
|
this.onmessage(
|
||||||
|
new MessageEvent("worker", {
|
||||||
|
data: resp,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
postMessage = (req: FileWorkerReq): void => {
|
||||||
|
this.onMsg(
|
||||||
|
new MessageEvent("worker", {
|
||||||
|
data: req,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
145
src/client/web/src/worker/upload_mgr.ts
Normal file
145
src/client/web/src/worker/upload_mgr.ts
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
import { Map } from "immutable";
|
||||||
|
|
||||||
|
import {
|
||||||
|
FileWorkerReq,
|
||||||
|
FileWorkerResp,
|
||||||
|
UploadInfoResp,
|
||||||
|
ErrResp,
|
||||||
|
UploadEntry,
|
||||||
|
syncReqKind,
|
||||||
|
errKind,
|
||||||
|
uploadInfoKind,
|
||||||
|
} from "./interface";
|
||||||
|
|
||||||
|
const win = self as any;
|
||||||
|
|
||||||
|
export interface IWorker {
|
||||||
|
onmessage: (event: MessageEvent) => void;
|
||||||
|
postMessage: (event: FileWorkerReq) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UploadMgr {
|
||||||
|
private static infos = Map<string, UploadEntry>();
|
||||||
|
private static worker: IWorker;
|
||||||
|
private static intervalID: NodeJS.Timeout;
|
||||||
|
private static cycle: number = 500;
|
||||||
|
private static statusCb = (infos: Map<string, UploadEntry>):void => {};
|
||||||
|
|
||||||
|
static _setInfos = (infos: Map<string, UploadEntry>) => {
|
||||||
|
UploadMgr.infos = infos;
|
||||||
|
};
|
||||||
|
|
||||||
|
static setCycle = (ms: number) => {
|
||||||
|
UploadMgr.cycle = ms;
|
||||||
|
};
|
||||||
|
|
||||||
|
static getCycle = (): number => {
|
||||||
|
return UploadMgr.cycle;
|
||||||
|
};
|
||||||
|
|
||||||
|
static setStatusCb = (cb: (infos: Map<string, UploadEntry>) => void) => {
|
||||||
|
UploadMgr.statusCb = cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
static init = (worker: IWorker) => {
|
||||||
|
UploadMgr.worker = worker;
|
||||||
|
// TODO: fallback to normal if Web Worker is not available
|
||||||
|
UploadMgr.worker.onmessage = UploadMgr.respHandler;
|
||||||
|
|
||||||
|
const syncing = () => {
|
||||||
|
UploadMgr.worker.postMessage({
|
||||||
|
kind: syncReqKind,
|
||||||
|
infos: UploadMgr.infos.valueSeq().toArray(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
UploadMgr.intervalID = win.setInterval(syncing, UploadMgr.cycle);
|
||||||
|
};
|
||||||
|
|
||||||
|
static destory = () => {
|
||||||
|
win.clearInterval(UploadMgr.intervalID);
|
||||||
|
};
|
||||||
|
|
||||||
|
static add = (file: File, filePath: string) => {
|
||||||
|
const entry = UploadMgr.infos.get(filePath);
|
||||||
|
if (entry == null) {
|
||||||
|
// new uploading
|
||||||
|
UploadMgr.infos = UploadMgr.infos.set(filePath, {
|
||||||
|
file: file,
|
||||||
|
filePath: filePath,
|
||||||
|
size: file.size,
|
||||||
|
uploaded: 0,
|
||||||
|
runnable: true,
|
||||||
|
err: "",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// restart the uploading
|
||||||
|
UploadMgr.infos = UploadMgr.infos.set(filePath, {
|
||||||
|
...entry,
|
||||||
|
runnable: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
static stop = (filePath: string) => {
|
||||||
|
const entry = UploadMgr.infos.get(filePath);
|
||||||
|
if (entry != null) {
|
||||||
|
UploadMgr.infos = UploadMgr.infos.set(filePath, {
|
||||||
|
...entry,
|
||||||
|
runnable: false,
|
||||||
|
});
|
||||||
|
console.log("stopped", filePath);
|
||||||
|
} else {
|
||||||
|
alert(`failed to stop uploading ${filePath}: not found`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
static delete = (filePath: string) => {
|
||||||
|
UploadMgr.stop(filePath);
|
||||||
|
UploadMgr.infos = UploadMgr.infos.delete(filePath);
|
||||||
|
};
|
||||||
|
|
||||||
|
static list = (): Map<string, UploadEntry> => {
|
||||||
|
return UploadMgr.infos;
|
||||||
|
};
|
||||||
|
|
||||||
|
static respHandler = (event: MessageEvent) => {
|
||||||
|
const resp = event.data as FileWorkerResp;
|
||||||
|
|
||||||
|
switch (resp.kind) {
|
||||||
|
case errKind:
|
||||||
|
// TODO: refine this
|
||||||
|
const errResp = resp as ErrResp;
|
||||||
|
console.error(`respHandler: ${errResp}`);
|
||||||
|
break;
|
||||||
|
case uploadInfoKind:
|
||||||
|
const infoResp = resp as UploadInfoResp;
|
||||||
|
const entry = UploadMgr.infos.get(infoResp.filePath);
|
||||||
|
|
||||||
|
if (entry != null) {
|
||||||
|
if (infoResp.uploaded === entry.size) {
|
||||||
|
UploadMgr.infos = UploadMgr.infos.delete(infoResp.filePath);
|
||||||
|
} else {
|
||||||
|
UploadMgr.infos = UploadMgr.infos.set(infoResp.filePath, {
|
||||||
|
...entry,
|
||||||
|
uploaded: infoResp.uploaded,
|
||||||
|
runnable: infoResp.runnable,
|
||||||
|
err: infoResp.err,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// call back to update the info
|
||||||
|
UploadMgr.statusCb(UploadMgr.infos);
|
||||||
|
} else {
|
||||||
|
// TODO: refine this
|
||||||
|
console.error(
|
||||||
|
`respHandler: fail to found: file(${
|
||||||
|
infoResp.filePath
|
||||||
|
}) infos(${UploadMgr.infos.toObject()})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.error(`respHandler: response kind not found: ${resp}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
176
src/client/web/src/worker/uploader.ts
Normal file
176
src/client/web/src/worker/uploader.ts
Normal file
|
@ -0,0 +1,176 @@
|
||||||
|
import { FilesClient } from "../client/files";
|
||||||
|
import { IFilesClient, Response, isFatalErr } from "../client";
|
||||||
|
|
||||||
|
// TODO: get settings from server
|
||||||
|
// TODO: move chunk copying to worker
|
||||||
|
const defaultChunkLen = 1024 * 1024 * 1;
|
||||||
|
const speedDownRatio = 0.5;
|
||||||
|
const speedUpRatio = 1.05;
|
||||||
|
const createRetryLimit = 2;
|
||||||
|
const uploadRetryLimit = 1024;
|
||||||
|
|
||||||
|
export interface IFileUploader {
|
||||||
|
stop: () => void;
|
||||||
|
err: () => string | null;
|
||||||
|
setClient: (client: IFilesClient) => void;
|
||||||
|
start: () => Promise<boolean>;
|
||||||
|
upload: () => Promise<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReaderResult {
|
||||||
|
chunk?: string;
|
||||||
|
err?: Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FileUploader {
|
||||||
|
private reader = new FileReader();
|
||||||
|
private client: IFilesClient = new FilesClient("");
|
||||||
|
|
||||||
|
private file: File;
|
||||||
|
private filePath: string;
|
||||||
|
private chunkLen: number = defaultChunkLen;
|
||||||
|
private offset: number = 0;
|
||||||
|
|
||||||
|
private errMsgs: string[] = new Array<string>();
|
||||||
|
private isOn: boolean = true;
|
||||||
|
private progressCb: (
|
||||||
|
filePath: string,
|
||||||
|
uploaded: number,
|
||||||
|
runnable: boolean,
|
||||||
|
err: string
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
file: File,
|
||||||
|
filePath: string,
|
||||||
|
progressCb: (
|
||||||
|
filePath: string,
|
||||||
|
uploaded: number,
|
||||||
|
runnable: boolean,
|
||||||
|
err: string
|
||||||
|
) => void
|
||||||
|
) {
|
||||||
|
this.file = file;
|
||||||
|
this.filePath = filePath;
|
||||||
|
this.progressCb = progressCb;
|
||||||
|
}
|
||||||
|
|
||||||
|
getOffset = (): number => {
|
||||||
|
return this.offset;
|
||||||
|
};
|
||||||
|
|
||||||
|
stop = () => {
|
||||||
|
this.isOn = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
err = (): string | null => {
|
||||||
|
return this.errMsgs.length === 0 ? null : this.errMsgs.reverse().join(";");
|
||||||
|
};
|
||||||
|
|
||||||
|
setClient = (client: IFilesClient) => {
|
||||||
|
this.client = client;
|
||||||
|
};
|
||||||
|
|
||||||
|
start = async (): Promise<boolean> => {
|
||||||
|
let resp: Response;
|
||||||
|
|
||||||
|
for (let i = 0; i < createRetryLimit; i++) {
|
||||||
|
try {
|
||||||
|
resp = await this.client.create(this.filePath, this.file.size);
|
||||||
|
if (resp.status === 200 || resp.status === 304) {
|
||||||
|
return await this.upload();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.errMsgs.push(
|
||||||
|
`failed to create ${this.filePath}: status=${resp.statusText}`
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
upload = async (): Promise<boolean> => {
|
||||||
|
while (
|
||||||
|
this.chunkLen > 0 &&
|
||||||
|
this.offset >= 0 &&
|
||||||
|
this.offset < this.file.size &&
|
||||||
|
this.isOn
|
||||||
|
) {
|
||||||
|
const readerPromise = new Promise<ReaderResult>(
|
||||||
|
(resolve: (result: ReaderResult) => void) => {
|
||||||
|
this.reader.onerror = (_: ProgressEvent<FileReader>) => {
|
||||||
|
resolve({ err: this.reader.error });
|
||||||
|
};
|
||||||
|
|
||||||
|
this.reader.onloadend = (ev: ProgressEvent<FileReader>) => {
|
||||||
|
const dataURL = ev.target.result as string; // readAsDataURL
|
||||||
|
const base64Chunk = dataURL.slice(dataURL.indexOf(",") + 1); // remove prefix
|
||||||
|
resolve({ chunk: base64Chunk });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const chunkRightPos =
|
||||||
|
this.offset + this.chunkLen > this.file.size
|
||||||
|
? this.file.size
|
||||||
|
: this.offset + this.chunkLen;
|
||||||
|
const blob = this.file.slice(this.offset, chunkRightPos);
|
||||||
|
this.reader.readAsDataURL(blob);
|
||||||
|
|
||||||
|
const result = await readerPromise;
|
||||||
|
if (result.err != null) {
|
||||||
|
this.errMsgs.push(result.err.toString());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const uploadResp = await this.client.uploadChunk(
|
||||||
|
this.filePath,
|
||||||
|
result.chunk,
|
||||||
|
this.offset
|
||||||
|
);
|
||||||
|
|
||||||
|
if (uploadResp.status === 200 && uploadResp.data != null) {
|
||||||
|
this.offset = uploadResp.data.uploaded;
|
||||||
|
this.chunkLen = Math.ceil(this.chunkLen * speedUpRatio);
|
||||||
|
} else if (isFatalErr(uploadResp)) {
|
||||||
|
this.errMsgs.push(`failed to upload chunk: ${uploadResp.statusText}`);
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
// this.errMsgs.push(uploadResp.statusText);
|
||||||
|
this.chunkLen = Math.ceil(this.chunkLen * speedDownRatio);
|
||||||
|
|
||||||
|
const uploadStatusResp = await this.client.uploadStatus(
|
||||||
|
this.filePath
|
||||||
|
);
|
||||||
|
if (uploadStatusResp.status === 200) {
|
||||||
|
this.offset = uploadStatusResp.data.uploaded;
|
||||||
|
} else if (isFatalErr(uploadStatusResp)) {
|
||||||
|
this.errMsgs.push(
|
||||||
|
`failed to get upload status: ${uploadStatusResp.statusText}`
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.errMsgs.push(e.toString());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.progressCb(this.filePath, this.offset, true, this.err());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.chunkLen === 0) {
|
||||||
|
this.errMsgs.push(
|
||||||
|
"the network condition may be poor, please retry later."
|
||||||
|
);
|
||||||
|
} else if (!this.isOn) {
|
||||||
|
this.errMsgs.push("uploading is stopped");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.progressCb(this.filePath, this.offset, false, this.err());
|
||||||
|
return this.offset === this.file.size;
|
||||||
|
};
|
||||||
|
}
|
|
@ -6,8 +6,10 @@
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"target": "es5",
|
"target": "es5",
|
||||||
"jsx": "react",
|
"jsx": "react",
|
||||||
"lib": ["es2015", "dom"]
|
"esModuleInterop": true,
|
||||||
|
"allowJs": true,
|
||||||
|
"lib": ["es5", "dom", "scripthost", "es2015.symbol"]
|
||||||
},
|
},
|
||||||
"include": ["./src/**/*", "webpack.config.js", "webpack..js"],
|
"include": ["./src/**/*"],
|
||||||
"exclude": ["**/*.test.ts", "**/*.test.tsx"]
|
"exclude": ["**/*.test.ts*"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,8 +5,13 @@ const dev = require("./webpack.dev.js");
|
||||||
|
|
||||||
module.exports = merge(dev, {
|
module.exports = merge(dev, {
|
||||||
entry: "./src/app.tsx",
|
entry: "./src/app.tsx",
|
||||||
|
// entry: [
|
||||||
|
// "./src/app.tsx",
|
||||||
|
// "./src/worker/uploader.worker.ts",
|
||||||
|
// ],
|
||||||
context: `${__dirname}`,
|
context: `${__dirname}`,
|
||||||
output: {
|
output: {
|
||||||
|
globalObject: "this",
|
||||||
path: `${__dirname}/../../../public/static`,
|
path: `${__dirname}/../../../public/static`,
|
||||||
chunkFilename: "[name].bundle.js",
|
chunkFilename: "[name].bundle.js",
|
||||||
filename: "[name].bundle.js"
|
filename: "[name].bundle.js"
|
||||||
|
|
|
@ -9,11 +9,20 @@ const BundleAnalyzerPlugin = require("webpack-bundle-analyzer")
|
||||||
module.exports = {
|
module.exports = {
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.worker\.ts$/,
|
||||||
|
use: {
|
||||||
|
loader: "worker-loader",
|
||||||
|
options: {
|
||||||
|
// inline: "fallback",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
test: /\.ts|tsx$/,
|
test: /\.ts|tsx$/,
|
||||||
loader: "ts-loader",
|
loader: "ts-loader",
|
||||||
include: [path.resolve(__dirname, "src")],
|
include: [path.resolve(__dirname, "src")],
|
||||||
exclude: /\.test\.(ts|tsx)$/
|
exclude: [/node_modules/, /\.test\.(ts|tsx)$/],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.css$/,
|
test: /\.css$/,
|
||||||
|
@ -22,15 +31,15 @@ module.exports = {
|
||||||
{
|
{
|
||||||
loader: "css-loader",
|
loader: "css-loader",
|
||||||
options: {
|
options: {
|
||||||
url: false
|
url: false,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
extensions: [".ts", ".tsx", ".js", ".json"]
|
extensions: [".ts", ".tsx", ".js", ".json"],
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
// new BundleAnalyzerPlugin()
|
// new BundleAnalyzerPlugin()
|
||||||
|
@ -38,7 +47,7 @@ module.exports = {
|
||||||
externals: {
|
externals: {
|
||||||
react: "React",
|
react: "React",
|
||||||
"react-dom": "ReactDOM",
|
"react-dom": "ReactDOM",
|
||||||
immutable: "Immutable"
|
immutable: "Immutable",
|
||||||
},
|
},
|
||||||
optimization: {
|
optimization: {
|
||||||
minimizer: [new TerserPlugin()],
|
minimizer: [new TerserPlugin()],
|
||||||
|
@ -48,16 +57,21 @@ module.exports = {
|
||||||
cacheGroups: {
|
cacheGroups: {
|
||||||
default: {
|
default: {
|
||||||
name: "main",
|
name: "main",
|
||||||
filename: "[name].bundle.js"
|
filename: "[name].bundle.js",
|
||||||
},
|
},
|
||||||
commons: {
|
commons: {
|
||||||
test: /[\\/]node_modules[\\/]/,
|
|
||||||
name: "vendors",
|
name: "vendors",
|
||||||
|
test: /[\\/]node_modules[\\/]/,
|
||||||
chunks: "all",
|
chunks: "all",
|
||||||
minChunks: 2,
|
minChunks: 2,
|
||||||
reuseExistingChunk: true
|
reuseExistingChunk: true,
|
||||||
}
|
},
|
||||||
}
|
// worker: {
|
||||||
}
|
// name: "worker",
|
||||||
}
|
// test: /[\\/]worker[\\/]/,
|
||||||
|
// filename: "[name].bundle.js"
|
||||||
|
// }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -118,7 +118,11 @@ func (h *FileHandlers) Create(c *gin.Context) {
|
||||||
locker.Exec(func() {
|
locker.Exec(func() {
|
||||||
err := h.deps.FS().Create(tmpFilePath)
|
err := h.deps.FS().Create(tmpFilePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if os.IsExist(err) {
|
||||||
|
c.JSON(q.ErrResp(c, 304, err))
|
||||||
|
} else {
|
||||||
c.JSON(q.ErrResp(c, 500, err))
|
c.JSON(q.ErrResp(c, 500, err))
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = h.uploadMgr.AddInfo(userName, req.Path, tmpFilePath, req.FileSize)
|
err = h.uploadMgr.AddInfo(userName, req.Path, tmpFilePath, req.FileSize)
|
||||||
|
|
75
yarn.lock
75
yarn.lock
|
@ -1287,6 +1287,11 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.6.tgz#f4c7ec43e81b319a9815115031709f26987891f0"
|
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.6.tgz#f4c7ec43e81b319a9815115031709f26987891f0"
|
||||||
integrity sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==
|
integrity sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==
|
||||||
|
|
||||||
|
"@types/json-schema@^7.0.5":
|
||||||
|
version "7.0.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.7.tgz#98a993516c859eb0d5c4c8f098317a9ea68db9ad"
|
||||||
|
integrity sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==
|
||||||
|
|
||||||
"@types/node@*":
|
"@types/node@*":
|
||||||
version "14.14.14"
|
version "14.14.14"
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.14.tgz#f7fd5f3cc8521301119f63910f0fb965c7d761ae"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.14.tgz#f7fd5f3cc8521301119f63910f0fb965c7d761ae"
|
||||||
|
@ -1624,7 +1629,7 @@ ajv-keywords@^3.1.0, ajv-keywords@^3.5.2:
|
||||||
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d"
|
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d"
|
||||||
integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==
|
integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==
|
||||||
|
|
||||||
ajv@^6.1.0, ajv@^6.12.3, ajv@^6.12.5:
|
ajv@^6.1.0, ajv@^6.12.3, ajv@^6.12.4, ajv@^6.12.5:
|
||||||
version "6.12.6"
|
version "6.12.6"
|
||||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
|
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
|
||||||
integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
|
integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
|
||||||
|
@ -1830,6 +1835,16 @@ babel-jest@^26.6.3:
|
||||||
graceful-fs "^4.2.4"
|
graceful-fs "^4.2.4"
|
||||||
slash "^3.0.0"
|
slash "^3.0.0"
|
||||||
|
|
||||||
|
babel-loader@^8.2.2:
|
||||||
|
version "8.2.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.2.2.tgz#9363ce84c10c9a40e6c753748e1441b60c8a0b81"
|
||||||
|
integrity sha512-JvTd0/D889PQBtUXJ2PXaKU/pjZDMtHA9V2ecm+eNRmmBCMR09a+fmpGTNwnJtFmFl5Ei7Vy47LjBb+L0wQ99g==
|
||||||
|
dependencies:
|
||||||
|
find-cache-dir "^3.3.1"
|
||||||
|
loader-utils "^1.4.0"
|
||||||
|
make-dir "^3.1.0"
|
||||||
|
schema-utils "^2.6.5"
|
||||||
|
|
||||||
babel-plugin-dynamic-import-node@^2.3.3:
|
babel-plugin-dynamic-import-node@^2.3.3:
|
||||||
version "2.3.3"
|
version "2.3.3"
|
||||||
resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz#84fda19c976ec5c6defef57f9427b3def66e17a3"
|
resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz#84fda19c976ec5c6defef57f9427b3def66e17a3"
|
||||||
|
@ -3077,6 +3092,15 @@ find-cache-dir@^2.1.0:
|
||||||
make-dir "^2.0.0"
|
make-dir "^2.0.0"
|
||||||
pkg-dir "^3.0.0"
|
pkg-dir "^3.0.0"
|
||||||
|
|
||||||
|
find-cache-dir@^3.3.1:
|
||||||
|
version "3.3.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.1.tgz#89b33fad4a4670daa94f855f7fbe31d6d84fe880"
|
||||||
|
integrity sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ==
|
||||||
|
dependencies:
|
||||||
|
commondir "^1.0.1"
|
||||||
|
make-dir "^3.0.2"
|
||||||
|
pkg-dir "^4.1.0"
|
||||||
|
|
||||||
find-up@^3.0.0:
|
find-up@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73"
|
resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73"
|
||||||
|
@ -4365,7 +4389,7 @@ loader-runner@^4.1.0:
|
||||||
resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.1.0.tgz#f70bc0c29edbabdf2043e7ee73ccc3fe1c96b42d"
|
resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.1.0.tgz#f70bc0c29edbabdf2043e7ee73ccc3fe1c96b42d"
|
||||||
integrity sha512-oR4lB4WvwFoC70ocraKhn5nkKSs23t57h9udUgw8o0iH8hMXeEoRuUgfcvgUwAJ1ZpRqBvcou4N2SMvM1DwMrA==
|
integrity sha512-oR4lB4WvwFoC70ocraKhn5nkKSs23t57h9udUgw8o0iH8hMXeEoRuUgfcvgUwAJ1ZpRqBvcou4N2SMvM1DwMrA==
|
||||||
|
|
||||||
loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@^1.2.3:
|
loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@^1.2.3, loader-utils@^1.4.0:
|
||||||
version "1.4.0"
|
version "1.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613"
|
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613"
|
||||||
integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==
|
integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==
|
||||||
|
@ -4374,6 +4398,15 @@ loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@^1.2.3:
|
||||||
emojis-list "^3.0.0"
|
emojis-list "^3.0.0"
|
||||||
json5 "^1.0.1"
|
json5 "^1.0.1"
|
||||||
|
|
||||||
|
loader-utils@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.0.tgz#e4cace5b816d425a166b5f097e10cd12b36064b0"
|
||||||
|
integrity sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==
|
||||||
|
dependencies:
|
||||||
|
big.js "^5.2.2"
|
||||||
|
emojis-list "^3.0.0"
|
||||||
|
json5 "^2.1.2"
|
||||||
|
|
||||||
locate-path@^3.0.0:
|
locate-path@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e"
|
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e"
|
||||||
|
@ -4406,7 +4439,7 @@ lodash.sortby@^4.7.0:
|
||||||
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
|
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
|
||||||
integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
|
integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
|
||||||
|
|
||||||
lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20:
|
lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.5:
|
||||||
version "4.17.20"
|
version "4.17.20"
|
||||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
|
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
|
||||||
integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
|
integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
|
||||||
|
@ -4447,7 +4480,7 @@ make-dir@^2.0.0:
|
||||||
pify "^4.0.1"
|
pify "^4.0.1"
|
||||||
semver "^5.6.0"
|
semver "^5.6.0"
|
||||||
|
|
||||||
make-dir@^3.0.0:
|
make-dir@^3.0.0, make-dir@^3.0.2, make-dir@^3.1.0:
|
||||||
version "3.1.0"
|
version "3.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f"
|
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f"
|
||||||
integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==
|
integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==
|
||||||
|
@ -5022,7 +5055,7 @@ pkg-dir@^3.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
find-up "^3.0.0"
|
find-up "^3.0.0"
|
||||||
|
|
||||||
pkg-dir@^4.2.0:
|
pkg-dir@^4.1.0, pkg-dir@^4.2.0:
|
||||||
version "4.2.0"
|
version "4.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3"
|
resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3"
|
||||||
integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==
|
integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==
|
||||||
|
@ -5601,6 +5634,15 @@ schema-utils@^1.0.0:
|
||||||
ajv-errors "^1.0.0"
|
ajv-errors "^1.0.0"
|
||||||
ajv-keywords "^3.1.0"
|
ajv-keywords "^3.1.0"
|
||||||
|
|
||||||
|
schema-utils@^2.6.5:
|
||||||
|
version "2.7.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7"
|
||||||
|
integrity sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==
|
||||||
|
dependencies:
|
||||||
|
"@types/json-schema" "^7.0.5"
|
||||||
|
ajv "^6.12.4"
|
||||||
|
ajv-keywords "^3.5.2"
|
||||||
|
|
||||||
schema-utils@^3.0.0:
|
schema-utils@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.0.0.tgz#67502f6aa2b66a2d4032b4279a2944978a0913ef"
|
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.0.0.tgz#67502f6aa2b66a2d4032b4279a2944978a0913ef"
|
||||||
|
@ -6251,6 +6293,13 @@ ts-loader@^6.0.0:
|
||||||
micromatch "^4.0.0"
|
micromatch "^4.0.0"
|
||||||
semver "^6.0.0"
|
semver "^6.0.0"
|
||||||
|
|
||||||
|
ts-mockito@^2.6.1:
|
||||||
|
version "2.6.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/ts-mockito/-/ts-mockito-2.6.1.tgz#bc9ee2619033934e6fad1c4455aca5b5ace34e73"
|
||||||
|
integrity sha512-qU9m/oEBQrKq5hwfbJ7MgmVN5Gu6lFnIGWvpxSjrqq6YYEVv+RwVFWySbZMBgazsWqv6ctAyVBpo9TmAxnOEKw==
|
||||||
|
dependencies:
|
||||||
|
lodash "^4.17.5"
|
||||||
|
|
||||||
ts-node@^8.2.0:
|
ts-node@^8.2.0:
|
||||||
version "8.10.2"
|
version "8.10.2"
|
||||||
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.10.2.tgz#eee03764633b1234ddd37f8db9ec10b75ec7fb8d"
|
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.10.2.tgz#eee03764633b1234ddd37f8db9ec10b75ec7fb8d"
|
||||||
|
@ -6357,10 +6406,10 @@ typedarray@^0.0.6:
|
||||||
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
|
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
|
||||||
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
|
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
|
||||||
|
|
||||||
typescript@^3.4.3:
|
typescript@^4.1.3:
|
||||||
version "3.9.7"
|
version "4.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.7.tgz#98d600a5ebdc38f40cb277522f12dc800e9e25fa"
|
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.3.tgz#519d582bd94cba0cf8934c7d8e8467e473f53bb7"
|
||||||
integrity sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==
|
integrity sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==
|
||||||
|
|
||||||
typical@^5.0.0, typical@^5.2.0:
|
typical@^5.0.0, typical@^5.2.0:
|
||||||
version "5.2.0"
|
version "5.2.0"
|
||||||
|
@ -6753,6 +6802,14 @@ worker-farm@^1.7.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
errno "~0.1.7"
|
errno "~0.1.7"
|
||||||
|
|
||||||
|
worker-loader@^3.0.7:
|
||||||
|
version "3.0.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/worker-loader/-/worker-loader-3.0.7.tgz#9cf2122a9a781d6742cb873c58c3769591b31988"
|
||||||
|
integrity sha512-LjYLuYJw6kqQKDoygpoD5vWeR1CbZjuVSW3/8pFsptMlUl8gatNM/pszhasSDAWt+dYxMipWB6695k+1zId+iQ==
|
||||||
|
dependencies:
|
||||||
|
loader-utils "^2.0.0"
|
||||||
|
schema-utils "^3.0.0"
|
||||||
|
|
||||||
wrap-ansi@^6.2.0:
|
wrap-ansi@^6.2.0:
|
||||||
version "6.2.0"
|
version "6.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53"
|
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue