mirror of
https://github.com/mickael-kerjean/filestash.git
synced 2025-11-01 10:56:31 +08:00
feature (upload): upload queue that show progress with abort and retry - #267
This commit is contained in:
1
client/assets/img/refresh.svg
Normal file
1
client/assets/img/refresh.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/><path d="M0 0h24v24H0z" fill="none"/></svg>
|
||||||
|
After Width: | Height: | Size: 340 B |
1
client/assets/img/stop.svg
Normal file
1
client/assets/img/stop.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M6 6h12v12H6z"/></svg>
|
||||||
|
After Width: | Height: | Size: 151 B |
@ -39,6 +39,8 @@ import img_info from '../assets/img/info.svg';
|
|||||||
import img_fullscreen from '../assets/img/fullscreen.svg';
|
import img_fullscreen from '../assets/img/fullscreen.svg';
|
||||||
import img_camera from '../assets/img/camera.svg';
|
import img_camera from '../assets/img/camera.svg';
|
||||||
import img_location from '../assets/img/location.svg';
|
import img_location from '../assets/img/location.svg';
|
||||||
|
import img_stop from '../assets/img/stop.svg';
|
||||||
|
import img_refresh from '../assets/img/refresh.svg';
|
||||||
export const img_placeholder = "/assets/icons/placeholder.png";
|
export const img_placeholder = "/assets/icons/placeholder.png";
|
||||||
|
|
||||||
export const Icon = (props) => {
|
export const Icon = (props) => {
|
||||||
@ -126,6 +128,10 @@ export const Icon = (props) => {
|
|||||||
img = img_camera;
|
img = img_camera;
|
||||||
}else if(props.name === 'location'){
|
}else if(props.name === 'location'){
|
||||||
img = img_location;
|
img = img_location;
|
||||||
|
} else if (props.name === 'stop') {
|
||||||
|
img = img_stop;
|
||||||
|
} else if (props.name === 'refresh') {
|
||||||
|
img = img_refresh;
|
||||||
}else{
|
}else{
|
||||||
throw('unknown icon: "'+props.name+"'");
|
throw('unknown icon: "'+props.name+"'");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,3 +21,4 @@ export { Dropdown, DropdownButton, DropdownList, DropdownItem } from './dropdown
|
|||||||
export { MapShot } from './mapshot';
|
export { MapShot } from './mapshot';
|
||||||
export { LoggedInOnly, ErrorPage, LoadingPage } from './decorator';
|
export { LoggedInOnly, ErrorPage, LoadingPage } from './decorator';
|
||||||
export { FormBuilder } from './formbuilder';
|
export { FormBuilder } from './formbuilder';
|
||||||
|
export { UploadQueue } from './upload_queue';
|
||||||
|
|||||||
381
client/components/upload_queue.js
Normal file
381
client/components/upload_queue.js
Normal file
@ -0,0 +1,381 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
99
client/components/upload_queue.scss
Normal file
99
client/components/upload_queue.scss
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
.component_stats{
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: 999;
|
||||||
|
max-width: 300px;
|
||||||
|
|
||||||
|
box-shadow: 1px 2px 20px rgba(0, 0, 0, 0.1);
|
||||||
|
background: white;
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0 0 5px 0;
|
||||||
|
font-size: 1.2em;
|
||||||
|
font-weight: 100;
|
||||||
|
.percent{color: var(--emphasis-primary);}
|
||||||
|
.count_block {
|
||||||
|
display: inline;
|
||||||
|
margin-left: 10px;
|
||||||
|
span.grandTotal{
|
||||||
|
font-size: 0.8em;
|
||||||
|
color: var(--emphasis-secondary);
|
||||||
|
&:before { content: "/"; }
|
||||||
|
}
|
||||||
|
span.completed{
|
||||||
|
color: var(--emphasis-secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.component_icon {
|
||||||
|
cursor: pointer;
|
||||||
|
margin-left: 10px;
|
||||||
|
width: 32px;
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h3 {
|
||||||
|
margin: 0 0 5px 0;
|
||||||
|
font-size: 1.0em;
|
||||||
|
font-weight: 100;
|
||||||
|
}
|
||||||
|
.stats_content {
|
||||||
|
clear: both;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
font-size: 0.85em;
|
||||||
|
|
||||||
|
.error_color{
|
||||||
|
color: var(--error);
|
||||||
|
}
|
||||||
|
.todo_color{
|
||||||
|
color: var(--light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file_row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.file_path {
|
||||||
|
padding: 5px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-basis: 100%;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.file_state {
|
||||||
|
padding: 5px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.file_control img {
|
||||||
|
display: block !important;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.file_control {
|
||||||
|
padding: 5px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
|
||||||
|
img {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -33,7 +33,7 @@ export function http_get(url, type = 'json'){
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function http_post(url, data, type = 'json'){
|
export function http_post(url, data, type = 'json', params){
|
||||||
return new Promise((done, err) => {
|
return new Promise((done, err) => {
|
||||||
var xhr = new XMLHttpRequest();
|
var xhr = new XMLHttpRequest();
|
||||||
xhr.open("POST", url, true);
|
xhr.open("POST", url, true);
|
||||||
@ -43,6 +43,9 @@ export function http_post(url, data, type = 'json'){
|
|||||||
data = JSON.stringify(data);
|
data = JSON.stringify(data);
|
||||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||||
}
|
}
|
||||||
|
if (params !== undefined && params.progress) {
|
||||||
|
xhr.upload.addEventListener("progress", params.progress, false);
|
||||||
|
}
|
||||||
xhr.send(data);
|
xhr.send(data);
|
||||||
xhr.onload = function () {
|
xhr.onload = function () {
|
||||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||||
@ -65,6 +68,12 @@ export function http_post(url, data, type = 'json'){
|
|||||||
xhr.onerror = function(){
|
xhr.onerror = function(){
|
||||||
handle_error_response(xhr, err)
|
handle_error_response(xhr, err)
|
||||||
}
|
}
|
||||||
|
if (params !== undefined && params.abort) {
|
||||||
|
params.abort(() => {
|
||||||
|
xhr.abort();
|
||||||
|
err({ message: 'aborted' });
|
||||||
|
})
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -14,3 +14,4 @@ export { leftPad, format, copyToClipboard } from './common';
|
|||||||
export { getMimeType } from './mimetype';
|
export { getMimeType } from './mimetype';
|
||||||
export { settings_get, settings_put } from './settings';
|
export { settings_get, settings_put } from './settings';
|
||||||
export { FormObjToJSON, createFormBackend, autocomplete } from './form';
|
export { FormObjToJSON, createFormBackend, autocomplete } from './form';
|
||||||
|
export { upload } from './upload';
|
||||||
|
|||||||
16
client/helpers/upload.js
Normal file
16
client/helpers/upload.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
const Upload = function () {
|
||||||
|
let fn = null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
add: function (path, files) {
|
||||||
|
if (!fn) { return window.setTimeout(() => this.add(path, files), 50); }
|
||||||
|
fn(path, files);
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
subscribe: function (_fn) {
|
||||||
|
fn = _fn;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const upload = new Upload();
|
||||||
@ -261,7 +261,7 @@ class FileSystem{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
touch(path, file, step){
|
touch(path, file, step, params){
|
||||||
const origin_path = pathBuilder(this.current_path, basename(path), 'file'),
|
const origin_path = pathBuilder(this.current_path, basename(path), 'file'),
|
||||||
destination_path = path;
|
destination_path = path;
|
||||||
|
|
||||||
@ -308,7 +308,7 @@ class FileSystem{
|
|||||||
const url = appendShareToUrl('/api/files/cat?path='+prepare(path));
|
const url = appendShareToUrl('/api/files/cat?path='+prepare(path));
|
||||||
let formData = new window.FormData();
|
let formData = new window.FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
return http_post(url, formData, 'multipart');
|
return http_post(url, formData, 'multipart', params);
|
||||||
}else{
|
}else{
|
||||||
const url = appendShareToUrl('/api/files/touch?path='+prepare(path));
|
const url = appendShareToUrl('/api/files/touch?path='+prepare(path));
|
||||||
return http_get(url);
|
return http_get(url);
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { Files } from '../model/';
|
import { Files } from '../model/';
|
||||||
import { notify, alert, currentShare } from '../helpers/';
|
import { notify, upload } from '../helpers/';
|
||||||
import Path from 'path';
|
import Path from 'path';
|
||||||
import { Observable } from "rxjs/Observable";
|
import { Observable } from "rxjs/Observable";
|
||||||
|
|
||||||
@ -120,9 +120,6 @@ export const onMultiRename = function(arrOfPath){
|
|||||||
* 3. user is coming from a upload form button as he doesn't have drag and drop with files
|
* 3. user is coming from a upload form button as he doesn't have drag and drop with files
|
||||||
*/
|
*/
|
||||||
export const onUpload = function(path, e){
|
export const onUpload = function(path, e){
|
||||||
const MAX_POOL_SIZE = 15;
|
|
||||||
let PRIOR_STATUS = {};
|
|
||||||
|
|
||||||
let extractFiles = null;
|
let extractFiles = null;
|
||||||
if(e.dataTransfer === undefined){ // case 3
|
if(e.dataTransfer === undefined){ // case 3
|
||||||
extractFiles = extract_upload_crappy_hack_but_official_way(e.target);
|
extractFiles = extract_upload_crappy_hack_but_official_way(e.target);
|
||||||
@ -141,165 +138,7 @@ export const onUpload = function(path, e){
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
extractFiles.then((files) => {
|
extractFiles.then((files) => upload.add(path, files));
|
||||||
var failed = [],
|
|
||||||
currents = [];
|
|
||||||
|
|
||||||
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')
|
|
||||||
};
|
|
||||||
}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')
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
class Stats extends React.Component {
|
|
||||||
constructor(props){
|
|
||||||
super(props);
|
|
||||||
this.state = {timeout: 1};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount(){
|
|
||||||
if(typeof this.state.timeout === "number"){
|
|
||||||
this.setState({
|
|
||||||
timeout: window.setTimeout(() => {
|
|
||||||
this.componentDidMount();
|
|
||||||
}, Math.random()*1000+200)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount(){
|
|
||||||
window.clearTimeout(this.state.timeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
emphasis(path){
|
|
||||||
notify.send(path.split("/").join(" / "), "info");
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const percent = Math.floor(100 * (files.length - processes.length - currents.length) / files.length);
|
|
||||||
return (
|
|
||||||
<div className="component_stats">
|
|
||||||
<h2>
|
|
||||||
UPLOADING <span className="percent">({percent}%)</span>
|
|
||||||
<div>
|
|
||||||
<span className="completed">{files.length - processes.length - currents.length}</span>
|
|
||||||
<span className="grandTotal">{files.length}</span>
|
|
||||||
</div>
|
|
||||||
</h2>
|
|
||||||
<div className="stats_content">
|
|
||||||
{
|
|
||||||
currents.slice(0, 1000).map((process, i) => {
|
|
||||||
return (
|
|
||||||
<div onClick={() => this.emphasis(process.path)} className="current_color" key={i}>{process.path.replace(/\//, '')}</div>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
{
|
|
||||||
processes.slice(0, 1000).map((process, i) => {
|
|
||||||
return (
|
|
||||||
<div onClick={() => this.emphasis(process.path)} className="todo_color" key={i}>{process.path.replace(/\//, '')}</div>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
{
|
|
||||||
failed.slice(0, 500).map((process, i) => {
|
|
||||||
return (
|
|
||||||
<div onClick={() => this.emphasis(process.path)} className="error_color" key={i}>{process.path}</div>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function runner(id){
|
|
||||||
let current_process = null;
|
|
||||||
if(processes.length === 0) 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
|
|
||||||
PRIOR_STATUS[processes[i].parent] === true
|
|
||||||
){
|
|
||||||
current_process = processes[i];
|
|
||||||
processes.splice(i, 1);
|
|
||||||
currents.push(current_process);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(current_process){
|
|
||||||
return current_process.fn(id)
|
|
||||||
.then(() => {
|
|
||||||
if(current_process.id) PRIOR_STATUS[current_process.id] = true;
|
|
||||||
currents = currents.filter((c) => c.path != current_process.path);
|
|
||||||
return runner(id);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
failed.push(current_process);
|
|
||||||
currents = currents.filter((c) => c.path != current_process.path);
|
|
||||||
notify.send(err, 'error');
|
|
||||||
return runner(id);
|
|
||||||
});
|
|
||||||
}else{
|
|
||||||
return waitABit()
|
|
||||||
.then(() => runner(id));
|
|
||||||
|
|
||||||
function waitABit(){
|
|
||||||
return new Promise((done) => {
|
|
||||||
window.setTimeout(() => {
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
}, 250);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(files.length >= 5){
|
|
||||||
alert.now(<Stats/>, () => {});
|
|
||||||
}
|
|
||||||
Promise.all(Array.apply(null, Array(MAX_POOL_SIZE)).map((process,index) => {
|
|
||||||
return runner();
|
|
||||||
})).then(() => {
|
|
||||||
// remove the popup
|
|
||||||
if(failed.length === 0){
|
|
||||||
var e = new Event("keydown");
|
|
||||||
e.keyCode = 27;
|
|
||||||
window.dispatchEvent(e);
|
|
||||||
}
|
|
||||||
currents = [];
|
|
||||||
// display message
|
|
||||||
window.setTimeout(() => {
|
|
||||||
notify.send('Upload completed', 'success');
|
|
||||||
}, 300);
|
|
||||||
}).catch((err) => {
|
|
||||||
currents = [];
|
|
||||||
notify.send(err, 'error');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// adapted from: https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript
|
// adapted from: https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript
|
||||||
function _rand_id(){
|
function _rand_id(){
|
||||||
|
|||||||
@ -42,43 +42,3 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.component_stats{
|
|
||||||
h2{
|
|
||||||
margin: 0 0 5px 0;
|
|
||||||
font-size: 1.2em;
|
|
||||||
font-weight: 100;
|
|
||||||
.percent{color: var(--emphasis-primary);}
|
|
||||||
> div{
|
|
||||||
float: right;
|
|
||||||
span.grandTotal{
|
|
||||||
font-size: 0.8em;
|
|
||||||
color: var(--emphasis-secondary);
|
|
||||||
&:before { content: "/"; }
|
|
||||||
}
|
|
||||||
span.completed{
|
|
||||||
color: var(--emphasis-secondary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.stats_content {
|
|
||||||
clear: both;
|
|
||||||
max-height: 150px;
|
|
||||||
overflow-y: auto;
|
|
||||||
overflow-x: hidden;
|
|
||||||
font-size: 0.85em;
|
|
||||||
div{
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
width: calc(100% - 10px);
|
|
||||||
-webkit-overflow-scrolling: touch;
|
|
||||||
}
|
|
||||||
.error_color{
|
|
||||||
color: var(--error);
|
|
||||||
}
|
|
||||||
.todo_color{
|
|
||||||
color: var(--light);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
import { BrowserRouter, Route, IndexRoute, Switch } from 'react-router-dom';
|
import { BrowserRouter, Route, IndexRoute, Switch } from 'react-router-dom';
|
||||||
import { NotFoundPage, ConnectPage, HomePage, SharePage, LogoutPage, FilesPage, ViewerPage } from './pages/';
|
import { NotFoundPage, ConnectPage, HomePage, SharePage, LogoutPage, FilesPage, ViewerPage } from './pages/';
|
||||||
import { URL_HOME, URL_FILES, URL_VIEWER, URL_LOGIN, URL_LOGOUT } from './helpers/';
|
import { URL_HOME, URL_FILES, URL_VIEWER, URL_LOGIN, URL_LOGOUT } from './helpers/';
|
||||||
import { Bundle, ModalPrompt, ModalAlert, ModalConfirm, Notification, Audio, Video } from './components/';
|
import { Bundle, ModalPrompt, ModalAlert, ModalConfirm, Notification, Audio, Video, UploadQueue } from './components/';
|
||||||
|
|
||||||
const AdminPage = (props) => (
|
const AdminPage = (props) => (
|
||||||
<Bundle loader={import(/* webpackChunkName: "admin" */"./pages/adminpage")} symbol="AdminPage">
|
<Bundle loader={import(/* webpackChunkName: "admin" */"./pages/adminpage")} symbol="AdminPage">
|
||||||
@ -27,7 +27,7 @@ export default class AppRouter extends React.Component {
|
|||||||
</Switch>
|
</Switch>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
<ModalPrompt /> <ModalAlert /> <ModalConfirm />
|
<ModalPrompt /> <ModalAlert /> <ModalConfirm />
|
||||||
<Notification />
|
<Notification /> <UploadQueue/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user