fix(uploader): add error info and fix issues in uploader

This commit is contained in:
hexxa 2021-09-12 20:35:34 +08:00 committed by Hexxa
parent e462c349a5
commit 3b1508af51
6 changed files with 138 additions and 92 deletions

View file

@ -8,7 +8,7 @@ server:
debug: true debug: true
host: "127.0.0.1" host: "127.0.0.1"
port: 8686 port: 8686
readTimeout: 2000 readTimeout: 4000
writerTimeout: 86400000 # 1 day writerTimeout: 86400000 # 1 day
maxHeaderBytes: 512 maxHeaderBytes: 512
publicPath: "public" publicPath: "public"

View file

@ -725,3 +725,9 @@ div.hr {
overflow-wrap: break-word; overflow-wrap: break-word;
display: block; display: block;
} }
.alert-red {
border: dashed 1px #e74c3c;
color: #e74c3c;
border-radius: 0.5rem;
}

View file

@ -33,7 +33,7 @@ export interface BrowserProps {
dirPath: List<string>; dirPath: List<string>;
isSharing: boolean; isSharing: boolean;
items: List<MetadataResp>; items: List<MetadataResp>;
uploadings: List<UploadInfo>; uploadings: List<UploadEntry>;
sharings: List<string>; sharings: List<string>;
uploadFiles: List<File>; uploadFiles: List<File>;
@ -328,9 +328,8 @@ export class Browser extends React.Component<Props, State, {}> {
} }
); );
const nameCellClass = `item-name item-name-${ const nameCellClass = `item-name item-name-${this.props.browser.isVertical ? "vertical" : "horizontal"
this.props.browser.isVertical ? "vertical" : "horizontal" } pointer`;
} pointer`;
const sizeCellClass = this.props.browser.isVertical const sizeCellClass = this.props.browser.isVertical
? `hidden margin-s` ? `hidden margin-s`
: ``; : ``;
@ -422,9 +421,8 @@ export class Browser extends React.Component<Props, State, {}> {
<span className="padding-m"> <span className="padding-m">
<button <button
onClick={() => this.select(item.name)} onClick={() => this.select(item.name)}
className={`${ className={`${isSelected ? "blue0-bg white-font" : "grey2-bg grey3-font"
isSelected ? "blue0-bg white-font" : "grey2-bg grey3-font" }`}
}`}
style={{ width: "8rem", display: "inline-block" }} style={{ width: "8rem", display: "inline-block" }}
> >
{isSelected {isSelected
@ -470,9 +468,8 @@ export class Browser extends React.Component<Props, State, {}> {
<button <button
type="button" type="button"
onClick={() => this.select(item.name)} onClick={() => this.select(item.name)}
className={`${ className={`${isSelected ? "blue0-bg white-font" : "grey2-bg grey3-font"
isSelected ? "blue0-bg white-font" : "grey2-bg grey3-font" }`}
}`}
style={{ width: "8rem", display: "inline-block" }} style={{ width: "8rem", display: "inline-block" }}
> >
{isSelected {isSelected
@ -593,50 +590,58 @@ export class Browser extends React.Component<Props, State, {}> {
) : null; ) : null;
const uploadingList = this.props.browser.uploadings.map( const uploadingList = this.props.browser.uploadings.map(
(uploading: UploadInfo) => { (uploading: UploadEntry) => {
const pathParts = uploading.realFilePath.split("/"); const pathParts = uploading.filePath.split("/");
const fileName = pathParts[pathParts.length - 1]; const fileName = pathParts[pathParts.length - 1];
return ( return (
<Flexbox <div key={uploading.filePath}>
key={uploading.realFilePath} <Flexbox
children={List([ children={List([
<span className="padding-m"> <span className="padding-m">
<Flexbox <Flexbox
children={List([ children={List([
<RiUploadCloudLine <RiUploadCloudLine
size="3rem" size="3rem"
className="margin-r-m blue0-font" className="margin-r-m blue0-font"
/>, />,
<div className={`${nameCellClass}`}> <div className={`${nameCellClass}`}>
<span className="title-m">{fileName}</span> <span className="title-m">{fileName}</span>
<div className="desc-m grey0-font"> <div className="desc-m grey0-font">
{FileSize(uploading.uploaded, { round: 0 })} {FileSize(uploading.uploaded, { round: 0 })}
&nbsp;/&nbsp;{FileSize(uploading.size, { round: 0 })} &nbsp;/&nbsp;{FileSize(uploading.size, { round: 0 })}
</div> </div>
</div>, </div>,
])} ])}
/> />
</span>, </span>,
<div className="item-op padding-m"> <div className="item-op padding-m">
<button <button
onClick={() => this.stopUploading(uploading.realFilePath)} onClick={() => this.stopUploading(uploading.filePath)}
className="grey3-bg grey4-font margin-r-m" className="grey3-bg grey4-font margin-r-m"
> >
{this.props.msg.pkg.get("browser.stop")} {this.props.msg.pkg.get("browser.stop")}
</button> </button>
<button <button
onClick={() => this.deleteUpload(uploading.realFilePath)} onClick={() => this.deleteUpload(uploading.filePath)}
className="grey3-bg grey4-font" className="grey3-bg grey4-font"
> >
{this.props.msg.pkg.get("browser.delete")} {this.props.msg.pkg.get("browser.delete")}
</button> </button>
</div>, </div>,
])} ])}
childrenStyles={List([{}, { justifyContent: "flex-end" }])} childrenStyles={List([{}, { justifyContent: "flex-end" }])}
/> />
{uploading.err.trim() === "" ? null : (
<div className="alert-red margin-s">
<span className="padding-m">
{uploading.err.trim()}
</span>
</div>
)}
</div>
); );
} }
); );
@ -668,7 +673,7 @@ export class Browser extends React.Component<Props, State, {}> {
/> />
</div> </div>
) : ( ) : (
<div className="container"> <div className="container padding-b-m">
<Flexbox <Flexbox
children={List([ children={List([
<span className="padding-m"> <span className="padding-m">
@ -721,9 +726,8 @@ export class Browser extends React.Component<Props, State, {}> {
type="text" type="text"
readOnly readOnly
className="margin-r-m" className="margin-r-m"
value={`${ value={`${document.location.href.split("?")[0]
document.location.href.split("?")[0] }?dir=${encodeURIComponent(dirPath)}`}
}?dir=${encodeURIComponent(dirPath)}`}
/> />
<button <button
onClick={() => { onClick={() => {

View file

@ -2,6 +2,7 @@ import { List, Set, Map } from "immutable";
import BgWorker from "../worker/upload.bg.worker"; import BgWorker from "../worker/upload.bg.worker";
import { FgWorker } from "../worker/upload.fg.worker"; import { FgWorker } from "../worker/upload.fg.worker";
import { UploadEntry } from "../worker/interface";
import { BrowserProps } from "./browser"; import { BrowserProps } from "./browser";
import { PanesProps } from "./panes"; import { PanesProps } from "./panes";
@ -9,8 +10,7 @@ import { LoginProps } from "./pane_login";
import { AdminProps } from "./pane_admin"; import { AdminProps } from "./pane_admin";
import { MsgPackage } from "../i18n/msger"; import { MsgPackage } from "../i18n/msger";
import { Item } from "./browser"; import { User, MetadataResp } from "../client";
import { UploadInfo, User, MetadataResp } from "../client";
import { initUploadMgr, IWorker } from "../worker/upload_mgr"; import { initUploadMgr, IWorker } from "../worker/upload_mgr";
export interface MsgProps { export interface MsgProps {
@ -56,7 +56,7 @@ export function initState(): ICoreState {
items: List<MetadataResp>([]), items: List<MetadataResp>([]),
sharings: List<string>([]), sharings: List<string>([]),
isSharing: false, isSharing: false,
uploadings: List<UploadInfo>([]), uploadings: List<UploadEntry>([]),
uploadValue: "", uploadValue: "",
uploadFiles: List<File>([]), uploadFiles: List<File>([]),
tab: "", tab: "",

View file

@ -39,7 +39,7 @@ export class Updater {
initUploads = () => { initUploads = () => {
this.props.browser.uploadings.forEach((entry) => { this.props.browser.uploadings.forEach((entry) => {
Up().addStopped(entry.realFilePath, entry.uploaded, entry.size); Up().addStopped(entry.filePath, entry.uploaded, entry.size);
}); });
// this.setUploadings(Up().list()); // this.setUploadings(Up().list());
}; };
@ -63,13 +63,9 @@ export class Updater {
}; };
setUploadings = (infos: Map<string, UploadEntry>) => { setUploadings = (infos: Map<string, UploadEntry>) => {
this.props.browser.uploadings = List<UploadInfo>( this.props.browser.uploadings = List<UploadEntry>(
infos.valueSeq().map((v: UploadEntry): UploadInfo => { infos.valueSeq().map((entry: UploadEntry): UploadEntry => {
return { return entry;
realFilePath: v.filePath,
size: v.size,
uploaded: v.uploaded,
};
}) })
); );
}; };
@ -105,13 +101,40 @@ export class Updater {
}; };
refreshUploadings = async (): Promise<boolean> => { refreshUploadings = async (): Promise<boolean> => {
// this function get information from server and merge them with local information
// because some information (error) can only be detected from local
const luResp = await this.filesClient.listUploadings(); const luResp = await this.filesClient.listUploadings();
if (luResp.status !== 200) {
// TODO: i18n
console.error(luResp.data);
return false;
}
this.props.browser.uploadings = let remoteUploads = Map<string, UploadInfo>([]);
luResp.status === 200 luResp.data.uploadInfos.forEach((info: UploadInfo) => {
? List<UploadInfo>(luResp.data.uploadInfos) remoteUploads = remoteUploads.set(info.realFilePath, info);
: this.props.browser.uploadings; })
return luResp.status === 200;
let updatedUploads = List<UploadEntry>([]);
this.props.browser.uploadings.forEach((localInfo: UploadEntry) => {
const remoteInfo = remoteUploads.get(localInfo.filePath);
if (remoteInfo == null) {
// TODO: i18n
localInfo.err = "not found in server";
} else {
updatedUploads.push({
file: localInfo.file,
filePath: localInfo.filePath,
size: remoteInfo.size,
uploaded: remoteInfo.uploaded,
state: localInfo.state,
err: localInfo.err,
});
}
});
this.props.browser.uploadings = updatedUploads;
return true;
}; };
stopUploading = (filePath: string) => { stopUploading = (filePath: string) => {

View file

@ -6,7 +6,8 @@ import { UploadStatus, UploadState } from "./interface";
// TODO: move chunk copying to worker // TODO: move chunk copying to worker
const defaultChunkLen = 1024 * 1024 * 1; const defaultChunkLen = 1024 * 1024 * 1;
const speedDownRatio = 0.5; const speedDownRatio = 0.5;
const speedUpRatio = 1.05; const speedUpRatio = 1.1;
const speedLimit = 1024 * 1024 * 10; // 10MB
const createRetryLimit = 2; const createRetryLimit = 2;
const uploadRetryLimit = 1024; const uploadRetryLimit = 1024;
const backoffMax = 2000; const backoffMax = 2000;
@ -17,12 +18,11 @@ export interface ReaderResult {
} }
export class ChunkUploader { export class ChunkUploader {
private reader = new FileReader();
private client: IFilesClient = new FilesClient(""); private client: IFilesClient = new FilesClient("");
private chunkLen: number = defaultChunkLen; private chunkLen: number = defaultChunkLen;
constructor() {} constructor() { }
setClient = (client: IFilesClient) => { setClient = (client: IFilesClient) => {
this.client = client; this.client = client;
@ -70,6 +70,8 @@ export class ChunkUploader {
): Promise<UploadStatus> => { ): Promise<UploadStatus> => {
if (this.chunkLen === 0) { if (this.chunkLen === 0) {
this.chunkLen = 1; // reset it to 1B this.chunkLen = 1; // reset it to 1B
} else if (this.chunkLen > speedLimit) {
this.chunkLen = speedLimit;
} else if (uploaded > file.size) { } else if (uploaded > file.size) {
return { return {
filePath, filePath,
@ -79,13 +81,14 @@ export class ChunkUploader {
}; };
} }
const reader = new FileReader();
const readerPromise = new Promise<ReaderResult>( const readerPromise = new Promise<ReaderResult>(
(resolve: (result: ReaderResult) => void) => { (resolve: (result: ReaderResult) => void) => {
this.reader.onerror = (_: ProgressEvent<FileReader>) => { reader.onerror = (_: ProgressEvent<FileReader>) => {
resolve({ err: this.reader.error }); resolve({ err: reader.error });
}; };
this.reader.onloadend = (ev: ProgressEvent<FileReader>) => { reader.onloadend = (ev: ProgressEvent<FileReader>) => {
const dataURL = ev.target.result as string; // readAsDataURL const dataURL = ev.target.result as string; // readAsDataURL
const base64Chunk = dataURL.slice(dataURL.indexOf(",") + 1); const base64Chunk = dataURL.slice(dataURL.indexOf(",") + 1);
resolve({ chunk: base64Chunk }); resolve({ chunk: base64Chunk });
@ -93,12 +96,22 @@ export class ChunkUploader {
} }
); );
const chunkRightPos = for (let i = 0; i < 3; i++) {
uploaded + this.chunkLen > file.size try {
? file.size const chunkRightPos =
: uploaded + this.chunkLen; uploaded + this.chunkLen > file.size
const blob = file.slice(uploaded, chunkRightPos); ? file.size
this.reader.readAsDataURL(blob); : uploaded + this.chunkLen;
const blob = file.slice(uploaded, chunkRightPos);
reader.readAsDataURL(blob);
break;
} catch (e) {
// The object is already busy reading Blobs
console.error(e);
await this.backOff();
continue;
}
}
const result = await readerPromise; const result = await readerPromise;
if (result.err != null) { if (result.err != null) {
@ -140,17 +153,17 @@ export class ChunkUploader {
const uploadStatusResp = await this.client.uploadStatus(filePath); const uploadStatusResp = await this.client.uploadStatus(filePath);
return uploadStatusResp.status === 200 return uploadStatusResp.status === 200
? { ? {
filePath, filePath,
uploaded: uploadStatusResp.data.uploaded, uploaded: uploadStatusResp.data.uploaded,
state: UploadState.Ready, state: UploadState.Ready,
err: "", err: "",
} }
: { : {
filePath, filePath,
uploaded: uploaded, uploaded: uploaded,
state: UploadState.Error, state: UploadState.Error,
err: `failed to get upload status: ${uploadStatusResp.statusText}`, err: `failed to get upload status: ${uploadStatusResp.statusText}`,
}; };
} catch (e) { } catch (e) {
return { return {
filePath, filePath,