mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-01 15:09:14 +00:00
[shared upload link] rewrote it with react (#4611)
* [shared upload link] rewrote it with react * modification
This commit is contained in:
@@ -210,6 +210,11 @@ module.exports = {
|
||||
require.resolve('react-dev-utils/webpackHotDevClient'),
|
||||
paths.appSrc + "/pages/search",
|
||||
],
|
||||
uploadLink: [
|
||||
require.resolve('./polyfills'),
|
||||
require.resolve('react-dev-utils/webpackHotDevClient'),
|
||||
paths.appSrc + "/pages/upload-link",
|
||||
]
|
||||
},
|
||||
|
||||
output: {
|
||||
|
@@ -90,6 +90,7 @@ module.exports = {
|
||||
orgAdmin: [require.resolve('./polyfills'), paths.appSrc + "/pages/org-admin"],
|
||||
sysAdmin: [require.resolve('./polyfills'), paths.appSrc + "/pages/sys-admin"],
|
||||
search: [require.resolve('./polyfills'), paths.appSrc + "/pages/search"],
|
||||
uploadLink: [require.resolve('./polyfills'), paths.appSrc + "/pages/upload-link"],
|
||||
},
|
||||
|
||||
output: {
|
||||
|
44
frontend/src/css/upload-link.css
Normal file
44
frontend/src/css/upload-link.css
Normal file
@@ -0,0 +1,44 @@
|
||||
body {
|
||||
overflow: hidden;
|
||||
}
|
||||
#wrapper {
|
||||
height: 100%;
|
||||
}
|
||||
.top-header {
|
||||
background: #f4f4f7;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
padding: .5rem 1rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#upload-link-panel {
|
||||
width: 928px;
|
||||
max-width: calc(100% - 20px);
|
||||
border: 1px solid #ddd;
|
||||
margin: 2em auto;
|
||||
}
|
||||
.shared-by .avatar {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
#upload-link-panel .warning-icon {
|
||||
color: #f25041;
|
||||
font-size: 48px;
|
||||
}
|
||||
#upload-link-panel .tip-list-item {
|
||||
list-style: decimal inside none;
|
||||
}
|
||||
#upload-link-drop-zone {
|
||||
background: rgba(240, 159, 63, 0.1);
|
||||
border: 2px dashed #f09f3f;
|
||||
border-radius: 4px;
|
||||
padding: 28px 0;
|
||||
}
|
||||
#upload-link-drop-zone .upload-icon {
|
||||
color: rgba(240, 159, 63, 0.8);
|
||||
font-size: 60px;
|
||||
line-height: 1;
|
||||
}
|
||||
.mh-2 {
|
||||
min-height: 2rem;
|
||||
}
|
635
frontend/src/pages/upload-link/file-uploader.js
Normal file
635
frontend/src/pages/upload-link/file-uploader.js
Normal file
@@ -0,0 +1,635 @@
|
||||
// This file is copied from frontend/src/components/file-uploader/file-uploader.js,
|
||||
// and modified according to the requirements of this page.
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Resumablejs from '@seafile/resumablejs';
|
||||
import MD5 from 'MD5';
|
||||
import { resumableUploadFileBlockSize, maxUploadFileSize, maxNumberOfFilesForFileupload } from '../../utils/constants';
|
||||
import { seafileAPI } from '../../utils/seafile-api';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import UploadProgressDialog from './upload-progress-dialog';
|
||||
import toaster from '../../components/toast';
|
||||
|
||||
import '../../css/file-uploader.css';
|
||||
|
||||
const propTypes = {
|
||||
dragAndDrop: PropTypes.bool.isRequired,
|
||||
token: PropTypes.string.isRequired,
|
||||
repoID: PropTypes.string.isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
|
||||
filetypes: PropTypes.array,
|
||||
chunkSize: PropTypes.number,
|
||||
withCredentials: PropTypes.bool,
|
||||
testMethod: PropTypes.string,
|
||||
testChunks: PropTypes.number,
|
||||
simultaneousUploads: PropTypes.number,
|
||||
fileParameterName: PropTypes.string,
|
||||
minFileSizeErrorCallback: PropTypes.func,
|
||||
fileTypeErrorCallback: PropTypes.func,
|
||||
onFileUploadSuccess: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
class FileUploader extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
retryFileList: [],
|
||||
uploadFileList: [],
|
||||
forbidUploadFileList: [],
|
||||
totalProgress: 0,
|
||||
isUploadProgressDialogShow: false,
|
||||
currentResumableFile: null,
|
||||
uploadBitrate: 0,
|
||||
allFilesUploaded: false,
|
||||
};
|
||||
|
||||
this.uploadInput = React.createRef();
|
||||
|
||||
this.notifiedFolders = [];
|
||||
|
||||
this.timestamp = null;
|
||||
this.loaded = 0;
|
||||
this.bitrateInterval = 500; // Interval in milliseconds to calculate the bitrate
|
||||
this.isUploadLinkLoaded = false;
|
||||
|
||||
window.onbeforeunload = this.onbeforeunload;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.resumable = new Resumablejs({
|
||||
target: '',
|
||||
query: this.setQuery || {},
|
||||
fileType: this.props.filetypes,
|
||||
maxFiles: maxNumberOfFilesForFileupload || undefined,
|
||||
maxFileSize: maxUploadFileSize * 1000 * 1000 || undefined,
|
||||
testMethod: this.props.testMethod || 'post',
|
||||
testChunks: this.props.testChunks || false,
|
||||
headers: this.setHeaders || {},
|
||||
withCredentials: this.props.withCredentials || false,
|
||||
chunkSize: parseInt(resumableUploadFileBlockSize) * 1024 * 1024 || 1 * 1024 * 1024,
|
||||
simultaneousUploads: this.props.simultaneousUploads || 1,
|
||||
fileParameterName: this.props.fileParameterName,
|
||||
generateUniqueIdentifier: this.generateUniqueIdentifier,
|
||||
forceChunkSize: true,
|
||||
maxChunkRetries: 3,
|
||||
minFileSize: 0,
|
||||
});
|
||||
|
||||
this.resumable.assignBrowse(this.uploadInput.current, true);
|
||||
if (this.props.dragAndDrop) {
|
||||
this.resumable.assignDrop(document.getElementById('upload-link-drop-zone'));
|
||||
}
|
||||
|
||||
this.bindCallbackHandler();
|
||||
this.bindEventHandler();
|
||||
}
|
||||
|
||||
componentWillUnmount = () => {
|
||||
window.onbeforeunload = null;
|
||||
if (this.props.dragAndDrop === true) {
|
||||
this.resumable.disableDropOnDocument();
|
||||
}
|
||||
}
|
||||
|
||||
onbeforeunload = () => {
|
||||
if (window.uploader &&
|
||||
window.uploader.isUploadProgressDialogShow &&
|
||||
window.uploader.totalProgress !== 100) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
bindCallbackHandler = () => {
|
||||
let { minFileSizeErrorCallback, fileTypeErrorCallback } = this.props;
|
||||
|
||||
if (this.maxFilesErrorCallback) {
|
||||
this.resumable.opts.maxFilesErrorCallback = this.maxFilesErrorCallback;
|
||||
}
|
||||
|
||||
if (minFileSizeErrorCallback) {
|
||||
this.resumable.opts.minFileSizeErrorCallback = this.props.minFileSizeErrorCallback;
|
||||
}
|
||||
|
||||
if (this.maxFileSizeErrorCallback) {
|
||||
this.resumable.opts.maxFileSizeErrorCallback = this.maxFileSizeErrorCallback;
|
||||
}
|
||||
|
||||
if (fileTypeErrorCallback) {
|
||||
this.resumable.opts.fileTypeErrorCallback = this.props.fileTypeErrorCallback;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bindEventHandler = () => {
|
||||
this.resumable.on('chunkingComplete', this.onChunkingComplete.bind(this));
|
||||
this.resumable.on('fileAdded', this.onFileAdded.bind(this));
|
||||
this.resumable.on('fileProgress', this.onFileProgress.bind(this));
|
||||
this.resumable.on('fileSuccess', this.onFileUploadSuccess.bind(this));
|
||||
this.resumable.on('progress', this.onProgress.bind(this));
|
||||
this.resumable.on('complete', this.onComplete.bind(this));
|
||||
this.resumable.on('fileError', this.onFileError.bind(this));
|
||||
this.resumable.on('error', this.onError.bind(this));
|
||||
this.resumable.on('dragstart', this.onDragStart.bind(this));
|
||||
}
|
||||
|
||||
maxFilesErrorCallback = (files, errorCount) => {
|
||||
let maxFiles = maxNumberOfFilesForFileupload;
|
||||
let message = gettext('Please upload no more than {maxFiles} files at a time.');
|
||||
message = message.replace('{maxFiles}', maxFiles);
|
||||
toaster.danger(message);
|
||||
}
|
||||
|
||||
maxFileSizeErrorCallback = (file) => {
|
||||
let { forbidUploadFileList } = this.state;
|
||||
forbidUploadFileList.push(file);
|
||||
this.setState({forbidUploadFileList: forbidUploadFileList});
|
||||
}
|
||||
|
||||
onChunkingComplete = (resumableFile) => {
|
||||
|
||||
let allFilesUploaded = this.state.allFilesUploaded;
|
||||
if (allFilesUploaded === true) {
|
||||
this.setState({allFilesUploaded: false});
|
||||
}
|
||||
|
||||
let path = this.props.path;
|
||||
let fileName = resumableFile.fileName;
|
||||
let relativePath = resumableFile.relativePath;
|
||||
let isFile = fileName === relativePath;
|
||||
|
||||
resumableFile.formData = {};
|
||||
if (isFile) { // upload file
|
||||
resumableFile.formData = {
|
||||
parent_dir: path,
|
||||
};
|
||||
} else { // upload folder
|
||||
let relative_path = relativePath.slice(0, relativePath.lastIndexOf('/') + 1);
|
||||
resumableFile.formData = {
|
||||
parent_dir: path,
|
||||
relative_path: relative_path
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
onFileAdded = (resumableFile, files) => {
|
||||
let isFile = resumableFile.fileName === resumableFile.relativePath;
|
||||
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);
|
||||
seafileAPI.sharedUploadLinkGetFileUploadUrl(this.props.token).then(res => {
|
||||
this.resumable.opts.target = res.data.upload_link + '?ret-json=1';
|
||||
this.resumableUpload(resumableFile);
|
||||
}).catch(error => {
|
||||
let errMessage = Utils.getErrorMsg(error);
|
||||
toaster.danger(errMessage);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.setUploadFileList(this.resumable.files);
|
||||
if (!this.isUploadLinkLoaded) {
|
||||
this.isUploadLinkLoaded = true;
|
||||
seafileAPI.sharedUploadLinkGetFileUploadUrl(this.props.token).then(res => {
|
||||
this.resumable.opts.target = res.data.upload_link + '?ret-json=1';
|
||||
this.resumable.upload();
|
||||
}).catch(error => {
|
||||
let errMessage = Utils.getErrorMsg(error);
|
||||
toaster.danger(errMessage);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resumableUpload = (resumableFile) => {
|
||||
let { repoID, path } = this.props;
|
||||
seafileAPI.getFileUploadedBytes(repoID, path, resumableFile.fileName).then(res => {
|
||||
let uploadedBytes = res.data.uploadedBytes;
|
||||
let blockSize = parseInt(resumableUploadFileBlockSize) * 1024 * 1024 || 1024 * 1024;
|
||||
let offset = Math.floor(uploadedBytes / blockSize);
|
||||
resumableFile.markChunksCompleted(offset);
|
||||
this.resumable.upload();
|
||||
}).catch(error => {
|
||||
let errMessage = Utils.getErrorMsg(error);
|
||||
toaster.danger(errMessage);
|
||||
});
|
||||
}
|
||||
|
||||
filesAddedComplete = (resumable, files) => {
|
||||
let { forbidUploadFileList } = this.state;
|
||||
if (forbidUploadFileList.length > 0 && files.length === 0) {
|
||||
this.setState({
|
||||
isUploadProgressDialogShow: true,
|
||||
totalProgress: 100
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setUploadFileList = () => {
|
||||
let uploadFileList = this.resumable.files;
|
||||
this.setState({
|
||||
uploadFileList: uploadFileList,
|
||||
isUploadProgressDialogShow: true,
|
||||
});
|
||||
Utils.registerGlobalVariable('uploader', 'isUploadProgressDialogShow', true);
|
||||
}
|
||||
|
||||
onFileProgress = (resumableFile) => {
|
||||
let uploadBitrate = this.getBitrate();
|
||||
let uploadFileList = this.state.uploadFileList.map(item => {
|
||||
if (item.uniqueIdentifier === resumableFile.uniqueIdentifier) {
|
||||
if (uploadBitrate) {
|
||||
let lastSize = (item.size - (item.size * item.progress())) * 8;
|
||||
let time = Math.floor(lastSize / uploadBitrate);
|
||||
item.remainingTime = time;
|
||||
}
|
||||
}
|
||||
return item;
|
||||
});
|
||||
|
||||
this.setState({
|
||||
uploadBitrate: uploadBitrate,
|
||||
uploadFileList: uploadFileList
|
||||
});
|
||||
}
|
||||
|
||||
getBitrate = () => {
|
||||
let loaded = 0;
|
||||
let uploadBitrate = 0;
|
||||
let now = new Date().getTime();
|
||||
|
||||
this.resumable.files.forEach(file => {
|
||||
loaded += file.progress() * file.size;
|
||||
});
|
||||
|
||||
if (this.timestamp) {
|
||||
let timeDiff = (now - this.timestamp);
|
||||
if (timeDiff < this.bitrateInterval) {
|
||||
return this.state.uploadBitrate;
|
||||
}
|
||||
|
||||
// 1. Cancel will produce loaded greater than this.loaded
|
||||
// 2. reset can make this.loaded to be 0
|
||||
if (loaded < this.loaded || this.loaded === 0) {
|
||||
this.loaded = loaded; //
|
||||
}
|
||||
|
||||
uploadBitrate = (loaded - this.loaded) * (1000 / timeDiff) * 8;
|
||||
}
|
||||
|
||||
this.timestamp = now;
|
||||
this.loaded = loaded;
|
||||
|
||||
return uploadBitrate;
|
||||
}
|
||||
|
||||
onProgress = () => {
|
||||
let progress = Math.round(this.resumable.progress() * 100);
|
||||
this.setState({totalProgress: progress});
|
||||
Utils.registerGlobalVariable('uploader', 'totalProgress', progress);
|
||||
}
|
||||
|
||||
onFileUploadSuccess = (resumableFile, message) => {
|
||||
let formData = resumableFile.formData;
|
||||
let currentTime = new Date().getTime()/1000;
|
||||
message = formData.replace ? message : JSON.parse(message)[0];
|
||||
if (formData.relative_path) { // upload folder
|
||||
let relative_path = formData.relative_path;
|
||||
let dir_name = relative_path.slice(0, relative_path.indexOf('/'));
|
||||
let dirent = {
|
||||
id: message.id,
|
||||
name: dir_name,
|
||||
type: 'dir',
|
||||
mtime: currentTime,
|
||||
};
|
||||
|
||||
// update folders cache
|
||||
let isExist = this.notifiedFolders.some(item => {return item.name === dirent.name;});
|
||||
if (!isExist) {
|
||||
this.notifiedFolders.push(dirent);
|
||||
this.props.onFileUploadSuccess(dirent);
|
||||
}
|
||||
|
||||
// update uploadFileList
|
||||
let uploadFileList = this.state.uploadFileList.map(item => {
|
||||
if (item.uniqueIdentifier === resumableFile.uniqueIdentifier) {
|
||||
item.newFileName = relative_path + message.name;
|
||||
item.isSaved = true;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
this.setState({uploadFileList: uploadFileList});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (formData.replace) { // upload file -- replace exist file
|
||||
let fileName = resumableFile.fileName;
|
||||
let dirent = {
|
||||
id: message,
|
||||
name: fileName,
|
||||
type: 'file',
|
||||
mtime: currentTime
|
||||
};
|
||||
this.props.onFileUploadSuccess(dirent); // this contance: just one file
|
||||
|
||||
let uploadFileList = this.state.uploadFileList.map(item => {
|
||||
if (item.uniqueIdentifier === resumableFile.uniqueIdentifier) {
|
||||
item.newFileName = fileName;
|
||||
item.isSaved = true;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
this.setState({uploadFileList: uploadFileList});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// upload file -- add files
|
||||
let dirent = {
|
||||
id: message.id,
|
||||
type: 'file',
|
||||
name: message.name,
|
||||
size: message.size,
|
||||
mtime: currentTime,
|
||||
};
|
||||
this.props.onFileUploadSuccess(dirent);
|
||||
|
||||
let uploadFileList = this.state.uploadFileList.map(item => {
|
||||
if (item.uniqueIdentifier === resumableFile.uniqueIdentifier) {
|
||||
item.newFileName = message.name;
|
||||
item.isSaved = true;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
this.setState({uploadFileList: uploadFileList});
|
||||
}
|
||||
|
||||
onFileError = (resumableFile, message) => {
|
||||
let error = '';
|
||||
if (!message) {
|
||||
error = gettext('Network error');
|
||||
} else {
|
||||
// eg: '{"error": "Internal error" \n }'
|
||||
let errorMessage = message.replace(/\n/g, '');
|
||||
errorMessage = JSON.parse(errorMessage);
|
||||
error = errorMessage.error;
|
||||
if (error === 'File locked by others.') {
|
||||
error = gettext('File is locked by others.');
|
||||
}
|
||||
if (error === 'Internal error.') {
|
||||
error = gettext('Internal Server Error');
|
||||
}
|
||||
}
|
||||
|
||||
let uploadFileList = this.state.uploadFileList.map(item => {
|
||||
if (item.uniqueIdentifier === resumableFile.uniqueIdentifier) {
|
||||
this.state.retryFileList.push(item);
|
||||
item.error = error;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
|
||||
this.loaded = 0; // reset loaded data;
|
||||
this.setState({
|
||||
retryFileList: this.state.retryFileList,
|
||||
uploadFileList: uploadFileList
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
onComplete = () => {
|
||||
this.notifiedFolders = [];
|
||||
// reset upload link loaded
|
||||
this.isUploadLinkLoaded = false;
|
||||
this.setState({allFilesUploaded: true});
|
||||
}
|
||||
|
||||
onError = (message) => {
|
||||
// reset upload link loaded
|
||||
this.isUploadLinkLoaded = false;
|
||||
// After the error, the user can switch windows
|
||||
Utils.registerGlobalVariable('uploader', 'totalProgress', 100);
|
||||
}
|
||||
|
||||
setHeaders = (resumableFile, resumable) => {
|
||||
let offset = resumable.offset;
|
||||
let chunkSize = resumable.getOpt('chunkSize');
|
||||
let fileSize = resumableFile.size === 0 ? 1 : 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.current.removeAttribute('webkitdirectory');
|
||||
this.uploadInput.current.click();
|
||||
}
|
||||
|
||||
onFolderUpload = () => {
|
||||
this.uploadInput.current.setAttribute('webkitdirectory', 'webkitdirectory');
|
||||
this.uploadInput.current.click();
|
||||
}
|
||||
|
||||
onDragStart = () => {
|
||||
this.uploadInput.current.setAttribute('webkitdirectory', 'webkitdirectory');
|
||||
}
|
||||
|
||||
onCloseUploadDialog = () => {
|
||||
this.loaded = 0;
|
||||
this.resumable.files = [];
|
||||
// reset upload link loaded
|
||||
this.isUploadLinkLoaded = false;
|
||||
this.setState({isUploadProgressDialogShow: false, uploadFileList: [], forbidUploadFileList: []});
|
||||
Utils.registerGlobalVariable('uploader', 'isUploadProgressDialogShow', false);
|
||||
}
|
||||
|
||||
onUploadCancel = (uploadingItem) => {
|
||||
|
||||
let uploadFileList = this.state.uploadFileList.filter(item => {
|
||||
if (item.uniqueIdentifier === uploadingItem.uniqueIdentifier) {
|
||||
item.cancel(); // execute cancel function will delete the file at the same time
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
if (!this.resumable.isUploading()) {
|
||||
this.setState({
|
||||
totalProgress: '100',
|
||||
allFilesUploaded: true,
|
||||
});
|
||||
this.loaded = 0;
|
||||
}
|
||||
|
||||
this.setState({uploadFileList: uploadFileList});
|
||||
}
|
||||
|
||||
onCancelAllUploading = () => {
|
||||
let uploadFileList = this.state.uploadFileList.filter(item => {
|
||||
if (Math.round(item.progress() !== 1)) {
|
||||
item.cancel();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
this.loaded = 0;
|
||||
|
||||
this.setState({
|
||||
allFilesUploaded: true,
|
||||
totalProgress: '100',
|
||||
uploadFileList: uploadFileList
|
||||
});
|
||||
// reset upload link loaded
|
||||
this.isUploadLinkLoaded = false;
|
||||
}
|
||||
|
||||
onUploadRetry = (resumableFile) => {
|
||||
seafileAPI.sharedUploadLinkGetFileUploadUrl(this.props.token).then(res => {
|
||||
this.resumable.opts.target = res.data.upload_link + '?ret-json=1';
|
||||
let retryFileList = this.state.retryFileList.filter(item => {
|
||||
return item.uniqueIdentifier !== resumableFile.uniqueIdentifier;
|
||||
});
|
||||
let uploadFileList = this.state.uploadFileList.map(item => {
|
||||
if (item.uniqueIdentifier === resumableFile.uniqueIdentifier) {
|
||||
item.error = null;
|
||||
this.retryUploadFile(item);
|
||||
}
|
||||
return item;
|
||||
});
|
||||
|
||||
this.setState({
|
||||
retryFileList: retryFileList,
|
||||
uploadFileList: uploadFileList
|
||||
});
|
||||
}).catch(error => {
|
||||
let errMessage = Utils.getErrorMsg(error);
|
||||
toaster.danger(errMessage);
|
||||
});
|
||||
}
|
||||
|
||||
retryUploadFile = (resumableFile) => {
|
||||
let { repoID, path } = this.props;
|
||||
let fileName = resumableFile.fileName;
|
||||
let isFile = resumableFile.fileName === resumableFile.relativePath;
|
||||
if (!isFile) {
|
||||
let relative_path = resumableFile.formData.relative_path;
|
||||
let prefix = path === '/' ? (path + relative_path) : (path + '/' + relative_path);
|
||||
fileName = prefix + fileName;
|
||||
}
|
||||
|
||||
resumableFile.bootstrap();
|
||||
var firedRetry = false;
|
||||
resumableFile.resumableObj.on('chunkingComplete', () => {
|
||||
if(!firedRetry) {
|
||||
seafileAPI.getFileUploadedBytes(repoID, path, fileName).then(res => {
|
||||
let uploadedBytes = res.data.uploadedBytes;
|
||||
let blockSize = parseInt(resumableUploadFileBlockSize) * 1024 * 1024 || 1024 * 1024;
|
||||
let offset = Math.floor(uploadedBytes / blockSize);
|
||||
resumableFile.markChunksCompleted(offset);
|
||||
|
||||
resumableFile.resumableObj.upload();
|
||||
|
||||
}).catch(error => {
|
||||
let errMessage = Utils.getErrorMsg(error);
|
||||
toaster.danger(errMessage);
|
||||
});
|
||||
}
|
||||
firedRetry = true;
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
replaceRepetitionFile = () => {
|
||||
let { repoID, path } = this.props;
|
||||
seafileAPI.getUpdateLink(repoID, path).then(res => {
|
||||
this.resumable.opts.target = res.data;
|
||||
|
||||
let resumableFile = this.resumable.files[this.resumable.files.length - 1];
|
||||
resumableFile.formData['replace'] = 1;
|
||||
resumableFile.formData['target_file'] = resumableFile.formData.parent_dir + resumableFile.fileName;
|
||||
this.setUploadFileList(this.resumable.files);
|
||||
this.resumable.upload();
|
||||
}).catch(error => {
|
||||
let errMessage = Utils.getErrorMsg(error);
|
||||
toaster.danger(errMessage);
|
||||
});
|
||||
}
|
||||
|
||||
cancelFileUpload = () => {
|
||||
this.resumable.files.pop(); //delete latest file;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="file-uploader-container">
|
||||
<div className="file-uploader">
|
||||
<input className="upload-input" type="file" ref={this.uploadInput} onClick={this.onClick} />
|
||||
</div>
|
||||
</div>
|
||||
<UploadProgressDialog
|
||||
retryFileList={this.state.retryFileList}
|
||||
uploadFileList={this.state.uploadFileList}
|
||||
forbidUploadFileList={this.state.forbidUploadFileList}
|
||||
totalProgress={this.state.totalProgress}
|
||||
uploadBitrate={this.state.uploadBitrate}
|
||||
allFilesUploaded={this.state.allFilesUploaded}
|
||||
onCloseUploadDialog={this.onCloseUploadDialog}
|
||||
onCancelAllUploading={this.onCancelAllUploading}
|
||||
onUploadCancel={this.onUploadCancel}
|
||||
onUploadRetry={this.onUploadRetry}
|
||||
onFileUpload={this.onFileUpload}
|
||||
onFolderUpload={this.onFolderUpload}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FileUploader.propTypes = propTypes;
|
||||
|
||||
export default FileUploader;
|
28
frontend/src/pages/upload-link/forbid-upload-list-item.js
Normal file
28
frontend/src/pages/upload-link/forbid-upload-list-item.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { gettext, maxUploadFileSize } from '../../utils/constants';
|
||||
|
||||
const propTypes = {
|
||||
file: PropTypes.object,
|
||||
};
|
||||
|
||||
class ForbidUploadListItem extends React.Component {
|
||||
|
||||
render() {
|
||||
let { file } = this.props;
|
||||
let msg = gettext('Please upload files less than {placeholder}M').replace('{placeholder}', maxUploadFileSize);
|
||||
return (
|
||||
<tr className="file-upload-item">
|
||||
<td className="upload-name">
|
||||
<div className="ellipsis">{file.name}</div>
|
||||
</td>
|
||||
|
||||
<td colSpan={3} className="error">{msg}</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ForbidUploadListItem.propTypes = propTypes;
|
||||
|
||||
export default ForbidUploadListItem;
|
72
frontend/src/pages/upload-link/index.js
Normal file
72
frontend/src/pages/upload-link/index.js
Normal file
@@ -0,0 +1,72 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import Logo from '../../components/logo';
|
||||
import Account from '../../components/common/account';
|
||||
import FileUploader from './file-uploader';
|
||||
|
||||
import '../../css/upload-link.css';
|
||||
|
||||
const loggedUser = window.app.pageOptions.username;
|
||||
const {
|
||||
dirName,
|
||||
sharedBy,
|
||||
noQuota,
|
||||
maxUploadFileSize,
|
||||
token,
|
||||
repoID,
|
||||
path
|
||||
} = window.uploadLink;
|
||||
|
||||
|
||||
class SharedUploadLink extends React.Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="h-100 d-flex flex-column">
|
||||
<div className="top-header d-flex justify-content-between">
|
||||
<Logo />
|
||||
{loggedUser && <Account />}
|
||||
</div>
|
||||
<div className="o-auto">
|
||||
<div className="py-4 px-6 mx-auto rounded" id="upload-link-panel">
|
||||
<h3 className="h5" dangerouslySetInnerHTML={{__html: gettext('Upload files to {folder_name_placeholder}')
|
||||
.replace('{folder_name_placeholder}', `<span class="op-target">${Utils.HTMLescape(dirName)}</span>`)}}></h3>
|
||||
<p className="small shared-by" dangerouslySetInnerHTML={{__html: `${gettext('shared by:')} ${sharedBy.avatar} ${sharedBy.name}`}}></p>
|
||||
{noQuota ? (
|
||||
<div className="py-6 text-center">
|
||||
<span className="sf3-font sf3-font-tips warning-icon"></span>
|
||||
<p>{gettext('The owner of this library has run out of space.')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<Fragment>
|
||||
<ol className="small text-gray">
|
||||
<li className="tip-list-item">{gettext('Folder upload is limited to Chrome, Firefox 50+, and Microsoft Edge.')}</li>
|
||||
{maxUploadFileSize && <li className="tip-list-item">{gettext('File size should be smaller than {max_size_placeholder}').replace('{max_size_placeholder}', maxUploadFileSize)}</li>}
|
||||
</ol>
|
||||
<div id="upload-link-drop-zone" className="text-center mt-2 mb-4">
|
||||
<span className="sf3-font sf3-font-upload upload-icon"></span>
|
||||
<p className="small text-gray mb-0">{gettext('Drag and drop files or folders here.')}</p>
|
||||
</div>
|
||||
<FileUploader
|
||||
ref={uploader => this.uploader = uploader}
|
||||
dragAndDrop={true}
|
||||
token={token}
|
||||
repoID={repoID}
|
||||
path={path}
|
||||
onFileUploadSuccess={() => {}}
|
||||
/>
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ReactDOM.render(
|
||||
<SharedUploadLink />,
|
||||
document.getElementById('wrapper')
|
||||
);
|
144
frontend/src/pages/upload-link/upload-list-item.js
Normal file
144
frontend/src/pages/upload-link/upload-list-item.js
Normal file
@@ -0,0 +1,144 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import { Utils } from '../../utils/utils';
|
||||
|
||||
const propTypes = {
|
||||
resumableFile: PropTypes.object.isRequired,
|
||||
onUploadCancel: PropTypes.func.isRequired,
|
||||
onUploadRetry: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const UPLOAD_UPLOADING = 'uploading';
|
||||
const UPLOAD_ERROR = 'error';
|
||||
const UPLOAD_ISSAVING = 'isSaving';
|
||||
const UPLOAD_UPLOADED = 'uploaded';
|
||||
|
||||
class UploadListItem extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
uploadState: UPLOAD_UPLOADING
|
||||
};
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
let { resumableFile } = nextProps;
|
||||
let uploadState = UPLOAD_UPLOADING;
|
||||
|
||||
if (resumableFile.error) {
|
||||
uploadState = UPLOAD_ERROR;
|
||||
} else {
|
||||
if (resumableFile.remainingTime === 0 && !resumableFile.isSaved) {
|
||||
uploadState = UPLOAD_ISSAVING;
|
||||
}
|
||||
|
||||
if (resumableFile.isSaved) {
|
||||
uploadState = UPLOAD_UPLOADED;
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({uploadState: uploadState});
|
||||
}
|
||||
|
||||
onUploadCancel = (e) => {
|
||||
e.preventDefault();
|
||||
this.props.onUploadCancel(this.props.resumableFile);
|
||||
}
|
||||
|
||||
onUploadRetry = (e) => {
|
||||
e.preventDefault();
|
||||
this.props.onUploadRetry(this.props.resumableFile);
|
||||
}
|
||||
|
||||
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 { resumableFile } = this.props;
|
||||
let progress = Math.round(resumableFile.progress() * 100);
|
||||
let error = resumableFile.error;
|
||||
|
||||
return (
|
||||
<tr className="file-upload-item">
|
||||
<td className="upload-name">
|
||||
<div className="ellipsis">{resumableFile.newFileName}</div>
|
||||
</td>
|
||||
<td>
|
||||
<span className="file-size">{this.formatFileSize(resumableFile.size)}</span>
|
||||
</td>
|
||||
<td className="upload-progress">
|
||||
{(this.state.uploadState === UPLOAD_UPLOADING || this.state.uploadState === UPLOAD_ISSAVING) &&
|
||||
<Fragment>
|
||||
{resumableFile.size >= (100 * 1000 * 1000) &&
|
||||
<Fragment>
|
||||
{resumableFile.isUploading() && (
|
||||
<div className="progress-container">
|
||||
<div className="progress">
|
||||
<div className="progress-bar" role="progressbar" style={{width: `${progress}%`}} aria-valuenow={progress} aria-valuemin="0" aria-valuemax="100"></div>
|
||||
</div>
|
||||
{(resumableFile.remainingTime === -1) && <div className="progress-text">{gettext('Preparing to upload...')}</div>}
|
||||
{(resumableFile.remainingTime > 0) && <div className="progress-text">{gettext('Remaining')}{' '}{Utils.formatTime(resumableFile.remainingTime)}</div>}
|
||||
{(resumableFile.remainingTime === 0) && <div className="progress-text">{gettext('Indexing...')}</div>}
|
||||
</div>
|
||||
)}
|
||||
{!resumableFile.isUploading() && (
|
||||
<div className="progress-container d-flex align-items-center">
|
||||
<div className="progress">
|
||||
<div className="progress-bar" role="progressbar" style={{width: `${progress}%`}} aria-valuenow={progress} aria-valuemin="0" aria-valuemax="100"></div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Fragment>
|
||||
}
|
||||
{(resumableFile.size < (100 * 1000 * 1000)) &&
|
||||
<div className="progress-container d-flex align-items-center">
|
||||
<div className="progress">
|
||||
<div className="progress-bar" role="progressbar" style={{width: `${progress}%`}} aria-valuenow={progress} aria-valuemin="0" aria-valuemax="100"></div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</Fragment>
|
||||
}
|
||||
{this.state.uploadState === UPLOAD_ERROR && (
|
||||
<div className="message err-message ml-0" dangerouslySetInnerHTML={{__html: error}}></div>
|
||||
)}
|
||||
</td>
|
||||
<td className="upload-operation">
|
||||
<Fragment>
|
||||
{this.state.uploadState === UPLOAD_UPLOADING && (
|
||||
<a href="#" onClick={this.onUploadCancel}>{gettext('Cancel')}</a>
|
||||
)}
|
||||
{this.state.uploadState === UPLOAD_ERROR && (
|
||||
<a href="#" onClick={this.onUploadRetry}>{gettext('Retry')}</a>
|
||||
)}
|
||||
{this.state.uploadState === UPLOAD_ISSAVING && (
|
||||
<span className="saving">{gettext('Saving...')}</span>
|
||||
)}
|
||||
{this.state.uploadState === UPLOAD_UPLOADED && (
|
||||
<span className="uploaded">{gettext('Uploaded')}</span>
|
||||
)}
|
||||
</Fragment>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
UploadListItem.propTypes = propTypes;
|
||||
|
||||
export default UploadListItem;
|
87
frontend/src/pages/upload-link/upload-progress-dialog.js
Normal file
87
frontend/src/pages/upload-link/upload-progress-dialog.js
Normal file
@@ -0,0 +1,87 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, ButtonDropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import UploadListItem from './upload-list-item';
|
||||
import ForbidUploadListItem from './forbid-upload-list-item';
|
||||
|
||||
const propTypes = {
|
||||
uploadFileList: PropTypes.array.isRequired,
|
||||
forbidUploadFileList: PropTypes.array.isRequired,
|
||||
onCancelAllUploading: PropTypes.func.isRequired,
|
||||
onUploadCancel: PropTypes.func.isRequired,
|
||||
onUploadRetry: PropTypes.func.isRequired,
|
||||
onFileUpload: PropTypes.func.isRequired,
|
||||
onFolderUpload: PropTypes.func.isRequired,
|
||||
allFilesUploaded: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
class UploadProgressDialog extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
dropdownOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
toggleDropdown = () => {
|
||||
this.setState({
|
||||
dropdownOpen: !this.state.dropdownOpen
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
let { allFilesUploaded } = this.props;
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="text-center">
|
||||
<ButtonDropdown isOpen={this.state.dropdownOpen} toggle={this.toggleDropdown}>
|
||||
<DropdownToggle color="primary" caret>{gettext('Upload')}</DropdownToggle>
|
||||
<DropdownMenu>
|
||||
<DropdownItem onClick={this.props.onFileUpload}>{gettext('Upload Files')}</DropdownItem>
|
||||
<DropdownItem onClick={this.props.onFolderUpload}>{gettext('Upload Folder')}</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</ButtonDropdown>
|
||||
<Button color="primary" outline={true} className="ml-4"
|
||||
onClick={this.props.onCancelAllUploading}
|
||||
disabled={allFilesUploaded}>
|
||||
{gettext('Cancel All')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-4 mh-2">
|
||||
<table className="table-thead-hidden">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="35%">{gettext('name')}</th>
|
||||
<th width="15%">{gettext('size')}</th>
|
||||
<th width="35%">{gettext('progress')}</th>
|
||||
<th width="15%">{gettext('state')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{this.props.forbidUploadFileList.map((file, index) => {
|
||||
return (<ForbidUploadListItem key={index} file={file} />);
|
||||
})}
|
||||
{this.props.uploadFileList.map((resumableFile, index) => {
|
||||
return (
|
||||
<UploadListItem
|
||||
key={index}
|
||||
resumableFile={resumableFile}
|
||||
onUploadCancel={this.props.onUploadCancel}
|
||||
onUploadRetry={this.props.onUploadRetry}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
UploadProgressDialog.propTypes = propTypes;
|
||||
|
||||
export default UploadProgressDialog;
|
Reference in New Issue
Block a user