1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-07-12 06:29:23 +00:00
seahub/frontend/src/components/file-uploader/file-uploader.js
2019-01-08 11:26:20 +08:00

512 lines
16 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.

import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import Resumablejs from '@seafile/resumablejs';
import MD5 from 'MD5';
import { enableResumableFileUpload } from '../../utils/constants';
import { seafileAPI } from '../../utils/seafile-api';
import { Utils } from '../../utils/utils';
import UploadProgressDialog from './upload-progress-dialog';
import UploadRemindDialog from '../dialog/upload-remind-dialog';
import '../../css/file-uploader.css';
const propTypes = {
repoID: PropTypes.string.isRequired,
direntList: PropTypes.array.isRequired,
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,
path: PropTypes.string.isRequired,
onFileUploadSuccess: PropTypes.func.isRequired,
};
class FileUploader extends React.Component {
constructor(props) {
super(props);
this.state = {
uploadFileList: [],
totalProgress: 0,
isUploadProgressDialogShow: false,
isUploadRemindDialogShow: false,
currentResumableFile: null,
uploadBitrate: 0
};
this.notifiedFolders = [];
this.timestamp = null;
this.loaded = 0;
this.bitrateInterval = 500; // Interval in milliseconds to calculate the bitrate
}
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.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));
}
onChunkingComplete = (file) => {
if (file.relativePath !== file.fileName) {
return; // is upload a folder;
}
if (enableResumableFileUpload) {
let repoID = this.props.repoID;
seafileAPI.getFileUploadedBytes(repoID, this.props.path, 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 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) {
resumableFile.formData = {
parent_dir: path,
};
} else {
let relative_path = relativePath.slice(0, relativePath.lastIndexOf('/') + 1);
resumableFile.formData = {
parent_dir: path,
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);
this.resumable.upload();
}
} else {
this.setUploadFileList(this.resumable.files);
this.resumable.upload();
}
}
filesAddedComplete = (resumable, files) => {
// single file uploading can check repetition, because custom dialog conn't prevent program execution;
}
setUploadFileList = () => {
let uploadFileList = this.resumable.files.map(resumableFile => {
return this.buildCustomFileObj(resumableFile);
});
this.setState({
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});
}
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;
}
uploadBitrate = (loaded - this.loaded) * (1000 / timeDiff) * 8;
}
this.timestamp = now;
this.loaded = loaded;
uploadBitrate = Utils.formatBitRate(uploadBitrate);
return uploadBitrate;
}
onProgress = () => {
let progress = Math.round(this.resumable.progress() * 100);
let uploadBitrate = this.getBitrate();
this.setState({
totalProgress: progress,
uploadBitrate: uploadBitrate
});
}
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.resumableFile.uniqueIdentifier === resumableFile.uniqueIdentifier) {
item.resumableFile.fileName = message.name;
item.resumableFile.relativePath = relative_path + message.name;
}
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
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.resumableFile.uniqueIdentifier === resumableFile.uniqueIdentifier) {
item.resumableFile.fileName = message.name;
item.resumableFile.relativePath = message.name;
}
return item;
});
this.setState({uploadFileList: uploadFileList});
}
onFileError = (file) => {
}
onComplete = () => {
this.notifiedFolders = [];
}
onPause = () => {
}
onError = () => {
}
onFileRetry = () => {
//todos, cancel upload file, uploded again;
}
onBeforeCancel = () => {
//todos, giving a pop message ?
}
onCancel = () => {
}
setHeaders = (resumableFile, resumable) => {
if (resumableFile.formData.replace) {
return [];
}
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();
let repoID = this.props.repoID;
seafileAPI.getUploadLink(repoID, this.props.path).then(res => {
this.resumable.opts.target = res.data;
});
}
onFolderUpload = () => {
this.uploadInput.setAttribute('webkitdirectory', 'webkitdirectory');
this.uploadInput.click();
let repoID = this.props.repoID;
seafileAPI.getUploadLink(repoID, this.props.path).then(res => {
this.resumable.opts.target = res.data;
});
}
onDragStart = () => {
let repoID = this.props.repoID;
this.uploadInput.setAttribute('webkitdirectory', 'webkitdirectory');
seafileAPI.getUploadLink(repoID, this.props.path).then(res => {
this.resumable.opts.target = res.data;
});
}
onCloseUploadDialog = () => {
this.setState({isUploadProgressDialogShow: false, uploadFileList: []});
}
onUploadCancel = (uploadingItem) => {
let uploadFileList = this.state.uploadFileList.filter(item => {
if (item.uniqueIdentifier === uploadingItem.uniqueIdentifier) {
uploadingItem.resumableFile.cancel();
this.resumable.removeFile(uploadingItem.resumableFile.file);
} else {
return item;
}
});
let newUploaderFileList = uploadFileList.map(item => {
let progress = Math.round(item.resumableFile.progress() * 100);
item.progress = progress;
return item;
});
this.setState({uploadFileList: newUploaderFileList});
}
onCancelAllUploading = () => {
let uploadFileList = this.state.uploadFileList.filter(item => {
let resumableFile = item.resumableFile;
if (Math.round(resumableFile.progress() !== 1)) {
resumableFile.cancel();
this.resumable.removeFile(resumableFile.file);
} else {
return item;
}
});
this.setState({uploadFileList: uploadFileList});
}
onUploaderRetry = () => {
}
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.setState({isUploadRemindDialogShow: false});
this.setUploadFileList(this.resumable.files);
this.resumable.upload();
});
}
uploadFile = () => {
let resumableFile = this.resumable.files[this.resumable.files.length - 1];
let fileObject = this.buildCustomFileObj(resumableFile);
this.setState({
isUploadRemindDialogShow: false,
isUploadProgressDialogShow: true,
uploadFileList: [...this.state.uploadFileList, fileObject]
}, () => {
this.resumable.upload();
});
}
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={node => this.uploadInput = node} onClick={this.onClick}/>
</div>
</div>
{
this.state.isUploadRemindDialogShow &&
<UploadRemindDialog
currentResumableFile={this.state.currentResumableFile}
replaceRepetitionFile={this.replaceRepetitionFile}
uploadFile={this.uploadFile}
cancelFileUpload={this.cancelFileUpload}
/>
}
{
this.state.isUploadProgressDialogShow &&
<UploadProgressDialog
uploadFileList={this.state.uploadFileList}
totalProgress={this.state.totalProgress}
onCloseUploadDialog={this.onCloseUploadDialog}
onCancelAllUploading={this.onCancelAllUploading}
onUploadCancel={this.onUploadCancel}
uploadBitrate={this.state.uploadBitrate}
/>
}
</Fragment>
);
}
}
FileUploader.propTypes = propTypes;
export default FileUploader;