mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-05 00:43:53 +00:00
Implement file upload (#2506)
This commit is contained in:
28
frontend/package-lock.json
generated
28
frontend/package-lock.json
generated
@@ -102,6 +102,11 @@
|
|||||||
"warning": "^3.0.0"
|
"warning": "^3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@seafile/resumablejs": {
|
||||||
|
"version": "1.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@seafile/resumablejs/-/resumablejs-1.1.6.tgz",
|
||||||
|
"integrity": "sha512-RTEJHxaEBHMC9bX2v04pfnI7YvFZhRxzilRpkhdJrNMUekVYFYW/C7N7g2DtoXXOrMoWin+okrVKpbp4xUICOA=="
|
||||||
|
},
|
||||||
"@seafile/seafile-editor": {
|
"@seafile/seafile-editor": {
|
||||||
"version": "0.1.35",
|
"version": "0.1.35",
|
||||||
"resolved": "https://registry.npmjs.org/@seafile/seafile-editor/-/seafile-editor-0.1.35.tgz",
|
"resolved": "https://registry.npmjs.org/@seafile/seafile-editor/-/seafile-editor-0.1.35.tgz",
|
||||||
@@ -174,7 +179,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"reactstrap": {
|
"reactstrap": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "http://registry.npmjs.org/reactstrap/-/reactstrap-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/reactstrap/-/reactstrap-5.0.0.tgz",
|
||||||
"integrity": "sha512-y0eju/LAK7gbEaTFfq2iW92MF7/5Qh0tc1LgYr2mg92IX8NodGc03a+I+cp7bJ0VXHAiLy0bFL9UP89oSm4cBg==",
|
"integrity": "sha512-y0eju/LAK7gbEaTFfq2iW92MF7/5Qh0tc1LgYr2mg92IX8NodGc03a+I+cp7bJ0VXHAiLy0bFL9UP89oSm4cBg==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"classnames": "^2.2.3",
|
"classnames": "^2.2.3",
|
||||||
@@ -253,6 +258,15 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.3.tgz",
|
||||||
"integrity": "sha512-sfGmOtSMSbQ/AKG8V9xD1gmjquC9awIIZ/Kj309pHb2n3bcRAcGMQv5nJ6gCXZVsneGE4+ve8DXKRCsrg3TFzg=="
|
"integrity": "sha512-sfGmOtSMSbQ/AKG8V9xD1gmjquC9awIIZ/Kj309pHb2n3bcRAcGMQv5nJ6gCXZVsneGE4+ve8DXKRCsrg3TFzg=="
|
||||||
},
|
},
|
||||||
|
"MD5": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/MD5/-/MD5-1.3.0.tgz",
|
||||||
|
"integrity": "sha1-PMJm8Oiau2tDpQ85pFnfW/3gskA=",
|
||||||
|
"requires": {
|
||||||
|
"charenc": ">= 0.0.1",
|
||||||
|
"crypt": ">= 0.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"abab": {
|
"abab": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/abab/-/abab-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/abab/-/abab-1.0.4.tgz",
|
||||||
@@ -617,7 +631,7 @@
|
|||||||
},
|
},
|
||||||
"axios": {
|
"axios": {
|
||||||
"version": "0.18.0",
|
"version": "0.18.0",
|
||||||
"resolved": "http://registry.npmjs.org/axios/-/axios-0.18.0.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-0.18.0.tgz",
|
||||||
"integrity": "sha1-MtU+SFHv3AoRmTts0AB4nXDAUQI=",
|
"integrity": "sha1-MtU+SFHv3AoRmTts0AB4nXDAUQI=",
|
||||||
"requires": {
|
"requires": {
|
||||||
"follow-redirects": "^1.3.0",
|
"follow-redirects": "^1.3.0",
|
||||||
@@ -2338,6 +2352,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz",
|
||||||
"integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I="
|
"integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I="
|
||||||
},
|
},
|
||||||
|
"charenc": {
|
||||||
|
"version": "0.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz",
|
||||||
|
"integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc="
|
||||||
|
},
|
||||||
"chokidar": {
|
"chokidar": {
|
||||||
"version": "1.7.0",
|
"version": "1.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz",
|
||||||
@@ -2792,6 +2811,11 @@
|
|||||||
"which": "^1.2.9"
|
"which": "^1.2.9"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"crypt": {
|
||||||
|
"version": "0.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
|
||||||
|
"integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs="
|
||||||
|
},
|
||||||
"crypto-browserify": {
|
"crypto-browserify": {
|
||||||
"version": "3.12.0",
|
"version": "3.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz",
|
"resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz",
|
||||||
|
@@ -4,7 +4,9 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@reach/router": "^1.2.0",
|
"@reach/router": "^1.2.0",
|
||||||
|
"@seafile/resumablejs": "^1.1.6",
|
||||||
"@seafile/seafile-editor": "^0.1.35",
|
"@seafile/seafile-editor": "^0.1.35",
|
||||||
|
"MD5": "^1.3.0",
|
||||||
"autoprefixer": "7.1.6",
|
"autoprefixer": "7.1.6",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
"css-loader": "0.28.7",
|
"css-loader": "0.28.7",
|
||||||
|
42
frontend/src/components/dialog/upload-remind-dialog.js
Normal file
42
frontend/src/components/dialog/upload-remind-dialog.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { gettext } from '../../utils/constants';
|
||||||
|
import { Modal, ModalHeader, ModalBody, ModalFooter, Button } from 'reactstrap';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
currentResumableFile: PropTypes.object.isRequired,
|
||||||
|
replaceRepetitionFile: PropTypes.func.isRequired,
|
||||||
|
uploadFile: PropTypes.func.isRequired,
|
||||||
|
cancelFileUpload: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
class UploadRemindDialog extends React.Component {
|
||||||
|
|
||||||
|
toggle = () => {
|
||||||
|
this.props.cancelFileUpload();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
|
||||||
|
let title = gettext('Replace file {filename}?');
|
||||||
|
title = title.replace('{filename}', '<span class="a-simaulte">' + this.props.currentResumableFile.fileName + '</span>');
|
||||||
|
return (
|
||||||
|
<Modal isOpen={true} toggle={this.toggle}>
|
||||||
|
<ModalHeader toggle={this.toggle} ><div dangerouslySetInnerHTML={{__html: title}}></div></ModalHeader>
|
||||||
|
<ModalBody>
|
||||||
|
<p>{gettext('A file with the same name already exists in this folder.')}</p>
|
||||||
|
<p>{gettext('Replacing it will overwrite its content.')}</p>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button outline color="primary" onClick={this.props.replaceRepetitionFile}>{gettext('Replace')}</Button>
|
||||||
|
<Button outline color="info" onClick={this.props.uploadFile}>{gettext("Don't Replace")}</Button>
|
||||||
|
<Button outline color="danger" onClick={this.toggle}>{gettext('Cancel')}</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UploadRemindDialog.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default UploadRemindDialog;
|
379
frontend/src/components/file-uploader/file-uploader.js
Normal file
379
frontend/src/components/file-uploader/file-uploader.js
Normal file
@@ -0,0 +1,379 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import Resumablejs from '@seafile/resumablejs';
|
||||||
|
import MD5 from 'MD5';
|
||||||
|
import { repoID, enableResumableFileUpload } from '../../utils/constants';
|
||||||
|
import { seafileAPI } from '../../utils/seafile-api';
|
||||||
|
import UploadProgressDialog from './upload-progress-dialog';
|
||||||
|
import UploadRemindDialog from '../dialog/upload-remind-dialog';
|
||||||
|
import '../../css/file-uploader.css';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
filetypes: PropTypes.array,
|
||||||
|
chunkSize: PropTypes.number,
|
||||||
|
withCredentials: PropTypes.bool,
|
||||||
|
maxFiles: PropTypes.number,
|
||||||
|
maxFileSize: PropTypes.number,
|
||||||
|
testMethod: PropTypes.string,
|
||||||
|
testChunks: PropTypes.number,
|
||||||
|
simultaneousUploads: PropTypes.number,
|
||||||
|
fileParameterName: PropTypes.string,
|
||||||
|
maxFilesErrorCallback: PropTypes.func,
|
||||||
|
maxFileSizeErrorCallback: PropTypes.func,
|
||||||
|
minFileSizeErrorCallback: PropTypes.func,
|
||||||
|
fileTypeErrorCallback: PropTypes.func,
|
||||||
|
dragAndDrop: PropTypes.bool.isRequired,
|
||||||
|
filePath: PropTypes.string.isRequired,
|
||||||
|
onFileSuccess: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
class FileUploader extends React.Component {
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
uploadFileList: [],
|
||||||
|
totalProgress: 0,
|
||||||
|
isUploadProgressDialogShow: false,
|
||||||
|
isUploadRemindDialogShow: false,
|
||||||
|
currentResumableFile: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.resumable = new Resumablejs({
|
||||||
|
target: '',
|
||||||
|
query: this.setQuery || {},
|
||||||
|
fileType: this.props.filetypes,
|
||||||
|
maxFiles: this.props.maxFiles,
|
||||||
|
maxFileSize: this.props.maxFileSize,
|
||||||
|
testMethod: this.props.testMethod || 'post',
|
||||||
|
testChunks: this.props.testChunks || false,
|
||||||
|
headers: this.setHeaders || {},
|
||||||
|
withCredentials: this.props.withCredentials || false,
|
||||||
|
chunkSize: this.props.chunkSize,
|
||||||
|
simultaneousUploads: this.props.simultaneousUploads || 1,
|
||||||
|
fileParameterName: this.props.fileParameterName,
|
||||||
|
generateUniqueIdentifier: this.generateUniqueIdentifier,
|
||||||
|
forceChunkSize: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.resumable.assignBrowse(this.uploadInput, true);
|
||||||
|
|
||||||
|
//Enable or Disable DragAnd Drop
|
||||||
|
if (this.props.dragAndDrop === true) {
|
||||||
|
this.resumable.enableDropOnDocument();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.bindCallbackHandler();
|
||||||
|
this.bindEventHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
bindCallbackHandler = () => {
|
||||||
|
let {maxFilesErrorCallback, minFileSizeErrorCallback, maxFileSizeErrorCallback, fileTypeErrorCallback } = this.props;
|
||||||
|
|
||||||
|
if (maxFilesErrorCallback) {
|
||||||
|
this.resumable.opts.maxFilesErrorCallback = this.props.maxFilesErrorCallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (minFileSizeErrorCallback) {
|
||||||
|
this.resumable.opts.minFileSizeErrorCallback = this.props.minFileSizeErrorCallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxFileSizeErrorCallback) {
|
||||||
|
this.resumable.opts.maxFileSizeErrorCallback = this.props.maxFileSizeErrorCallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileTypeErrorCallback) {
|
||||||
|
this.resumable.opts.fileTypeErrorCallback = this.props.fileTypeErrorCallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
bindEventHandler = () => {
|
||||||
|
this.resumable.on('chunkingComplete', this.onChunkingComplete);
|
||||||
|
this.resumable.on('fileAdded', this.onFileAdded);
|
||||||
|
this.resumable.on('filesAddedComplete', this.filesAddedComplete);
|
||||||
|
this.resumable.on('fileProgress', this.onFileProgress);
|
||||||
|
this.resumable.on('fileSuccess', this.onFileSuccess);
|
||||||
|
this.resumable.on('progress', this.onProgress);
|
||||||
|
this.resumable.on('complete', this.onComplete);
|
||||||
|
this.resumable.on('pause', this.onPause);
|
||||||
|
this.resumable.on('fileRetry', this.onFileRetry);
|
||||||
|
this.resumable.on('fileError', this.onFileError);
|
||||||
|
this.resumable.on('error', this.onError);
|
||||||
|
this.resumable.on('beforeCancel', this.onBeforeCancel);
|
||||||
|
this.resumable.on('cancel', this.onCancel);
|
||||||
|
this.resumable.on('dragstart', this.onDragStart);
|
||||||
|
}
|
||||||
|
|
||||||
|
onChunkingComplete = (file) => {
|
||||||
|
if (file.relativePath !== file.fileName) {
|
||||||
|
return; // is upload a folder;
|
||||||
|
}
|
||||||
|
if (enableResumableFileUpload) {
|
||||||
|
seafileAPI.getFileUploadedBytes(repoID, this.props.filePath, file.fileName).then(res => {
|
||||||
|
let uploadedBytes = res.data.uploadedBytes;
|
||||||
|
let offset = Math.floor(uploadedBytes / (1024 * 1024));
|
||||||
|
file.markChunksCompleted(offset);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onFileAdded = (resumableFile, files) => {
|
||||||
|
//get parent_dir、relative_path;
|
||||||
|
let filePath = this.props.filePath === '/' ? '/' : this.props.filePath + '/';
|
||||||
|
let fileName = resumableFile.fileName;
|
||||||
|
let relativePath = resumableFile.relativePath;
|
||||||
|
let isFile = fileName === relativePath;
|
||||||
|
|
||||||
|
//update formdata;
|
||||||
|
resumableFile.formData = {};
|
||||||
|
if (isFile) {
|
||||||
|
resumableFile.formData = {
|
||||||
|
parent_dir: filePath,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
let relative_path = relativePath.slice(0, relativePath.lastIndexOf('/') + 1);
|
||||||
|
resumableFile.formData = {
|
||||||
|
parent_dir: filePath,
|
||||||
|
relative_path: relative_path
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//check repetition
|
||||||
|
//uploading is file and only upload one file
|
||||||
|
if (isFile && files.length === 1) {
|
||||||
|
let hasRepetition = false;
|
||||||
|
let direntList = this.props.direntList;
|
||||||
|
for (let i = 0; i < direntList.length; i++) {
|
||||||
|
if (direntList[i].type === 'file' && direntList[i].name === resumableFile.fileName) {
|
||||||
|
hasRepetition = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hasRepetition) {
|
||||||
|
this.setState({
|
||||||
|
isUploadRemindDialogShow: true,
|
||||||
|
currentResumableFile: resumableFile,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.setUploadFileList(this.resumable.files);
|
||||||
|
resumableFile.upload();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.setUploadFileList(this.resumable.files);
|
||||||
|
resumableFile.upload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
filesAddedComplete = (resumable, files) => {
|
||||||
|
// single file uploading can check repetition, because custom dialog conn't prevent program execution;
|
||||||
|
}
|
||||||
|
|
||||||
|
setUploadFileList = (files) => {
|
||||||
|
let uploadFileList = files.map(resumableFile => {
|
||||||
|
return this.buildCustomFileObj(resumableFile);
|
||||||
|
});
|
||||||
|
this.setState({
|
||||||
|
isUploadRemindDialogShow: false,
|
||||||
|
isUploadProgressDialogShow: true,
|
||||||
|
uploadFileList: uploadFileList
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
buildCustomFileObj = (resumableFile) => {
|
||||||
|
return {
|
||||||
|
uniqueIdentifier: resumableFile.uniqueIdentifier,
|
||||||
|
resumableFile: resumableFile,
|
||||||
|
progress: resumableFile.progress(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
onFileProgress = (file) => {
|
||||||
|
let uniqueIdentifier = file.uniqueIdentifier;
|
||||||
|
let uploadFileList = this.state.uploadFileList.map(item => {
|
||||||
|
if (item.uniqueIdentifier === uniqueIdentifier) {
|
||||||
|
item.progress = Math.round(file.progress() * 100);
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setState({uploadFileList: uploadFileList});
|
||||||
|
}
|
||||||
|
|
||||||
|
onFileSuccess = (file) => {
|
||||||
|
// todos, update uploadList or updateList;
|
||||||
|
}
|
||||||
|
|
||||||
|
onFileError = (file) => {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
onProgress = () => {
|
||||||
|
let progress = Math.round(this.resumable.progress() * 100);
|
||||||
|
this.setState({totalProgress: progress});
|
||||||
|
}
|
||||||
|
|
||||||
|
onComplete = () => {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
onPause = () => [
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
onError = () => {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
onFileRetry = () => {
|
||||||
|
//todos, cancel upload file, uploded again;
|
||||||
|
}
|
||||||
|
|
||||||
|
onBeforeCancel = () => {
|
||||||
|
//todos, giving a pop message ?
|
||||||
|
}
|
||||||
|
|
||||||
|
onCancel = () => {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
setHeaders = (resumableFile, resumable) => {
|
||||||
|
let offset = resumable.offset;
|
||||||
|
let chunkSize = resumable.getOpt('chunkSize');
|
||||||
|
let fileSize = resumableFile.size;
|
||||||
|
let startByte = offset !== 0 ? offset * chunkSize : 0;
|
||||||
|
let endByte = Math.min(fileSize, (offset + 1) * chunkSize) - 1;
|
||||||
|
|
||||||
|
if (fileSize - resumable.endByte < chunkSize && !resumable.getOpt('forceChunkSize')) {
|
||||||
|
endByte = fileSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
let headers = {
|
||||||
|
'Accept': 'application/json; text/javascript, */*; q=0.01',
|
||||||
|
'Content-Disposition': 'attachment; filename="' + encodeURI(resumableFile.fileName) + '"',
|
||||||
|
'Content-Range': 'bytes ' + startByte + '-' + endByte + '/' + fileSize,
|
||||||
|
};
|
||||||
|
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
setQuery = (resumableFile) => {
|
||||||
|
let formData = resumableFile.formData;
|
||||||
|
return formData;
|
||||||
|
}
|
||||||
|
|
||||||
|
generateUniqueIdentifier = (file) => {
|
||||||
|
let relativePath = file.webkitRelativePath||file.relativePath||file.fileName||file.name;
|
||||||
|
return MD5(relativePath + new Date()) + relativePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
onClick = (e) => {
|
||||||
|
e.nativeEvent.stopImmediatePropagation();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
onFileUpload = () => {
|
||||||
|
this.uploadInput.removeAttribute('webkitdirectory');
|
||||||
|
this.uploadInput.click();
|
||||||
|
seafileAPI.getUploadLink(repoID, this.props.filePath).then(res => {
|
||||||
|
this.resumable.opts.target = res.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onFolderUpload = () => {
|
||||||
|
this.uploadInput.setAttribute('webkitdirectory', 'webkitdirectory');
|
||||||
|
this.uploadInput.click();
|
||||||
|
seafileAPI.getUploadLink(repoID, this.props.filePath).then(res => {
|
||||||
|
this.resumable.opts.target = res.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onDragStart = () => {
|
||||||
|
this.uploadInput.setAttribute('webkitdirectory', 'webkitdirectory');
|
||||||
|
seafileAPI.getUploadLink(repoID, this.props.filePath).then(res => {
|
||||||
|
this.resumable.opts.target = res.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onMinimizeUploadDialog = () => {
|
||||||
|
this.setState({isUploadProgressDialogShow: false});
|
||||||
|
}
|
||||||
|
|
||||||
|
onCloseUploadDialog = () => {
|
||||||
|
this.setState({isUploadProgressDialogShow: false, uploadFileList: []});
|
||||||
|
}
|
||||||
|
|
||||||
|
onUploadCancel = (resumableFile) => {
|
||||||
|
let uploadFileList = this.state.uploadFileList.filter(item => {
|
||||||
|
return item.uniqueIdentifier !== resumableFile.uniqueIdentifier;
|
||||||
|
});
|
||||||
|
let newUploaderFileList = uploadFileList.map(item => {
|
||||||
|
let progress = Math.round(item.resumableFile.progress() * 100);
|
||||||
|
item.progress = progress;
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
this.setState({uploadFileList: newUploaderFileList});
|
||||||
|
}
|
||||||
|
|
||||||
|
onUploaderRetry = () => {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
replaceRepetitionFile = () => {
|
||||||
|
let resumableFile = this.resumable.files[this.resumable.files.length - 1];
|
||||||
|
resumableFile.formData['replace'] = 1;
|
||||||
|
|
||||||
|
// this.setState({isUploadRemindDialogShow: false});
|
||||||
|
|
||||||
|
this.setUploadFileList(this.resumable.files);
|
||||||
|
|
||||||
|
this.resumable.upload();
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadFile = () => {
|
||||||
|
// this.setState({isUploadRemindDialogShow: false});
|
||||||
|
|
||||||
|
this.setUploadFileList(this.resumable.files);
|
||||||
|
this.resumable.upload();
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelFileUpload = () => {
|
||||||
|
this.resumable.files.pop(); //delete latest file;
|
||||||
|
this.setState({isUploadRemindDialogShow: false});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className="file-uploader-container">
|
||||||
|
<div className="file-uploader">
|
||||||
|
<input className="upload-input" type="file" ref={node => this.uploadInput = node} onClick={this.onClick}/>
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
this.state.isUploadProgressDialogShow &&
|
||||||
|
<UploadProgressDialog
|
||||||
|
uploadFileList={this.state.uploadFileList}
|
||||||
|
totalProgress={this.state.totalProgress}
|
||||||
|
onMinimizeUploadDialog={this.onMinimizeUploadDialog}
|
||||||
|
onCloseUploadDialog={this.onCloseUploadDialog}
|
||||||
|
onUploadCancel={this.onUploadCancel}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
this.state.isUploadRemindDialogShow &&
|
||||||
|
<UploadRemindDialog
|
||||||
|
currentResumableFile={this.state.currentResumableFile}
|
||||||
|
replaceRepetitionFile={this.replaceRepetitionFile}
|
||||||
|
uploadFile={this.uploadFile}
|
||||||
|
cancelFileUpload={this.cancelFileUpload}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileUploader.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default FileUploader;
|
58
frontend/src/components/file-uploader/upload-list-item.js
Normal file
58
frontend/src/components/file-uploader/upload-list-item.js
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { gettext } from '../../utils/constants';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
item: PropTypes.object.isRequired,
|
||||||
|
onUploadCancel: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
class UploadListItem extends React.Component {
|
||||||
|
|
||||||
|
onUploadCancel = () => {
|
||||||
|
let item = this.props.item;
|
||||||
|
item.resumableFile.cancel();
|
||||||
|
this.props.onUploadCancel(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
formatFileSize = (size) => {
|
||||||
|
if (typeof size !== 'number') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if (size >= 1000 * 1000 * 1000) {
|
||||||
|
return (size / (1000 * 1000 * 1000)).toFixed(1) + ' G';
|
||||||
|
}
|
||||||
|
if (size >= 1000 * 1000) {
|
||||||
|
return (size / (1000 * 1000)).toFixed(1) + ' M';
|
||||||
|
}
|
||||||
|
if (size >= 1000) {
|
||||||
|
return (size / 1000).toFixed(1) + ' K';
|
||||||
|
}
|
||||||
|
return size.toFixed(1) + ' B';
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let { item } = this.props;
|
||||||
|
let progress = Math.round(item.resumableFile.progress() * 100);
|
||||||
|
return (
|
||||||
|
<tr className="file-upload-item">
|
||||||
|
<td width="50%" className="upload-name ellipsis">{item.resumableFile.relativePath}</td>
|
||||||
|
<td width="30%" className="upload-progress upload-size">
|
||||||
|
{
|
||||||
|
progress === 100 ? this.formatFileSize(item.resumableFile.size) : progress + '%'
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td width="20%" className="upload-operation">
|
||||||
|
{ progress !== 100 ?
|
||||||
|
<span className="a-simulate" onClick={this.onUploadCancel}>{gettext(('cancel'))}</span> :
|
||||||
|
<span>{gettext('uploaded')}</span>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UploadListItem.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default UploadListItem;
|
@@ -0,0 +1,71 @@
|
|||||||
|
import React, { Fragment } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { gettext } from '../../utils/constants';
|
||||||
|
import UploadListItem from './upload-list-item';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
totalProgress: PropTypes.number.isRequired,
|
||||||
|
uploadFileList: PropTypes.array.isRequired,
|
||||||
|
onMinimizeUploadDialog: PropTypes.func.isRequired,
|
||||||
|
onCloseUploadDialog: PropTypes.func.isRequired,
|
||||||
|
onUploadCancel: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
class UploadProgressDialog extends React.Component {
|
||||||
|
|
||||||
|
onMinimizeUpload = (e) => {
|
||||||
|
e.nativeEvent.stopImmediatePropagation();
|
||||||
|
this.props.onMinimizeUploadDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
onCloseUpload = (e) => {
|
||||||
|
e.nativeEvent.stopImmediatePropagation();
|
||||||
|
this.props.onCloseUploadDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let uploadedMessage = gettext('File Upload');
|
||||||
|
let uploadingMessage = gettext('File is upload...') + this.props.totalProgress + '%';
|
||||||
|
|
||||||
|
let uploadingOptions = (<span className="sf2-icon-minus" onClick={this.onMinimizeUpload}></span>);
|
||||||
|
|
||||||
|
let uploadedOptions = (
|
||||||
|
<Fragment>
|
||||||
|
<span className="sf2-icon-minus" onClick={this.onMinimizeUpload}></span>
|
||||||
|
<span className="sf2-icon-x1" onClick={this.onCloseUpload}></span>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
|
||||||
|
let totalProgress = this.props.totalProgress;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="uploader-list-view">
|
||||||
|
<div className="uploader-list-header">
|
||||||
|
<div className="title">
|
||||||
|
{totalProgress === 100 ? uploadedMessage : uploadingMessage}
|
||||||
|
</div>
|
||||||
|
<div className="uploader-options">
|
||||||
|
{totalProgress === 100 ? uploadedOptions : uploadingOptions}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="uploader-list-content table-container">
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
{
|
||||||
|
this.props.uploadFileList.map((item, index) => {
|
||||||
|
return (
|
||||||
|
<UploadListItem key={index} item={item} onUploadCancel={this.props.onUploadCancel}/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UploadProgressDialog.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default UploadProgressDialog;
|
46
frontend/src/css/file-uploader.css
Normal file
46
frontend/src/css/file-uploader.css
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
.file-uploader-container {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-uploader {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 99999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploader-list-view {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: fixed;
|
||||||
|
right: 1px;
|
||||||
|
bottom: 1px;
|
||||||
|
width: 35rem;
|
||||||
|
min-height: 15rem;
|
||||||
|
max-height: 20rem;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 3px;
|
||||||
|
box-shadow: 0 0 6px #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploader-list-header {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
padding: 0.375rem 0.625rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #322;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
min-height: 2.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploader-list-header .uploader-options span{
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 0.25rem;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #b8b8b8;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploader-list-content {
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
13
frontend/src/css/react-resumable.css
Normal file
13
frontend/src/css/react-resumable.css
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
.sf-resumable-input-container {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-resumable-input-container .resumable-input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-resumable-input-container .input-placeholder {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
}
|
@@ -2,6 +2,7 @@ import React, { Component, Fragment } from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { gettext, repoID, serviceUrl, slug, siteRoot } from '../../utils/constants';
|
import { gettext, repoID, serviceUrl, slug, siteRoot } from '../../utils/constants';
|
||||||
import { seafileAPI } from '../../utils/seafile-api';
|
import { seafileAPI } from '../../utils/seafile-api';
|
||||||
|
import { Utils } from '../../utils/utils';
|
||||||
import Repo from '../../models/repo';
|
import Repo from '../../models/repo';
|
||||||
import Dirent from '../../models/dirent';
|
import Dirent from '../../models/dirent';
|
||||||
import CommonToolbar from '../../components/toolbar/common-toolbar';
|
import CommonToolbar from '../../components/toolbar/common-toolbar';
|
||||||
@@ -11,6 +12,7 @@ import DirentListView from '../../components/dirent-list-view/dirent-list-view';
|
|||||||
import DirentDetail from '../../components/dirent-detail/dirent-details';
|
import DirentDetail from '../../components/dirent-detail/dirent-details';
|
||||||
import CreateFolder from '../../components/dialog/create-folder-dialog';
|
import CreateFolder from '../../components/dialog/create-folder-dialog';
|
||||||
import CreateFile from '../../components/dialog/create-file-dialog';
|
import CreateFile from '../../components/dialog/create-file-dialog';
|
||||||
|
import FileUploader from '../../components/file-uploader/file-uploader';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
content: PropTypes.string,
|
content: PropTypes.string,
|
||||||
@@ -208,6 +210,20 @@ class MainPanel extends Component {
|
|||||||
this.updateViewList(this.props.filePath);
|
this.updateViewList(this.props.filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uploadFile = (e) => {
|
||||||
|
e.nativeEvent.stopImmediatePropagation();
|
||||||
|
this.uploader.onFileUpload();
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadFolder = (e) => {
|
||||||
|
e.nativeEvent.stopImmediatePropagation();
|
||||||
|
this.uploader.onFolderUpload();
|
||||||
|
}
|
||||||
|
|
||||||
|
onFileSuccess = (file) => {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let filePathList = this.props.filePath.split('/');
|
let filePathList = this.props.filePath.split('/');
|
||||||
let nodePath = '';
|
let nodePath = '';
|
||||||
@@ -249,17 +265,21 @@ class MainPanel extends Component {
|
|||||||
{
|
{
|
||||||
!this.props.isViewFileState &&
|
!this.props.isViewFileState &&
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<button className="btn btn-secondary operation-item" title={gettext('Edit File')} onClick={this.onUploadClick}>{gettext('Upload')}</button>
|
{
|
||||||
<button className="btn btn-secondary operation-item" title={gettext('Edit File')} onClick={this.onNewClick}>{gettext('New')}</button>
|
Utils.isSupportUploadFolder() ?
|
||||||
<button className="btn btn-secondary operation-item" title={gettext('Edit File')} onClick={this.onShareClick}>{gettext('Share')}</button>
|
<button className="btn btn-secondary operation-item" title={gettext('Upload')} onClick={this.onUploadClick}>{gettext('Upload')}</button> :
|
||||||
|
<button className="btn btn-secondary operation-item" title={gettext('Upload')} onClick={this.uploadFile}>{gettext('Upload')}</button>
|
||||||
|
}
|
||||||
|
<button className="btn btn-secondary operation-item" title={gettext('New')} onClick={this.onNewClick}>{gettext('New')}</button>
|
||||||
|
<button className="btn btn-secondary operation-item" title={gettext('Share')} onClick={this.onShareClick}>{gettext('Share')}</button>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
{
|
{
|
||||||
this.state.uploadMenuShow &&
|
this.state.uploadMenuShow &&
|
||||||
<ul className="menu dropdown-menu" style={this.state.operationMenuStyle}>
|
<ul className="menu dropdown-menu" style={this.state.operationMenuStyle}>
|
||||||
<li className="dropdown-item">{gettext('Upload Files')}</li>
|
<li className="dropdown-item" onClick={this.uploadFile}>{gettext('File Upload')}</li>
|
||||||
<li className="dropdown-item">{gettext('Upload Folder')}</li>
|
<li className="dropdown-item" onClick={this.uploadFolder}>{gettext('Folder Upload')}</li>
|
||||||
</ul>
|
</ul>
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
@@ -303,20 +323,29 @@ class MainPanel extends Component {
|
|||||||
onLinkClick={this.props.onLinkClick}
|
onLinkClick={this.props.onLinkClick}
|
||||||
isFileLoading={this.props.isFileLoading}
|
isFileLoading={this.props.isFileLoading}
|
||||||
/> :
|
/> :
|
||||||
<DirentListView
|
<Fragment>
|
||||||
direntList={this.state.direntList}
|
<DirentListView
|
||||||
filePath={this.props.filePath}
|
direntList={this.state.direntList}
|
||||||
onItemClick={this.props.onMainItemClick}
|
filePath={this.props.filePath}
|
||||||
onItemDelete={this.props.onMainItemDelete}
|
onItemClick={this.props.onMainItemClick}
|
||||||
onItemRename={this.props.onMainItemRename}
|
onItemDelete={this.props.onMainItemDelete}
|
||||||
onItemMove={this.props.onMainItemMove}
|
onItemRename={this.props.onMainItemRename}
|
||||||
onItemCopy={this.props.onMainItemCopy}
|
onItemMove={this.props.onMainItemMove}
|
||||||
onItemDetails={this.onItemDetails}
|
onItemCopy={this.props.onMainItemCopy}
|
||||||
updateViewList={this.updateViewList}
|
onItemDetails={this.onItemDetails}
|
||||||
isDirentListLoading={this.state.isDirentListLoading}
|
updateViewList={this.updateViewList}
|
||||||
currentRepo={this.state.currentRepo}
|
isDirentListLoading={this.state.isDirentListLoading}
|
||||||
isRepoOwner={this.state.isRepoOwner}
|
currentRepo={this.state.currentRepo}
|
||||||
/>
|
isRepoOwner={this.state.isRepoOwner}
|
||||||
|
/>
|
||||||
|
<FileUploader
|
||||||
|
ref={uploader => this.uploader = uploader}
|
||||||
|
dragAndDrop={true}
|
||||||
|
filePath={this.props.filePath}
|
||||||
|
onFileSuccess={this.onFileSuccess}
|
||||||
|
direntList={this.state.direntList}
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -19,6 +19,9 @@ export const canGenerateUploadLink = window.app.pageOptions.canGenerateUploadLin
|
|||||||
export const fileAuditEnabled = window.app.pageOptions.fileAuditEnabled ? true : false;
|
export const fileAuditEnabled = window.app.pageOptions.fileAuditEnabled ? true : false;
|
||||||
export const enableFileComment = window.app.pageOptions.enableFileComment ? true : false;
|
export const enableFileComment = window.app.pageOptions.enableFileComment ? true : false;
|
||||||
export const folderPermEnabled = window.app.pageOptions.folderPermEnabled === 'True';
|
export const folderPermEnabled = window.app.pageOptions.folderPermEnabled === 'True';
|
||||||
|
export const enableUploadFolder = window.app.pageOptions.enableUploadFolder === 'True';
|
||||||
|
export const enableResumableFileUpload = window.app.pageOptions.enableResumableFileUpload === 'True';
|
||||||
|
|
||||||
// wiki
|
// wiki
|
||||||
export const slug = window.wiki ? window.wiki.config.slug : '';
|
export const slug = window.wiki ? window.wiki.config.slug : '';
|
||||||
export const repoID = window.wiki ? window.wiki.config.repoId : '';
|
export const repoID = window.wiki ? window.wiki.config.repoId : '';
|
||||||
|
@@ -166,5 +166,10 @@ export const Utils = {
|
|||||||
getFileName: function(filePath) {
|
getFileName: function(filePath) {
|
||||||
let lastIndex = filePath.lastIndexOf('/');
|
let lastIndex = filePath.lastIndexOf('/');
|
||||||
return filePath.slice(lastIndex+1);
|
return filePath.slice(lastIndex+1);
|
||||||
|
},
|
||||||
|
|
||||||
|
isSupportUploadFolder: function() {
|
||||||
|
return navigator.userAgent.indexOf('Firefox')!=-1 ||
|
||||||
|
navigator.userAgent.indexOf('Chrome') > -1;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@@ -77,6 +77,7 @@
|
|||||||
.sf2-icon-confirm:before {content:"\e01e"}
|
.sf2-icon-confirm:before {content:"\e01e"}
|
||||||
.sf2-icon-cancel:before {content:"\e01f"}
|
.sf2-icon-cancel:before {content:"\e01f"}
|
||||||
.sf2-icon-tag:before {content:"\e037"}
|
.sf2-icon-tag:before {content:"\e037"}
|
||||||
|
.sf2-icon-minus:before {content:"\e01c"}
|
||||||
|
|
||||||
/* common class and element style*/
|
/* common class and element style*/
|
||||||
a { color:#eb8205; }
|
a { color:#eb8205; }
|
||||||
@@ -773,6 +774,7 @@ a.op-icon:focus {
|
|||||||
}
|
}
|
||||||
.table-container table {
|
.table-container table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
table-layout: fixed;
|
||||||
}
|
}
|
||||||
.table-container table th {
|
.table-container table th {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
@@ -117,6 +117,8 @@ def base(request):
|
|||||||
'show_logout_icon': SHOW_LOGOUT_ICON,
|
'show_logout_icon': SHOW_LOGOUT_ICON,
|
||||||
'is_pro': True if is_pro_version() else False,
|
'is_pro': True if is_pro_version() else False,
|
||||||
'enable_repo_wiki_mode': dj_settings.ENABLE_REPO_WIKI_MODE,
|
'enable_repo_wiki_mode': dj_settings.ENABLE_REPO_WIKI_MODE,
|
||||||
|
'enable_upload_folder': dj_settings.ENABLE_UPLOAD_FOLDER,
|
||||||
|
'enable_resumable_fileupload': dj_settings.ENABLE_RESUMABLE_FILEUPLOAD,
|
||||||
}
|
}
|
||||||
|
|
||||||
if request.user.is_staff:
|
if request.user.is_staff:
|
||||||
|
@@ -39,7 +39,9 @@
|
|||||||
canGenerateUploadLink: '{{ user.permissions.can_generate_upload_link }}',
|
canGenerateUploadLink: '{{ user.permissions.can_generate_upload_link }}',
|
||||||
fileAuditEnabled: '{{ file_audit_enabled }}',
|
fileAuditEnabled: '{{ file_audit_enabled }}',
|
||||||
enableFileComment: '{{ enable_file_comment }}',
|
enableFileComment: '{{ enable_file_comment }}',
|
||||||
folderPermEnabled: '{{ folder_perm_enabled }}'
|
folderPermEnabled: '{{ folder_perm_enabled }}',
|
||||||
|
enableUploadFolder: '{{ enable_upload_folder }}',
|
||||||
|
enableResumableFileUpload: '{{ enable_resumable_fileupload }}'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
Reference in New Issue
Block a user