1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-01 23:20:51 +00:00

Implement file upload (#2506)

This commit is contained in:
杨顺强
2018-11-14 10:55:11 +08:00
committed by Daniel Pan
parent dbbba4f93c
commit 360bd739ce
14 changed files with 701 additions and 23 deletions

View 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;

View 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;

View 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;

View File

@@ -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;

View 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;
}

View 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;
}

View File

@@ -2,6 +2,7 @@ import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import { gettext, repoID, serviceUrl, slug, siteRoot } from '../../utils/constants';
import { seafileAPI } from '../../utils/seafile-api';
import { Utils } from '../../utils/utils';
import Repo from '../../models/repo';
import Dirent from '../../models/dirent';
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 CreateFolder from '../../components/dialog/create-folder-dialog';
import CreateFile from '../../components/dialog/create-file-dialog';
import FileUploader from '../../components/file-uploader/file-uploader';
const propTypes = {
content: PropTypes.string,
@@ -208,6 +210,20 @@ class MainPanel extends Component {
this.updateViewList(this.props.filePath);
}
uploadFile = (e) => {
e.nativeEvent.stopImmediatePropagation();
this.uploader.onFileUpload();
}
uploadFolder = (e) => {
e.nativeEvent.stopImmediatePropagation();
this.uploader.onFolderUpload();
}
onFileSuccess = (file) => {
}
render() {
let filePathList = this.props.filePath.split('/');
let nodePath = '';
@@ -249,17 +265,21 @@ class MainPanel extends Component {
{
!this.props.isViewFileState &&
<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>
<button className="btn btn-secondary operation-item" title={gettext('Edit File')} onClick={this.onShareClick}>{gettext('Share')}</button>
{
Utils.isSupportUploadFolder() ?
<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>
}
</div>
{
{
this.state.uploadMenuShow &&
<ul className="menu dropdown-menu" style={this.state.operationMenuStyle}>
<li className="dropdown-item">{gettext('Upload Files')}</li>
<li className="dropdown-item">{gettext('Upload Folder')}</li>
<li className="dropdown-item" onClick={this.uploadFile}>{gettext('File Upload')}</li>
<li className="dropdown-item" onClick={this.uploadFolder}>{gettext('Folder Upload')}</li>
</ul>
}
{
@@ -303,20 +323,29 @@ class MainPanel extends Component {
onLinkClick={this.props.onLinkClick}
isFileLoading={this.props.isFileLoading}
/> :
<DirentListView
direntList={this.state.direntList}
filePath={this.props.filePath}
onItemClick={this.props.onMainItemClick}
onItemDelete={this.props.onMainItemDelete}
onItemRename={this.props.onMainItemRename}
onItemMove={this.props.onMainItemMove}
onItemCopy={this.props.onMainItemCopy}
onItemDetails={this.onItemDetails}
updateViewList={this.updateViewList}
isDirentListLoading={this.state.isDirentListLoading}
currentRepo={this.state.currentRepo}
isRepoOwner={this.state.isRepoOwner}
/>
<Fragment>
<DirentListView
direntList={this.state.direntList}
filePath={this.props.filePath}
onItemClick={this.props.onMainItemClick}
onItemDelete={this.props.onMainItemDelete}
onItemRename={this.props.onMainItemRename}
onItemMove={this.props.onMainItemMove}
onItemCopy={this.props.onMainItemCopy}
onItemDetails={this.onItemDetails}
updateViewList={this.updateViewList}
isDirentListLoading={this.state.isDirentListLoading}
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>

View File

@@ -19,6 +19,9 @@ export const canGenerateUploadLink = window.app.pageOptions.canGenerateUploadLin
export const fileAuditEnabled = window.app.pageOptions.fileAuditEnabled ? true : false;
export const enableFileComment = window.app.pageOptions.enableFileComment ? true : false;
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
export const slug = window.wiki ? window.wiki.config.slug : '';
export const repoID = window.wiki ? window.wiki.config.repoId : '';

View File

@@ -166,5 +166,10 @@ export const Utils = {
getFileName: function(filePath) {
let lastIndex = filePath.lastIndexOf('/');
return filePath.slice(lastIndex+1);
},
isSupportUploadFolder: function() {
return navigator.userAgent.indexOf('Firefox')!=-1 ||
navigator.userAgent.indexOf('Chrome') > -1;
}
};