diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 474b82d..0000000 --- a/.travis.yml +++ /dev/null @@ -1,7 +0,0 @@ -language: go -sudo: false - -go: - - "1.15.x" - -go_import_path: github.com/ihexxa/quickshare diff --git a/src/client/web/package.json b/src/client/web/package.json index c3c7ed3..0b66f95 100644 --- a/src/client/web/package.json +++ b/src/client/web/package.json @@ -15,8 +15,8 @@ "build:task:dev": "webpack --config webpack.task.dev.js --watch", "e2e": "jest -c jest.e2e.config.js", "e2e:watch": "jest --watch -c jest.e2e.config.js", - "test": "jest test", - "test:watch": "jest test --watch", + "test": "jest test --maxWorkers=2", + "test:watch": "jest test --watch --maxWorkers=2", "copy": "cp -r ../../static ../../../dockers/nginx/" }, "author": "hexxa", diff --git a/src/client/web/src/common/env.ts b/src/client/web/src/common/env.ts new file mode 100644 index 0000000..6a95c48 --- /dev/null +++ b/src/client/web/src/common/env.ts @@ -0,0 +1,7 @@ +export function alertMsg(msg: string) { + if (alert != null) { + alert(msg); + } else { + console.log(msg); + } +} diff --git a/src/client/web/src/components/__test__/browser.test.tsx b/src/client/web/src/components/__test__/browser.test.tsx index a2f8441..86227a0 100644 --- a/src/client/web/src/components/__test__/browser.test.tsx +++ b/src/client/web/src/components/__test__/browser.test.tsx @@ -1,72 +1,74 @@ +import * as React from "react"; import { List, Map } from "immutable"; -import { mock, instance, anyString, when } from "ts-mockito"; +import { mock, instance, anyString, anything, when, verify } from "ts-mockito"; import { ICoreState, initWithWorker, mockState } from "../core_state"; import { makePromise, makeNumberResponse, mockUpdate, + addMockUpdate, + mockFileList, } from "../../test/helpers"; -import { Updater, Browser } from "../browser"; +import { Browser } from "../browser"; +import { Updater, setUpdater } from "../browser.updater"; import { MockUsersClient } from "../../client/users_mock"; import { UsersClient } from "../../client/users"; import { FilesClient } from "../../client/files"; import { FilesClient as MockFilesClient } from "../../client/files_mock"; import { MetadataResp, UploadInfo } from "../../client"; import { MockWorker, UploadEntry } from "../../worker/interface"; -import { UploadMgr } from "../../worker/upload_mgr"; +import { UploadMgr, setUploadMgr } from "../../worker/upload_mgr"; describe("Browser", () => { const mockWorkerClass = mock(MockWorker); const mockWorker = instance(mockWorkerClass); - test("Updater: setPwd", async () => { - const tests = [ - { - listResp: { - status: 200, - statusText: "", - data: { - metadatas: [ - { - name: "file", - size: 1, - modTime: "1-1", - isDir: false, - }, - { - name: "folder", - size: 0, - modTime: "1-1", - isDir: true, - }, - ], - }, - }, - filePath: "path/file", - }, - ]; + test("Updater: addUploads: add each files to UploadMgr", async () => { + let coreState = mockState(); + const UploadMgrClass = mock(UploadMgr); + const uploadMgr = instance(UploadMgrClass); + setUploadMgr(uploadMgr); - const usersClient = new MockUsersClient(""); - const filesClient = new MockFilesClient(""); - for (let i = 0; i < tests.length; i++) { - const tc = tests[i]; + const filePaths = ["./file1", "./file2"]; + const fileList = mockFileList(filePaths); + const updater = new Updater(); + updater.setUploadings = (infos: Map) => {}; + updater.init(coreState.panel.browser); - filesClient.listMock(makePromise(tc.listResp)); - Updater.setClients(usersClient, filesClient); + updater.addUploads(fileList); - const coreState = initWithWorker(mockWorker); - Updater.init(coreState.panel.browser); - await Updater.setItems(List(tc.filePath.split("/"))); - const newState = Updater.setBrowser(coreState); + // it seems that new File will do some file path escaping, so just check call time here + verify(UploadMgrClass.add(anything(), anything())).times(filePaths.length); + // filePaths.forEach((filePath, i) => { + // verify(UploadMgrClass.add(anything(), filePath)).once(); + // }); + }); - newState.panel.browser.items.forEach((item, i) => { - expect(item.name).toEqual(tc.listResp.data.metadatas[i].name); - expect(item.size).toEqual(tc.listResp.data.metadatas[i].size); - expect(item.modTime).toEqual(tc.listResp.data.metadatas[i].modTime); - expect(item.isDir).toEqual(tc.listResp.data.metadatas[i].isDir); - }); - } + test("Updater: deleteUploads: call UploadMgr and api to delete", async () => { + let coreState = mockState(); + const UploadMgrClass = mock(UploadMgr); + const uploadMgr = instance(UploadMgrClass); + setUploadMgr(uploadMgr); + + const updater = new Updater(); + const filesClientClass = mock(FilesClient); + when(filesClientClass.deleteUploading(anyString())).thenResolve({ + status: 200, + statusText: "", + data: "", + }); + const filesClient = instance(filesClientClass); + const usersClientClass = mock(UsersClient); + const usersClient = instance(usersClientClass); + updater.init(coreState.panel.browser); + updater.setClients(usersClient, filesClient); + + const filePath = "./path/file"; + updater.deleteUpload(filePath); + + verify(filesClientClass.deleteUploading(filePath)).once(); + verify(UploadMgrClass.delete(filePath)).once(); }); test("Updater: delete", async () => { @@ -112,21 +114,71 @@ describe("Browser", () => { const filesClient = new MockFilesClient(""); for (let i = 0; i < tests.length; i++) { const tc = tests[i]; - + const updater = new Updater(); + updater.setClients(usersClient, filesClient); filesClient.listMock(makePromise(tc.listResp)); filesClient.deleteMock(makeNumberResponse(200)); - Updater.setClients(usersClient, filesClient); - const coreState = initWithWorker(mockWorker); - Updater.init(coreState.panel.browser); - await Updater.delete( + updater.init(coreState.panel.browser); + + await updater.delete( List(tc.dirPath.split("/")), List(tc.items), - Map(tc.selected) + Map(tc.selected) ); - const newState = Updater.setBrowser(coreState); + + const newState = updater.setBrowser(coreState); // TODO: check inputs of delete + newState.panel.browser.items.forEach((item, i) => { + expect(item.name).toEqual(tc.listResp.data.metadatas[i].name); + expect(item.size).toEqual(tc.listResp.data.metadatas[i].size); + expect(item.modTime).toEqual(tc.listResp.data.metadatas[i].modTime); + expect(item.isDir).toEqual(tc.listResp.data.metadatas[i].isDir); + }); + } + }); + + test("Updater: setItems", async () => { + const tests = [ + { + listResp: { + status: 200, + statusText: "", + data: { + metadatas: [ + { + name: "file", + size: 1, + modTime: "1-1", + isDir: false, + }, + { + name: "folder", + size: 0, + modTime: "1-1", + isDir: true, + }, + ], + }, + }, + filePath: "path/file", + }, + ]; + + const usersClient = new MockUsersClient(""); + const filesClient = new MockFilesClient(""); + + for (let i = 0; i < tests.length; i++) { + const tc = tests[i]; + const updater = new Updater(); + filesClient.listMock(makePromise(tc.listResp)); + updater.setClients(usersClient, filesClient); + const coreState = initWithWorker(mockWorker); + updater.init(coreState.panel.browser); + + await updater.setItems(List(tc.filePath.split("/"))); + const newState = updater.setBrowser(coreState); newState.panel.browser.items.forEach((item, i) => { expect(item.name).toEqual(tc.listResp.data.metadatas[i].name); @@ -173,22 +225,19 @@ describe("Browser", () => { const filesClient = new MockFilesClient(""); for (let i = 0; i < tests.length; i++) { const tc = tests[i]; + const updater = new Updater(); filesClient.listMock(makePromise(tc.listResp)); filesClient.moveMock(makeNumberResponse(200)); - Updater.setClients(usersClient, filesClient); + updater.setClients(usersClient, filesClient); const coreState = initWithWorker(mockWorker); - Updater.init(coreState.panel.browser); - await Updater.moveHere( - tc.dirPath1, - tc.dirPath2, - Map(tc.selected) - ); + updater.init(coreState.panel.browser); + await updater.moveHere(tc.dirPath1, tc.dirPath2, Map(tc.selected)); + + const newState = updater.setBrowser(coreState); // TODO: check inputs of move - - const newState = Updater.setBrowser(coreState); newState.panel.browser.items.forEach((item, i) => { expect(item.name).toEqual(tc.listResp.data.metadatas[i].name); expect(item.size).toEqual(tc.listResp.data.metadatas[i].size); @@ -198,71 +247,36 @@ describe("Browser", () => { } }); - xtest("Browser: deleteUploading", async () => { - interface TestCase { - deleteFile: string; - preState: ICoreState; - postState: ICoreState; - } + test("Browser: deleteUpload: tell uploader to deleteUpload and refreshUploadings", async () => { + let coreState = mockState(); + addMockUpdate(coreState.panel.browser); + const component = new Browser(coreState.panel.browser); + const UpdaterClass = mock(Updater); + const mockUpdater = instance(UpdaterClass); + setUpdater(mockUpdater); + when(UpdaterClass.setItems(anything())).thenResolve(); + when(UpdaterClass.deleteUpload(anyString())).thenResolve(true); + when(UpdaterClass.refreshUploadings()).thenResolve(true); - const tcs: any = [ - { - deleteFile: "./path/file", - preState: { - browser: { - uploadings: List([ - { - realFilePath: "./path/file", - size: 1, - uploaded: 0, - }, - ]), - update: mockUpdate, - }, - }, - postState: { - browser: { - uploadings: List(), - update: mockUpdate, - }, - }, - }, - ]; + const filePath = "filePath"; + await component.deleteUpload(filePath); - const setState = (patch: any, state: ICoreState): ICoreState => { - state.panel.browser = patch.browser; - return state; - }; - const mockFilesClientClass = mock(FilesClient); - when(mockFilesClientClass.deleteUploading(anyString())).thenResolve({ - status: 200, - statusText: "", - data: "", - }); - // TODO: the return should dpends on test case - when(mockFilesClientClass.listUploadings()).thenResolve({ - status: 200, - statusText: "", - data: { uploadInfos: Array() }, - }); + verify(UpdaterClass.deleteUpload(filePath)).once(); + verify(UpdaterClass.refreshUploadings()).once(); + }); - const mockUsersClientClass = mock(UsersClient); + test("Browser: stopUploading: tell updater to stopUploading", async () => { + let coreState = mockState(); + addMockUpdate(coreState.panel.browser); + const component = new Browser(coreState.panel.browser); + const UpdaterClass = mock(Updater); + const mockUpdater = instance(UpdaterClass); + setUpdater(mockUpdater); + when(UpdaterClass.stopUploading(anyString())).thenReturn(); - const mockFilesClient = instance(mockFilesClientClass); - const mockUsersClient = instance(mockUsersClientClass); - tcs.forEach((tc: TestCase) => { - const preState = setState(tc.preState, mockState()); - const postState = setState(tc.postState, mockState()); - // const existingFileName = preState.panel.browser.uploadings.get(0).realFilePath; - const infos:Map = Map(); - UploadMgr._setInfos(infos); + const filePath = "filePath"; + component.stopUploading(filePath); - const component = new Browser(preState.panel.browser); - Updater.init(preState.panel.browser); - Updater.setClients(mockUsersClient, mockFilesClient); - - component.deleteUploading(tc.deleteFile); - expect(Updater.props).toEqual(postState.panel.browser); - }); + verify(UpdaterClass.stopUploading(filePath)).once(); }); }); diff --git a/src/client/web/src/components/browser.tsx b/src/client/web/src/components/browser.tsx index add79dc..ada6c9d 100644 --- a/src/client/web/src/components/browser.tsx +++ b/src/client/web/src/components/browser.tsx @@ -4,6 +4,8 @@ import { List, Map } from "immutable"; import FileSize from "filesize"; import { Layouter } from "./layouter"; +import { alertMsg } from "../common/env"; +import { updater } from "./browser.updater"; import { ICoreState } from "./core_state"; import { IUsersClient, @@ -11,9 +13,7 @@ import { MetadataResp, UploadInfo, } from "../client"; -import { FilesClient } from "../client/files"; -import { UsersClient } from "../client/users"; -import { UploadMgr } from "../worker/upload_mgr"; +import { Up } from "../worker/upload_mgr"; import { UploadEntry } from "../worker/interface"; export const uploadCheckCycle = 1000; @@ -39,143 +39,12 @@ export interface Props { update?: (updater: (prevState: ICoreState) => ICoreState) => void; } -function getItemPath(dirPath: string, itemName: string): string { +export function getItemPath(dirPath: string, itemName: string): string { return dirPath.endsWith("/") ? `${dirPath}${itemName}` : `${dirPath}/${itemName}`; } -export class Updater { - static props: Props; - private static usersClient: IUsersClient; - private static filesClient: IFilesClient; - - static init = (props: Props) => (Updater.props = { ...props }); - static setClients(usersClient: IUsersClient, filesClient: IFilesClient) { - Updater.usersClient = usersClient; - Updater.filesClient = filesClient; - } - - static setUploadings = (infos: Map) => { - Updater.props.uploadings = List( - infos.valueSeq().map( - (v: UploadEntry): UploadInfo => { - return { - realFilePath: v.filePath, - size: v.size, - uploaded: v.uploaded, - }; - } - ) - ); - }; - - static setItems = async (dirParts: List): Promise => { - const dirPath = dirParts.join("/"); - const listResp = await Updater.filesClient.list(dirPath); - - Updater.props.dirPath = dirParts; - Updater.props.items = - listResp.status === 200 - ? List(listResp.data.metadatas) - : Updater.props.items; - }; - - static refreshUploadings = async (): Promise => { - const luResp = await Updater.filesClient.listUploadings(); - - Updater.props.uploadings = - luResp.status === 200 - ? List(luResp.data.uploadInfos) - : Updater.props.uploadings; - return luResp.status === 200; - }; - - static deleteUploading = async (filePath: string): Promise => { - 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 => { - let resp = await Updater.filesClient.mkdir(dirPath); - if (resp.status !== 200) { - alert(`failed to make dir ${dirPath}`); - } - }; - - static delete = async ( - dirParts: List, - items: List, - selectedItems: Map - ): Promise => { - const delRequests = items - .filter((item) => { - return selectedItems.has(item.name); - }) - .map( - async (selectedItem: MetadataResp): Promise => { - const itemPath = getItemPath(dirParts.join("/"), selectedItem.name); - const resp = await Updater.filesClient.delete(itemPath); - return resp.status === 200 ? "" : selectedItem.name; - } - ); - - const failedFiles = await Promise.all(delRequests); - failedFiles.forEach((failedFile) => { - if (failedFile !== "") { - alert(`failed to delete ${failedFile}`); - } - }); - return Updater.setItems(dirParts); - }; - - static moveHere = async ( - srcDir: string, - dstDir: string, - selectedItems: Map - ): Promise => { - const moveRequests = List(selectedItems.keys()).map( - async (itemName: string): Promise => { - const oldPath = getItemPath(srcDir, itemName); - const newPath = getItemPath(dstDir, itemName); - const resp = await Updater.filesClient.move(oldPath, newPath); - return resp.status === 200 ? "" : itemName; - } - ); - - const failedFiles = await Promise.all(moveRequests); - failedFiles.forEach((failedItem) => { - if (failedItem !== "") { - alert(`failed to move ${failedItem}`); - } - }); - - return Updater.setItems(List(dstDir.split("/"))); - }; - - static addUploadFiles = (fileList: FileList, len: number) => { - for (let i = 0; i < len; i++) { - const filePath = getItemPath( - Updater.props.dirPath.join("/"), - fileList[i].name - ); - // do not wait for the promise - UploadMgr.add(fileList[i], filePath); - } - Updater.setUploadings(UploadMgr.list()); - }; - - static setBrowser = (prevState: ICoreState): ICoreState => { - prevState.panel.browser = { ...prevState.panel, ...Updater.props }; - return prevState; - }; -} - export interface State { inputValue: string; selectedSrc: string; @@ -190,8 +59,6 @@ export class Browser extends React.Component { constructor(p: Props) { super(p); - Updater.init(p); - Updater.setClients(new UsersClient(""), new FilesClient("")); this.update = p.update; this.state = { inputValue: "", @@ -199,6 +66,7 @@ export class Browser extends React.Component { selectedItems: Map(), }; + Up().setStatusCb(this.updateProgress); this.uploadInput = undefined; this.assignInput = (input) => { this.uploadInput = ReactDOM.findDOMNode(input); @@ -208,83 +76,64 @@ export class Browser extends React.Component { const uploadInput = this.uploadInput as HTMLButtonElement; uploadInput.click(); }; - - UploadMgr.setStatusCb(this.updateProgress); - Updater.setItems(p.dirPath) - .then(() => { - return Updater.refreshUploadings(); - }) - .then((_: boolean) => { - this.update(Updater.setBrowser); - }); } onInputChange = (ev: React.ChangeEvent) => { this.setState({ inputValue: ev.target.value }); }; - select = (itemName: string) => { - const selectedItems = this.state.selectedItems.has(itemName) - ? this.state.selectedItems.delete(itemName) - : this.state.selectedItems.set(itemName, true); - this.setState({ - selectedSrc: this.props.dirPath.join("/"), - selectedItems: selectedItems, - }); + addUploads = (event: React.ChangeEvent) => { + const fileList = List(); + for (let i = 0; i < event.target.files.length; i++) { + fileList.push(event.target.files[i]); + } + updater().addUploads(fileList); + this.update(updater().setBrowser); }; - addUploadFile = (event: React.ChangeEvent) => { - Updater.addUploadFiles(event.target.files, event.target.files.length); - this.update(Updater.setBrowser); + deleteUpload = (filePath: string): Promise => { + return updater() + .deleteUpload(filePath) + .then((ok: boolean) => { + if (!ok) { + alertMsg(`Failed to delete uploading ${filePath}`); + } + return updater().refreshUploadings(); + }) + .then(() => { + this.update(updater().setBrowser); + }); }; - updateProgress = (infos: Map) => { - Updater.setUploadings(infos); - Updater.setItems(this.props.dirPath).then(() => { - this.update(Updater.setBrowser); - }); + stopUploading = (filePath: string) => { + updater().stopUploading(filePath); + this.update(updater().setBrowser); }; onMkDir = () => { if (this.state.inputValue === "") { - alert("folder name can not be empty"); + alertMsg("folder name can not be empty"); + return; } const dirPath = getItemPath( this.props.dirPath.join("/"), this.state.inputValue ); - Updater.mkDir(dirPath) + updater() + .mkDir(dirPath) .then(() => { this.setState({ inputValue: "" }); - return Updater.setItems(this.props.dirPath); + return updater().setItems(this.props.dirPath); }) .then(() => { - this.update(Updater.setBrowser); + this.update(updater().setBrowser); }); }; - 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 = () => { if (this.props.dirPath.join("/") !== this.state.selectedSrc) { - alert("please select file or folder to delete at first"); + alertMsg("please select file or folder to delete at first"); this.setState({ selectedSrc: this.props.dirPath.join("/"), selectedItems: Map(), @@ -292,17 +141,38 @@ export class Browser extends React.Component { return; } - Updater.delete( - this.props.dirPath, - this.props.items, - this.state.selectedItems - ).then(() => { - this.update(Updater.setBrowser); - this.setState({ - selectedSrc: "", - selectedItems: Map(), + updater() + .delete(this.props.dirPath, this.props.items, this.state.selectedItems) + .then(() => { + this.update(updater().setBrowser); + this.setState({ + selectedSrc: "", + selectedItems: Map(), + }); + }); + }; + + moveHere = () => { + const oldDir = this.state.selectedSrc; + const newDir = this.props.dirPath.join("/"); + if (oldDir === newDir) { + alertMsg("source directory is same as destination directory"); + return; + } + + updater() + .moveHere( + this.state.selectedSrc, + this.props.dirPath.join("/"), + this.state.selectedItems + ) + .then(() => { + this.update(updater().setBrowser); + this.setState({ + selectedSrc: "", + selectedItems: Map(), + }); }); - }); }; gotoChild = (childDirName: string) => { @@ -314,29 +184,49 @@ export class Browser extends React.Component { return; } - Updater.setItems(dirPath).then(() => { - this.update(Updater.setBrowser); + updater() + .setItems(dirPath) + .then(() => { + this.update(updater().setBrowser); + }); + }; + + updateProgress = (infos: Map) => { + updater().setUploadings(infos); + updater() + .setItems(this.props.dirPath) + .then(() => { + this.update(updater().setBrowser); + }); + }; + + select = (itemName: string) => { + const selectedItems = this.state.selectedItems.has(itemName) + ? this.state.selectedItems.delete(itemName) + : this.state.selectedItems.set(itemName, true); + + this.setState({ + selectedSrc: this.props.dirPath.join("/"), + selectedItems: selectedItems, }); }; - moveHere = () => { - const oldDir = this.state.selectedSrc; - const newDir = this.props.dirPath.join("/"); - if (oldDir === newDir) { - alert("source directory is same as destination directory"); - return; + selectAll = () => { + let newSelected = Map(); + const someSelected = this.state.selectedItems.size === 0 ? true : false; + if (someSelected) { + this.props.items.forEach((item) => { + newSelected = newSelected.set(item.name, true); + }); + } else { + this.props.items.forEach((item) => { + newSelected = newSelected.delete(item.name); + }); } - Updater.moveHere( - this.state.selectedSrc, - this.props.dirPath.join("/"), - this.state.selectedItems - ).then(() => { - this.update(Updater.setBrowser); - this.setState({ - selectedSrc: "", - selectedItems: Map(), - }); + this.setState({ + selectedSrc: this.props.dirPath.join("/"), + selectedItems: newSelected, }); }; @@ -397,7 +287,7 @@ export class Browser extends React.Component { { Stop + {itemList} diff --git a/src/client/web/src/components/browser.updater.ts b/src/client/web/src/components/browser.updater.ts new file mode 100644 index 0000000..4343e7a --- /dev/null +++ b/src/client/web/src/components/browser.updater.ts @@ -0,0 +1,153 @@ +import { List, Map } from "immutable"; + +import { ICoreState } from "./core_state"; +import { Props, getItemPath } from "./browser"; +import { + IUsersClient, + IFilesClient, + MetadataResp, + UploadInfo, +} from "../client"; +import { FilesClient } from "../client/files"; +import { UsersClient } from "../client/users"; +import { UploadEntry } from "../worker/interface"; +import { Up } from "../worker/upload_mgr"; + +export class Updater { + props: Props; + private usersClient: IUsersClient = new UsersClient(""); + private filesClient: IFilesClient = new FilesClient(""); + + init = (props: Props) => (this.props = { ...props }); + setClients(usersClient: IUsersClient, filesClient: IFilesClient) { + this.usersClient = usersClient; + this.filesClient = filesClient; + } + + addUploads = (fileList: List) => { + fileList.forEach(file => { + const filePath = getItemPath( + this.props.dirPath.join("/"), + file.name + ); + // do not wait for the promise + Up().add(file, filePath); + }) + this.setUploadings(Up().list()); + }; + + deleteUpload = async (filePath: string): Promise => { + Up().delete(filePath); + const resp = await this.filesClient.deleteUploading(filePath); + return resp.status === 200; + }; + + setUploadings = (infos: Map) => { + this.props.uploadings = List( + infos.valueSeq().map( + (v: UploadEntry): UploadInfo => { + return { + realFilePath: v.filePath, + size: v.size, + uploaded: v.uploaded, + }; + } + ) + ); + }; + + refreshUploadings = async (): Promise => { + const luResp = await this.filesClient.listUploadings(); + + this.props.uploadings = + luResp.status === 200 + ? List(luResp.data.uploadInfos) + : this.props.uploadings; + return luResp.status === 200; + }; + + stopUploading = (filePath: string) => { + Up().stop(filePath); + }; + + mkDir = async (dirPath: string): Promise => { + const resp = await this.filesClient.mkdir(dirPath); + if (resp.status !== 200) { + alert(`failed to make dir ${dirPath}`); + } + }; + + delete = async ( + dirParts: List, + items: List, + selectedItems: Map + ): Promise => { + const delRequests = items + .filter((item) => { + return selectedItems.has(item.name); + }) + .map( + async (selectedItem: MetadataResp): Promise => { + const itemPath = getItemPath(dirParts.join("/"), selectedItem.name); + const resp = await this.filesClient.delete(itemPath); + return resp.status === 200 ? "" : selectedItem.name; + } + ); + + const failedFiles = await Promise.all(delRequests); + failedFiles.forEach((failedFile) => { + if (failedFile !== "") { + alert(`failed to delete ${failedFile}`); + } + }); + return this.setItems(dirParts); + }; + + setItems = async (dirParts: List): Promise => { + const dirPath = dirParts.join("/"); + const listResp = await this.filesClient.list(dirPath); + + this.props.dirPath = dirParts; + this.props.items = + listResp.status === 200 + ? List(listResp.data.metadatas) + : this.props.items; + }; + + moveHere = async ( + srcDir: string, + dstDir: string, + selectedItems: Map + ): Promise => { + const moveRequests = List(selectedItems.keys()).map( + async (itemName: string): Promise => { + const oldPath = getItemPath(srcDir, itemName); + const newPath = getItemPath(dstDir, itemName); + const resp = await this.filesClient.move(oldPath, newPath); + return resp.status === 200 ? "" : itemName; + } + ); + + const failedFiles = await Promise.all(moveRequests); + failedFiles.forEach((failedItem) => { + if (failedItem !== "") { + alert(`failed to move ${failedItem}`); + } + }); + + return this.setItems(List(dstDir.split("/"))); + }; + + setBrowser = (prevState: ICoreState): ICoreState => { + prevState.panel.browser = { ...prevState.panel, ...this.props }; + return prevState; + }; +} + +export let browserUpdater = new Updater(); +export const updater = (): Updater => { + return browserUpdater; +}; +export const setUpdater = (updater: Updater) => { + browserUpdater = updater; +}; diff --git a/src/client/web/src/components/core_state.ts b/src/client/web/src/components/core_state.ts index bc368ce..7be585e 100644 --- a/src/client/web/src/components/core_state.ts +++ b/src/client/web/src/components/core_state.ts @@ -6,7 +6,7 @@ import { FgWorker } from "../worker/upload.fgworker"; import { Props as PanelProps } from "./root_frame"; import { Item } from "./browser"; import { UploadInfo } from "../client"; -import { UploadMgr, IWorker } from "../worker/upload_mgr"; +import { Up, initUploadMgr, IWorker } from "../worker/upload_mgr"; export class BaseUpdater { public static props: any; @@ -27,15 +27,15 @@ export interface ICoreState { } export function initWithWorker(worker: IWorker): ICoreState { - UploadMgr.init(worker); + initUploadMgr(worker); return initState(); } export function init(): ICoreState { const scripts = Array.from(document.querySelectorAll("script")); const worker = Worker == null ? new FgWorker() : new BgWorker(); + initUploadMgr(worker); - UploadMgr.init(worker); return initState(); } diff --git a/src/client/web/src/components/pane_login.tsx b/src/client/web/src/components/pane_login.tsx index c8ab867..2f1700f 100644 --- a/src/client/web/src/components/pane_login.tsx +++ b/src/client/web/src/components/pane_login.tsx @@ -5,7 +5,7 @@ import { ICoreState } from "./core_state"; import { IUsersClient } from "../client"; import { UsersClient } from "../client/users"; import { Updater as PanesUpdater } from "./panes"; -import { Updater as BrowserUpdater } from "./browser"; +import { updater as BrowserUpdater } from "./browser.updater"; import { Layouter } from "./layouter"; export interface Props { @@ -104,7 +104,7 @@ export class AuthPane extends React.Component { this.update(PanesUpdater.updateState); // refresh - return BrowserUpdater.setItems( + return BrowserUpdater().setItems( List(["."]) ); } else { @@ -113,10 +113,10 @@ export class AuthPane extends React.Component { } }) .then(() => { - return BrowserUpdater.refreshUploadings(); + return BrowserUpdater().refreshUploadings(); }) .then((_: boolean) => { - this.update(BrowserUpdater.setBrowser); + this.update(BrowserUpdater().setBrowser); }); }; diff --git a/src/client/web/src/components/state_mgr.tsx b/src/client/web/src/components/state_mgr.tsx index e492808..7795d92 100644 --- a/src/client/web/src/components/state_mgr.tsx +++ b/src/client/web/src/components/state_mgr.tsx @@ -1,7 +1,10 @@ import * as React from "react"; +import { updater as BrowserUpdater } from "./browser.updater"; import { ICoreState, init } from "./core_state"; import { RootFrame } from "./root_frame"; +import { FilesClient } from "../client/files"; +import { UsersClient } from "../client/users"; export interface Props {} export interface State extends ICoreState {} @@ -10,8 +13,22 @@ export class StateMgr extends React.Component { constructor(p: Props) { super(p); this.state = init(); + this.initUpdaters(this.state); } + initUpdaters = (state: ICoreState) => { + BrowserUpdater().init(state.panel.browser); + BrowserUpdater().setClients(new UsersClient(""), new FilesClient("")); + BrowserUpdater() + .setItems(state.panel.browser.dirPath) + .then(() => { + return BrowserUpdater().refreshUploadings(); + }) + .then((_: boolean) => { + this.update(BrowserUpdater().setBrowser); + }); + }; + update = (apply: (prevState: ICoreState) => ICoreState): void => { this.setState(apply(this.state)); }; diff --git a/src/client/web/src/test/helpers.ts b/src/client/web/src/test/helpers.ts index 3f0ad21..069a50b 100644 --- a/src/client/web/src/test/helpers.ts +++ b/src/client/web/src/test/helpers.ts @@ -1,5 +1,6 @@ import { Response } from "../client"; -import { ICoreState } from "../components/core_state"; +import { ICoreState, mockState } from "../components/core_state"; +import { List } from "immutable"; export const makePromise = (ret: any): Promise => { return new Promise((resolve) => { @@ -15,4 +16,25 @@ export const makeNumberResponse = (status: number): Promise => { }); }; -export const mockUpdate = (apply: (prevState: ICoreState) => ICoreState): void => {}; +export const mockUpdate = ( + apply: (prevState: ICoreState) => ICoreState +): void => { + apply(mockState()); +}; + +export const addMockUpdate = (subState: any) => { + subState.update = mockUpdate; +}; + +export function mockRandFile(filePath: string): File { + const values = new Array(Math.floor(7 * Math.random())); + const content = [values.join("")]; + return new File(content, filePath); +} + +export function mockFileList(filePaths: Array): List { + const files = filePaths.map(filePath => { + return mockRandFile(filePath); + }) + return List(files); +} diff --git a/src/client/web/src/worker/__test__/upload_mgr.test.ts b/src/client/web/src/worker/__test__/upload_mgr.test.ts index 7f6b352..37a5c6e 100644 --- a/src/client/web/src/worker/__test__/upload_mgr.test.ts +++ b/src/client/web/src/worker/__test__/upload_mgr.test.ts @@ -4,7 +4,7 @@ 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 { Up, initUploadMgr } from "../upload_mgr"; import { FileWorkerReq, @@ -115,17 +115,18 @@ describe("UploadMgr", () => { ]; const worker = new MockWorker(); - UploadMgr.setCycle(100); - for (let i = 0; i < tcs.length; i++) { - const infoMap = arraytoMap(tcs[i].inputInfos); - UploadMgr._setInfos(infoMap); + initUploadMgr(worker); + const up = Up(); + up.setCycle(100); + + const infoMap = arraytoMap(tcs[i].inputInfos); + up._setInfos(infoMap); - UploadMgr.init(worker); // polling needs several rounds to finish all the tasks - await delay(tcs.length * UploadMgr.getCycle() + 1000); + await delay(tcs.length * up.getCycle() + 1000); // TODO: find a better way to wait - const gotInfos = UploadMgr.list(); + const gotInfos = up.list(); const expectedInfoMap = arraytoMap(tcs[i].expectedInfos); gotInfos.keySeq().forEach((filePath) => { @@ -135,7 +136,7 @@ describe("UploadMgr", () => { expect(expectedInfoMap.get(filePath)).toEqual(gotInfos.get(filePath)); }); - UploadMgr.destory(); + up.destory(); } }); }); diff --git a/src/client/web/src/worker/upload_mgr.ts b/src/client/web/src/worker/upload_mgr.ts index 53d1518..3e4c6ac 100644 --- a/src/client/web/src/worker/upload_mgr.ts +++ b/src/client/web/src/worker/upload_mgr.ts @@ -10,8 +10,9 @@ import { errKind, uploadInfoKind, } from "./interface"; +import { FgWorker } from "./upload.fgworker"; -const win = self as any; +const win: Window = self as any; export interface IWorker { onmessage: (event: MessageEvent) => void; @@ -19,51 +20,51 @@ export interface IWorker { } export class UploadMgr { - private static infos = Map(); - private static worker: IWorker; - private static intervalID: NodeJS.Timeout; - private static cycle: number = 500; - private static statusCb = (infos: Map):void => {}; + private infos = Map(); + private worker: IWorker; + private intervalID: number; + private cycle: number = 500; + private statusCb = (infos: Map): void => {}; - static _setInfos = (infos: Map) => { - UploadMgr.infos = infos; - }; - - static setCycle = (ms: number) => { - UploadMgr.cycle = ms; - }; - - static getCycle = (): number => { - return UploadMgr.cycle; - }; - - static setStatusCb = (cb: (infos: Map) => void) => { - UploadMgr.statusCb = cb; - } - - static init = (worker: IWorker) => { - UploadMgr.worker = worker; + constructor(worker: IWorker) { + this.worker = worker; // TODO: fallback to normal if Web Worker is not available - UploadMgr.worker.onmessage = UploadMgr.respHandler; + this.worker.onmessage = this.respHandler; const syncing = () => { - UploadMgr.worker.postMessage({ + this.worker.postMessage({ kind: syncReqKind, - infos: UploadMgr.infos.valueSeq().toArray(), + infos: this.infos.valueSeq().toArray(), }); }; - UploadMgr.intervalID = win.setInterval(syncing, UploadMgr.cycle); + this.intervalID = win.setInterval(syncing, this.cycle); + } + + destory = () => { + win.clearInterval(this.intervalID); }; - static destory = () => { - win.clearInterval(UploadMgr.intervalID); + _setInfos = (infos: Map) => { + this.infos = infos; }; - static add = (file: File, filePath: string) => { - const entry = UploadMgr.infos.get(filePath); + setCycle = (ms: number) => { + this.cycle = ms; + }; + + getCycle = (): number => { + return this.cycle; + }; + + setStatusCb = (cb: (infos: Map) => void) => { + this.statusCb = cb; + }; + + add = (file: File, filePath: string) => { + const entry = this.infos.get(filePath); if (entry == null) { // new uploading - UploadMgr.infos = UploadMgr.infos.set(filePath, { + this.infos = this.infos.set(filePath, { file: file, filePath: filePath, size: file.size, @@ -73,36 +74,35 @@ export class UploadMgr { }); } else { // restart the uploading - UploadMgr.infos = UploadMgr.infos.set(filePath, { + this.infos = this.infos.set(filePath, { ...entry, runnable: true, }); } }; - static stop = (filePath: string) => { - const entry = UploadMgr.infos.get(filePath); + stop = (filePath: string) => { + const entry = this.infos.get(filePath); if (entry != null) { - UploadMgr.infos = UploadMgr.infos.set(filePath, { + this.infos = this.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); + delete = (filePath: string) => { + this.stop(filePath); + this.infos = this.infos.delete(filePath); }; - static list = (): Map => { - return UploadMgr.infos; + list = (): Map => { + return this.infos; }; - static respHandler = (event: MessageEvent) => { + respHandler = (event: MessageEvent) => { const resp = event.data as FileWorkerResp; switch (resp.kind) { @@ -113,13 +113,13 @@ export class UploadMgr { break; case uploadInfoKind: const infoResp = resp as UploadInfoResp; - const entry = UploadMgr.infos.get(infoResp.filePath); + const entry = this.infos.get(infoResp.filePath); if (entry != null) { if (infoResp.uploaded === entry.size) { - UploadMgr.infos = UploadMgr.infos.delete(infoResp.filePath); + this.infos = this.infos.delete(infoResp.filePath); } else { - UploadMgr.infos = UploadMgr.infos.set(infoResp.filePath, { + this.infos = this.infos.set(infoResp.filePath, { ...entry, uploaded: infoResp.uploaded, runnable: infoResp.runnable, @@ -128,13 +128,13 @@ export class UploadMgr { } // call back to update the info - UploadMgr.statusCb(UploadMgr.infos); + this.statusCb(this.infos); } else { // TODO: refine this console.error( `respHandler: fail to found: file(${ infoResp.filePath - }) infos(${UploadMgr.infos.toObject()})` + }) infos(${this.infos.toObject()})` ); } break; @@ -143,3 +143,15 @@ export class UploadMgr { } }; } + +export let uploadMgr = new UploadMgr(new FgWorker()); +export const initUploadMgr = (worker: IWorker): UploadMgr => { + uploadMgr = new UploadMgr(worker); + return uploadMgr; +}; +export const Up = (): UploadMgr => { + return uploadMgr; +}; +export const setUploadMgr = (up: UploadMgr) => { + uploadMgr = up; +};