diff --git a/web/package.json b/web/package.json index 82952acb..de5e18bd 100644 --- a/web/package.json +++ b/web/package.json @@ -8,7 +8,7 @@ "license": "MIT", "scripts": { "build": "webpack --config webpack.config.js --mode production", - "upload-server": "DEBUG=upload-server:* nodemon -r @std/esm scripts/upload-server.mjs", + "upload-server": "DEBUG=upload-server:* nodemon --watch scripts -r @std/esm scripts/upload-server.mjs", "dev": "webpack-dev-server --config webpack.config.js --mode development", "lint": "eslint ./src --fix" }, diff --git a/web/src/file-upload/components/UploadReport/FileListItem.js b/web/src/file-upload/components/UploadReport/FileListItem.js index 8ec37b46..42dd3b54 100644 --- a/web/src/file-upload/components/UploadReport/FileListItem.js +++ b/web/src/file-upload/components/UploadReport/FileListItem.js @@ -95,6 +95,7 @@ FileListItem.propTypes = { file: PropTypes.shape({ path: PropTypes.string.isRequired, status: PropTypes.oneOf(["pending", "active", "success", "failure"]).isRequired, + progress: PropTypes.number, }).isRequired, onCancel: PropTypes.func.isRequired, }; diff --git a/web/src/file-upload/state/sagas.js b/web/src/file-upload/state/sagas.js index bdb53d2e..720f1aae 100644 --- a/web/src/file-upload/state/sagas.js +++ b/web/src/file-upload/state/sagas.js @@ -1,6 +1,8 @@ -import {all, call, cancelled, put, select, take, takeEvery} from "redux-saga/effects"; +import {all, call, cancel, cancelled, fork, put, select, take, takeEvery} from "redux-saga/effects"; import {END, eventChannel} from "redux-saga"; import { + ACTION_CANCEL_ALL, + ACTION_CANCEL_FILE, ACTION_UPDATE_STATUS, ACTION_UPLOAD, updateStatus, @@ -12,12 +14,14 @@ import { import Pandora from "../../server-api"; -function makeUploadChannel({pandora, file, cancel}) { +function makeUploadChannel({pandora, file}) { + const uploadingOperation = Pandora.makeCancellableOperation(); + return eventChannel(emit => { const onProgress = (percent) => { emit(uploadProgress(file, percent)); }; - pandora.uploadFile(file, {onProgress, cancel}).then(() => { + pandora.uploadFile(file, {onProgress, cancel: uploadingOperation}).then(() => { emit(uploadSuccess(file)); emit(END); }).catch((err) => { @@ -27,33 +31,72 @@ function makeUploadChannel({pandora, file, cancel}) { }); return () => { - cancel.cancel("cancelled"); + uploadingOperation.cancel("Cancelled by user"); }; }); } -export function* handleFileUploadSaga(file) { - const uploading = Pandora.makeCancellableOperation(); +export function* doUpload(file) { + yield put(updateStatus(file, UploadStatus.ACTIVE)); + const uploading = yield call(makeUploadChannel, {pandora: Pandora, file}); try { - yield put(updateStatus(file, UploadStatus.ACTIVE)); - const channel = yield call(makeUploadChannel, {pandora: Pandora, file, cancel: uploading}); - + // Receive actions indicating uploading progress + // and redirect them to the redux store... while (true) { - const action = yield take(channel); - console.log(action); + const action = yield take(uploading); yield put(action); } } catch (err) { + console.error(err); + } finally { if (yield cancelled()) { - uploading.cancel("cancelled"); + // If cancel received simply close the channel + // This will result in uploading cancellation. + uploading.close(); } - console.error(err); + } +} + +function isCancelled(action, file) { + return action.type === ACTION_CANCEL_ALL || + action.type === ACTION_CANCEL_FILE && + action.file.path === file.path; +} + +function isDone(action, file) { + return action.file.path === file.path && + action.type === ACTION_UPDATE_STATUS && + (action.file.status === UploadStatus.SUCCESS || + action.file.status === UploadStatus.FAILURE); +} + +export function* manageUpload(file) { + const task = yield fork(doUpload, file); + + // Monitor for cancellation until uploading complete. + while (true) { + // Receive uploading life-cycle action + const action = yield take([ + ACTION_CANCEL_ALL, + ACTION_CANCEL_FILE, + ACTION_UPDATE_STATUS + ]); + + // The operation is either cancelled or completed + if (isCancelled(action, file)) { + yield cancel(task); + return; + } else if (isDone(action, file)) { + return; + } + + // Skip if action is related to different file... } } -export function* handleUploadUpdateSaga() { +export function* handleUploadQueue() { try { const files = yield select(state => state.upload.files); for (let file of files) { @@ -62,7 +105,7 @@ export function* handleUploadUpdateSaga() { return; } if (file.status === UploadStatus.PENDING) { - yield all([handleFileUploadSaga(file)]); + yield all([manageUpload(file)]); return; } } @@ -73,8 +116,8 @@ export function* handleUploadUpdateSaga() { } export function* uploadRootSaga() { - yield takeEvery(ACTION_UPLOAD, handleUploadUpdateSaga); - yield takeEvery(ACTION_UPDATE_STATUS, handleUploadUpdateSaga); + yield takeEvery(ACTION_UPLOAD, handleUploadQueue); + yield takeEvery(ACTION_UPDATE_STATUS, handleUploadQueue); } export default uploadRootSaga; diff --git a/web/src/search-page/components/Dashboard/Dashboard.js b/web/src/search-page/components/Dashboard/Dashboard.js index 933f2654..6580368d 100644 --- a/web/src/search-page/components/Dashboard/Dashboard.js +++ b/web/src/search-page/components/Dashboard/Dashboard.js @@ -1,4 +1,4 @@ -import React, {useState} from "react"; +import React from "react"; import {makeStyles} from "@material-ui/styles"; import {AppBar, Container, Drawer, IconButton, Toolbar, Typography} from "@material-ui/core"; import MenuIcon from "@material-ui/icons/Menu";