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 UploadRemindDialog from '../dialog/upload-remind-dialog'; import toaster from '../toast'; import '../../css/file-uploader.css'; const propTypes = { 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, isCustomPermission: PropTypes.bool, }; class FileUploader extends React.Component { static defaultProps = { isCustomPermission: false }; 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) => { const { isCustomPermission } = this.props; let isFile = resumableFile.fileName === resumableFile.relativePath; // uploading is file and only upload one file if (isFile && files.length === 1) { let hasRepetition = false; if (!isCustomPermission) { 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 { repoID, path } = this.props; seafileAPI.getFileServerUploadLink(repoID, path).then(res => { this.resumable.opts.target = res.data + '?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 { repoID, path } = this.props; seafileAPI.getFileServerUploadLink(repoID, path).then(res => { this.resumable.opts.target = res.data + '?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); // 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}); }; getFileServerErrorMessage = (key) => { const errorMessage = { 'File locked by others.': gettext('File is locked by others.'), // 403 'Invalid filename.': gettext('Invalid filename.'), // 440 'File already exists.': gettext('File already exists.'), // 441 'File size is too large.': gettext('File size is too large.'), // 442 'Out of quota.': gettext('Out of quota.'), // 443 'Internal error.': gettext('Internal Server Error'), // 500 }; return errorMessage[key] || key; }; 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 = this.getFileServerErrorMessage(errorMessage.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) => { seafileAPI.getFileServerUploadLink(this.props.repoID, this.props.path).then(res => { this.resumable.opts.target = res.data + '?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 = () => { seafileAPI.getFileServerUploadLink(this.props.repoID, this.props.path).then(res => { this.resumable.opts.target = res.data + '?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; }); }; 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(); }).catch(error => { let errMessage = Utils.getErrorMsg(error); toaster.danger(errMessage); }); }; uploadFile = () => { let resumableFile = this.resumable.files[this.resumable.files.length - 1]; let { repoID, path } = this.props; seafileAPI.getFileServerUploadLink(repoID, path).then((res) => { // get upload link this.resumable.opts.target = res.data + '?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 (
{this.state.isUploadRemindDialogShow && } {this.state.isUploadProgressDialogShow && }
); } } FileUploader.propTypes = propTypes; export default FileUploader;