chore(client): clean up and ci (#35)

* fix(client/web): move browser updater to single file

* fix(client/web): make UploadMgr singleton

* test(client/web): add unit tests for browser

* fix(client/web): updater init should be in StateMgr

* feat(client/browser): add selectAll button

* chore(ci): disable travis although it is awsome
This commit is contained in:
Hexxa 2021-01-30 16:48:21 +08:00 committed by GitHub
parent 46f03e2e84
commit e87a342c93
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 532 additions and 415 deletions

View file

@ -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",

View file

@ -0,0 +1,7 @@
export function alertMsg(msg: string) {
if (alert != null) {
alert(msg);
} else {
console.log(msg);
}
}

View file

@ -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<string, UploadEntry>) => {};
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<string>(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<string>(tc.dirPath.split("/")),
List<MetadataResp>(tc.items),
Map<boolean>(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<string>(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<boolean>(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<UploadInfo>([
{
realFilePath: "./path/file",
size: 1,
uploaded: 0,
},
]),
update: mockUpdate,
},
},
postState: {
browser: {
uploadings: List<UploadInfo>(),
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<UploadInfo>() },
});
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<string, UploadEntry> = 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();
});
});

View file

@ -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<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> => {
const dirPath = dirParts.join("/");
const listResp = await Updater.filesClient.list(dirPath);
Updater.props.dirPath = dirParts;
Updater.props.items =
listResp.status === 200
? List<MetadataResp>(listResp.data.metadatas)
: 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> => {
let resp = await Updater.filesClient.mkdir(dirPath);
if (resp.status !== 200) {
alert(`failed to make dir ${dirPath}`);
}
};
static delete = async (
dirParts: List<string>,
items: List<MetadataResp>,
selectedItems: Map<string, boolean>
): Promise<void> => {
const delRequests = items
.filter((item) => {
return selectedItems.has(item.name);
})
.map(
async (selectedItem: MetadataResp): Promise<string> => {
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<string, boolean>
): Promise<void> => {
const moveRequests = List<string>(selectedItems.keys()).map(
async (itemName: string): Promise<string> => {
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<string>(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<Props, State, {}> {
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<Props, State, {}> {
selectedItems: Map<string, boolean>(),
};
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<Props, State, {}> {
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<HTMLInputElement>) => {
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<HTMLInputElement>) => {
const fileList = List<File>();
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<HTMLInputElement>) => {
Updater.addUploadFiles(event.target.files, event.target.files.length);
this.update(Updater.setBrowser);
deleteUpload = (filePath: string): Promise<void> => {
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<string, UploadEntry>) => {
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<string, boolean>(),
@ -292,17 +141,38 @@ export class Browser extends React.Component<Props, State, {}> {
return;
}
Updater.delete(
this.props.dirPath,
this.props.items,
this.state.selectedItems
).then(() => {
this.update(Updater.setBrowser);
this.setState({
selectedSrc: "",
selectedItems: Map<string, boolean>(),
updater()
.delete(this.props.dirPath, this.props.items, this.state.selectedItems)
.then(() => {
this.update(updater().setBrowser);
this.setState({
selectedSrc: "",
selectedItems: Map<string, boolean>(),
});
});
};
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<string, boolean>(),
});
});
});
};
gotoChild = (childDirName: string) => {
@ -314,29 +184,49 @@ export class Browser extends React.Component<Props, State, {}> {
return;
}
Updater.setItems(dirPath).then(() => {
this.update(Updater.setBrowser);
updater()
.setItems(dirPath)
.then(() => {
this.update(updater().setBrowser);
});
};
updateProgress = (infos: Map<string, UploadEntry>) => {
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<string, boolean>();
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<string, boolean>(),
});
this.setState({
selectedSrc: this.props.dirPath.join("/"),
selectedItems: newSelected,
});
};
@ -397,7 +287,7 @@ export class Browser extends React.Component<Props, State, {}> {
</button>
<input
type="file"
onChange={this.addUploadFile}
onChange={this.addUploads}
multiple={true}
value={this.props.uploadValue}
ref={this.assignInput}
@ -505,7 +395,7 @@ export class Browser extends React.Component<Props, State, {}> {
Stop
</button>
<button
onClick={() => this.deleteUploading(uploading.realFilePath)}
onClick={() => this.deleteUpload(uploading.realFilePath)}
className="white-font"
>
Delete
@ -563,7 +453,15 @@ export class Browser extends React.Component<Props, State, {}> {
<td>Name</td>
<td className={sizeCellClass}>File Size</td>
<td className={modTimeCellClass}>Mod Time</td>
<td>Edit</td>
<td>
<button
onClick={() => this.selectAll()}
className={`white-font`}
style={{ width: "8rem", display: "inline-block" }}
>
Select All
</button>
</td>
</tr>
</thead>
<tbody>{itemList}</tbody>

View file

@ -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<File>) => {
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<boolean> => {
Up().delete(filePath);
const resp = await this.filesClient.deleteUploading(filePath);
return resp.status === 200;
};
setUploadings = (infos: Map<string, UploadEntry>) => {
this.props.uploadings = List<UploadInfo>(
infos.valueSeq().map(
(v: UploadEntry): UploadInfo => {
return {
realFilePath: v.filePath,
size: v.size,
uploaded: v.uploaded,
};
}
)
);
};
refreshUploadings = async (): Promise<boolean> => {
const luResp = await this.filesClient.listUploadings();
this.props.uploadings =
luResp.status === 200
? List<UploadInfo>(luResp.data.uploadInfos)
: this.props.uploadings;
return luResp.status === 200;
};
stopUploading = (filePath: string) => {
Up().stop(filePath);
};
mkDir = async (dirPath: string): Promise<void> => {
const resp = await this.filesClient.mkdir(dirPath);
if (resp.status !== 200) {
alert(`failed to make dir ${dirPath}`);
}
};
delete = async (
dirParts: List<string>,
items: List<MetadataResp>,
selectedItems: Map<string, boolean>
): Promise<void> => {
const delRequests = items
.filter((item) => {
return selectedItems.has(item.name);
})
.map(
async (selectedItem: MetadataResp): Promise<string> => {
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<string>): Promise<void> => {
const dirPath = dirParts.join("/");
const listResp = await this.filesClient.list(dirPath);
this.props.dirPath = dirParts;
this.props.items =
listResp.status === 200
? List<MetadataResp>(listResp.data.metadatas)
: this.props.items;
};
moveHere = async (
srcDir: string,
dstDir: string,
selectedItems: Map<string, boolean>
): Promise<void> => {
const moveRequests = List<string>(selectedItems.keys()).map(
async (itemName: string): Promise<string> => {
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<string>(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;
};

View file

@ -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();
}

View file

@ -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<Props, State, {}> {
this.update(PanesUpdater.updateState);
// refresh
return BrowserUpdater.setItems(
return BrowserUpdater().setItems(
List<string>(["."])
);
} else {
@ -113,10 +113,10 @@ export class AuthPane extends React.Component<Props, State, {}> {
}
})
.then(() => {
return BrowserUpdater.refreshUploadings();
return BrowserUpdater().refreshUploadings();
})
.then((_: boolean) => {
this.update(BrowserUpdater.setBrowser);
this.update(BrowserUpdater().setBrowser);
});
};

View file

@ -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<Props, State, {}> {
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));
};

View file

@ -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<any> => {
return new Promise<any>((resolve) => {
@ -15,4 +16,25 @@ export const makeNumberResponse = (status: number): Promise<Response> => {
});
};
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<string>(Math.floor(7 * Math.random()));
const content = [values.join("")];
return new File(content, filePath);
}
export function mockFileList(filePaths: Array<string>): List<File> {
const files = filePaths.map(filePath => {
return mockRandFile(filePath);
})
return List<File>(files);
}

View file

@ -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();
}
});
});

View file

@ -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<string, UploadEntry>();
private static worker: IWorker;
private static intervalID: NodeJS.Timeout;
private static cycle: number = 500;
private static statusCb = (infos: Map<string, UploadEntry>):void => {};
private infos = Map<string, UploadEntry>();
private worker: IWorker;
private intervalID: number;
private cycle: number = 500;
private 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;
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<string, UploadEntry>) => {
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<string, UploadEntry>) => 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<string, UploadEntry> => {
return UploadMgr.infos;
list = (): Map<string, UploadEntry> => {
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;
};