1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-08-04 00:30:57 +00:00
seahub/frontend/src/components/shared-link-file-uploader/file-uploader.js

701 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// the parent folder of this file is copied from components/file-uploader
// this file is modified according to the requirements of 'file upload' in 'shared dir link'
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 '../toast';
import '../../css/file-uploader.css';
const propTypes = {
token: PropTypes.string.isRequired,
repoID: PropTypes.string.isRequired,
//direntList: PropTypes.array.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,
dragAndDrop: PropTypes.bool.isRequired,
path: PropTypes.string.isRequired,
onFileUploadSuccess: PropTypes.func.isRequired
};
class FileUploader extends React.Component {
constructor(props) {
super(props);
this.state = {
retryFileList: [],
uploadFileList: [],
forbidUploadFileList: [],
totalProgress: 0,
isUploadProgressDialogShow: false,
isUploadRemindDialogShow: 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
window.onbeforeunload = this.onbeforeunload;
this.isUploadLinkLoaded = false;
}
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);
//Enable or Disable DragAnd Drop
if (this.props.dragAndDrop === true) {
this.resumable.enableDropOnDocument();
}
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('filesAddedComplete', this.filesAddedComplete.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('pause', this.onPause.bind(this));
this.resumable.on('fileRetry', this.onFileRetry.bind(this));
this.resumable.on('fileError', this.onFileError.bind(this));
this.resumable.on('error', this.onError.bind(this));
this.resumable.on('beforeCancel', this.onBeforeCancel.bind(this));
this.resumable.on('cancel', this.onCancel.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});
}
//get parent_dir relative_path
let path = this.props.path === '/' ? '/' : this.props.path + '/';
let fileName = resumableFile.fileName;
let relativePath = resumableFile.relativePath;
let isFile = fileName === relativePath;
//update formdata
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;
// 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);
let { token } = this.props;
seafileAPI.sharedLinkGetFileUploadUrl(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;
let { token } = this.props;
seafileAPI.sharedLinkGetFileUploadUrl(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
// 'upload folder' is not supported
/*
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;
*/
}
// replacing file is not allowed in shared link with 'can_upload' permission
/*
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); // this contance: no repetition file
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});
}
onPause = () => {
}
onError = (message) => {
// reset upload link loaded
this.isUploadLinkLoaded = false;
// After the error, the user can switch windows
Utils.registerGlobalVariable('uploader', 'totalProgress', 100);
}
onFileRetry = () => {
// todo, cancel upload file, uploded again;
}
onBeforeCancel = () => {
// todo, giving a pop message ?
}
onCancel = () => {
}
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) => {
let { token } = this.props;
seafileAPI.sharedLinkGetFileUploadUrl(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);
});
}
onUploadRetryAll = () => {
let { token } = this.props;
seafileAPI.sharedLinkGetFileUploadUrl(token).then(res => {
this.resumable.opts.target = res.data.upload_link + '?ret-json=1';
this.state.retryFileList.forEach(item => {
item.error = false;
this.retryUploadFile(item);
});
let uploadFileList = this.state.uploadFileList.slice(0);
this.setState({
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;
});
}
uploadFile = () => {
let resumableFile = this.resumable.files[this.resumable.files.length - 1];
let { token } = this.props;
seafileAPI.sharedLinkGetFileUploadUrl(token).then(res => {
this.resumable.opts.target = res.data.upload_link + '?ret-json=1';
this.setState({
isUploadRemindDialogShow: false,
isUploadProgressDialogShow: true,
uploadFileList: [...this.state.uploadFileList, resumableFile]
}, () => {
this.resumable.upload();
});
Utils.registerGlobalVariable('uploader', 'isUploadProgressDialogShow', true);
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
}
cancelFileUpload = () => {
this.resumable.files.pop(); //delete latest file
this.setState({isUploadRemindDialogShow: false});
}
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>
{this.state.isUploadProgressDialogShow &&
<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}
onUploadRetryAll={this.onUploadRetryAll}
/>
}
</Fragment>
);
}
}
FileUploader.propTypes = propTypes;
export default FileUploader;