mirror of
https://github.com/mickael-kerjean/filestash.git
synced 2025-11-01 10:56:31 +08:00
382 lines
12 KiB
JavaScript
382 lines
12 KiB
JavaScript
import React from 'react';
|
|
import Path from 'path';
|
|
|
|
import { Files } from '../model/';
|
|
import { confirm, notify, upload } from '../helpers/';
|
|
import { Icon, NgIf } from './';
|
|
import './upload_queue.scss';
|
|
|
|
const MAX_POOL_SIZE = 15;
|
|
|
|
function humanFileSize(bytes, si) {
|
|
var thresh = si ? 1000 : 1024;
|
|
if (Math.abs(bytes) < thresh) {
|
|
return bytes.toFixed(1) + ' B';
|
|
}
|
|
var units = si
|
|
? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
|
|
: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
|
|
var u = -1;
|
|
do {
|
|
bytes /= thresh;
|
|
++u;
|
|
} while (Math.abs(bytes) >= thresh && u < units.length - 1);
|
|
return bytes.toFixed(1) + ' ' + units[u];
|
|
}
|
|
|
|
export class UploadQueue extends React.Component {
|
|
constructor(props) {
|
|
super(props);
|
|
this.state = {
|
|
timeout: 1,
|
|
running: false,
|
|
files: [],
|
|
processes: [],
|
|
currents: [],
|
|
failed: [],
|
|
finished: [],
|
|
prior_status: {},
|
|
progress: {},
|
|
speed: [],
|
|
};
|
|
}
|
|
|
|
componentDidMount() {
|
|
if (typeof this.state.timeout === "number") {
|
|
this.setState({
|
|
timeout: window.setTimeout(() => {
|
|
this.componentDidMount();
|
|
}, Math.random() * 1000 + 200)
|
|
});
|
|
}
|
|
upload.subscribe((path, files) => this.addFiles(path, files));
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
window.clearTimeout(this.state.timeout);
|
|
}
|
|
|
|
reset() {
|
|
this.setState({
|
|
files: [],
|
|
processes: [],
|
|
currents: [],
|
|
failed: [],
|
|
finished: [],
|
|
prior_status: {},
|
|
progress: {},
|
|
speed: [],
|
|
});
|
|
}
|
|
|
|
emphasis(path) {
|
|
notify.send(path.split("/").join(" / "), "info");
|
|
}
|
|
|
|
runner(id) {
|
|
let current_process = null;
|
|
let processes = [...this.state.processes];
|
|
if (processes.length === 0 || !this.state.running) {
|
|
return Promise.resolve();
|
|
}
|
|
|
|
var i;
|
|
for (i = 0; i < processes.length; i++) {
|
|
if (
|
|
// init: getting started with creation of files/folders
|
|
processes[i].parent === null ||
|
|
// running: make sure we've created the parent folder
|
|
this.state.prior_status[processes[i].parent] === true
|
|
) {
|
|
current_process = this.state.processes[i];
|
|
processes.splice(i, 1);
|
|
this.setState({
|
|
processes,
|
|
currents: [...this.state.currents, current_process],
|
|
})
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (current_process) {
|
|
return current_process.fn(id)
|
|
.then(() => {
|
|
if (current_process.id) {
|
|
this.setState({
|
|
prior_status: {
|
|
...this.state.prior_status,
|
|
[current_process.id]: true
|
|
}
|
|
})
|
|
}
|
|
this.setState({
|
|
currents: this.state.currents.filter((c) => c.path != current_process.path),
|
|
finished: [...this.state.finished, current_process],
|
|
})
|
|
return this.runner(id);
|
|
})
|
|
.catch((err) => {
|
|
current_process.err = err;
|
|
this.setState({
|
|
failed: [...this.state.failed, current_process],
|
|
currents: this.state.currents.filter((c) => c.path != current_process.path),
|
|
});
|
|
let { message } = err;
|
|
if (message !== 'aborted') {
|
|
notify.send(err, 'error');
|
|
}
|
|
return this.runner(id);
|
|
});
|
|
} else {
|
|
function waitABit() {
|
|
return new Promise((done) => {
|
|
window.setTimeout(() => {
|
|
requestAnimationFrame(() => {
|
|
done();
|
|
});
|
|
}, 250);
|
|
});
|
|
}
|
|
return waitABit().then(() => this.runner(id));
|
|
}
|
|
}
|
|
|
|
updateProgress(path, e) {
|
|
if (e.lengthComputable) {
|
|
let prev = this.state.progress[path];
|
|
this.setState({
|
|
progress: {
|
|
...this.state.progress,
|
|
[path]: {
|
|
...prev,
|
|
percent: Math.round(100 * e.loaded / e.total),
|
|
loaded: e.loaded,
|
|
time: Date.now(),
|
|
prev: prev ? prev : null,
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
updateAbort(path, abort) {
|
|
this.setState({
|
|
progress: {
|
|
...this.state.progress,
|
|
[path]: {
|
|
...this.state.progress[path],
|
|
abort,
|
|
},
|
|
}
|
|
})
|
|
}
|
|
|
|
addFiles(path, files) {
|
|
const processes = files.map((file) => {
|
|
let original_path = file.path;
|
|
file.path = Path.join(path, file.path);
|
|
if (file.type === 'file') {
|
|
if (files.length < 150) Files.touch(file.path, file.file, 'prepare_only');
|
|
return {
|
|
path: original_path,
|
|
parent: file._prior || null,
|
|
fn: Files.touch.bind(
|
|
Files, file.path, file.file, 'execute_only',
|
|
{
|
|
progress: (e) => this.updateProgress(original_path, e),
|
|
abort: (x) => this.updateAbort(original_path, x),
|
|
}
|
|
)
|
|
};
|
|
} else {
|
|
Files.mkdir(file.path, 'prepare_only');
|
|
return {
|
|
id: file._id || null,
|
|
path: original_path,
|
|
parent: file._prior || null,
|
|
fn: Files.mkdir.bind(Files, file.path, 'execute_only')
|
|
};
|
|
}
|
|
});
|
|
|
|
this.setState({
|
|
processes: [...this.state.processes, ...processes],
|
|
files: [...this.state.files, ...files],
|
|
});
|
|
this.start();
|
|
}
|
|
|
|
retryFiles(process) {
|
|
this.setState({
|
|
processes: [...this.state.processes, process],
|
|
failed: this.state.failed.filter((c) => c.path != process.path),
|
|
});
|
|
window.setTimeout(() => this.start(), 300);
|
|
}
|
|
|
|
start() {
|
|
if (!this.state.running) {
|
|
window.setTimeout(() => this.calcSpeed(), 500);
|
|
this.setState({
|
|
running: true,
|
|
});
|
|
|
|
Promise.all(Array.apply(null, Array(MAX_POOL_SIZE)).map((process, index) => {
|
|
return this.runner();
|
|
})).then(() => {
|
|
window.setTimeout(() => {
|
|
notify.send('Upload completed', 'success');
|
|
}, 300);
|
|
this.setState({ running: false });
|
|
}).catch((err) => {
|
|
notify.send(err, 'error');
|
|
this.setState({ running: false });
|
|
});
|
|
}
|
|
}
|
|
|
|
abort(p) {
|
|
let info = this.state.progress[p.path];
|
|
if (info && info.abort) {
|
|
info.abort();
|
|
}
|
|
}
|
|
|
|
getCurrentPercent(path) {
|
|
let info = this.state.progress[path];
|
|
if (info && info.percent) {
|
|
return this.state.progress[path].percent + "%";
|
|
}
|
|
return "0%"
|
|
}
|
|
|
|
calcSpeed() {
|
|
let now = Date.now();
|
|
let curSpeed = [];
|
|
for (const [key, value] of Object.entries(this.state.progress)) {
|
|
if (value.prev && now - value.time < 5 * 1000) {
|
|
let bytes = value.loaded - value.prev.loaded;
|
|
let timeMs = value.time - value.prev.time;
|
|
curSpeed.push(1000 * bytes / timeMs);
|
|
}
|
|
}
|
|
let avgSpeed = curSpeed.reduce(function (p, c, i) { return p + (c - p) / (i + 1) }, 0);
|
|
this.setState({
|
|
speed: [...this.state.speed, avgSpeed].slice(-5),
|
|
});
|
|
if (this.state.running) {
|
|
window.setTimeout(() => this.calcSpeed(), 500);
|
|
}
|
|
}
|
|
|
|
getState() {
|
|
let avgSpeed = this.state.speed.reduce(function (p, c, i) { return p + (c - p) / (i + 1) }, 0);
|
|
let speedStr = ""
|
|
if (avgSpeed > 0) {
|
|
speedStr = " ~ " + humanFileSize(avgSpeed) + "/s";
|
|
}
|
|
if (this.state.running) {
|
|
return "Running..." + speedStr
|
|
}
|
|
return "Done" + speedStr
|
|
}
|
|
|
|
onClose() {
|
|
if(this.state.running) {
|
|
confirm.now(
|
|
"Abort current uploads?",
|
|
() => {
|
|
this.setState({
|
|
running: false,
|
|
});
|
|
this.state.currents.map(p => this.abort(p));
|
|
window.setTimeout(() => this.reset(), 30);
|
|
},
|
|
() => {}
|
|
);
|
|
} else {
|
|
this.reset()
|
|
}
|
|
}
|
|
|
|
renderRows(arr, state, col_state, action) {
|
|
let row_class = state + "_color";
|
|
return arr.slice(0, 1000).map((process, i) => {
|
|
return (
|
|
<div className={"file_row " + row_class} key={i}>
|
|
<div
|
|
className="file_path"
|
|
onClick={() => this.emphasis(process.path)}
|
|
>
|
|
{process.path.replace(/\//, '')}
|
|
</div>
|
|
{col_state(process)}
|
|
<div className="file_control">
|
|
{action ? action(process): (<span></span>)}
|
|
</div>
|
|
</div>
|
|
);
|
|
});
|
|
}
|
|
|
|
render() {
|
|
let { finished, files, processes, currents, failed } = this.state;
|
|
let totalFiles = files.length;
|
|
return (
|
|
<NgIf cond={totalFiles > 0}>
|
|
<div className="component_stats">
|
|
<h2>
|
|
CURRENT UPLOAD
|
|
<div className="count_block">
|
|
<span className="completed">{finished.length}</span>
|
|
<span className="grandTotal">{totalFiles}</span>
|
|
</div>
|
|
<Icon name="close" onClick={(e) => this.onClose()} />
|
|
</h2>
|
|
<h3>{this.getState()}</h3>
|
|
<div className="stats_content">
|
|
{this.renderRows(
|
|
finished,
|
|
"done",
|
|
(_) => (<div className="file_state file_state_done">Done</div>),
|
|
)}
|
|
{this.renderRows(
|
|
currents,
|
|
"current",
|
|
(p) => (
|
|
<div className="file_state file_state_current">
|
|
{this.getCurrentPercent(p.path)}
|
|
</div>
|
|
),
|
|
(p) => (
|
|
<Icon name="stop" onClick={(e) => this.abort(p)} ></Icon>
|
|
)
|
|
)}
|
|
{this.renderRows(
|
|
processes,
|
|
"todo",
|
|
(_) => (
|
|
<div className="file_state file_state_todo">Waiting</div>
|
|
)
|
|
)}
|
|
{this.renderRows(
|
|
failed,
|
|
"error",
|
|
(p) => (
|
|
(p.err && p.err.message == 'aborted')
|
|
?
|
|
<div className="file_state file_state_error">Aborted</div>
|
|
:
|
|
<div className="file_state file_state_error">Error</div>
|
|
),
|
|
(p) => (
|
|
<Icon name="refresh" onClick={(e) => this.retryFiles(p)} ></Icon>
|
|
)
|
|
)}
|
|
</div>
|
|
</div>
|
|
</NgIf>
|
|
);
|
|
}
|
|
}
|