mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-03 16:10:26 +00:00
Share link file upload (#4633)
* [share dialog] fixed permission bugs; added 'download and upload' permission * share link: fixed permission bug for library * upload link: fixed permission bug * share link: added 'download and upload' permission for folder * [folder share link] added 'file upload' for folder share link with 'can_upload' permission
This commit is contained in:
@@ -83,8 +83,9 @@ class GenerateShareLink extends React.Component {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (isPro) {
|
if (isPro) {
|
||||||
if (this.props.itemType === 'library') {
|
const { itemType, userPerm } = this.props;
|
||||||
let permissionOptions = Utils.getShareLinkPermissionList(this.props.itemType, '', path);
|
if (itemType == 'library') {
|
||||||
|
let permissionOptions = Utils.getShareLinkPermissionList(itemType, userPerm, path);
|
||||||
this.setState({
|
this.setState({
|
||||||
permissionOptions: permissionOptions,
|
permissionOptions: permissionOptions,
|
||||||
currentPermission: permissionOptions[0],
|
currentPermission: permissionOptions[0],
|
||||||
|
@@ -54,7 +54,7 @@ class ShareDialog extends React.Component {
|
|||||||
getInitialActiveTab = () => {
|
getInitialActiveTab = () => {
|
||||||
let { repoEncrypted, userPerm, enableDirPrivateShare, itemType } = this.props;
|
let { repoEncrypted, userPerm, enableDirPrivateShare, itemType } = this.props;
|
||||||
const enableShareLink = !repoEncrypted && canGenerateShareLink;
|
const enableShareLink = !repoEncrypted && canGenerateShareLink;
|
||||||
const enableUploadLink = !repoEncrypted && canGenerateUploadLink && userPerm == 'rw';
|
const enableUploadLink = !repoEncrypted && canGenerateUploadLink && (userPerm == 'rw' || userPerm == 'admin');
|
||||||
|
|
||||||
// for encrypted repo, 'dir private share' is only enabled for the repo itself,
|
// for encrypted repo, 'dir private share' is only enabled for the repo itself,
|
||||||
// not for the folders in it.
|
// not for the folders in it.
|
||||||
@@ -88,7 +88,7 @@ class ShareDialog extends React.Component {
|
|||||||
let activeTab = this.state.activeTab;
|
let activeTab = this.state.activeTab;
|
||||||
let { repoEncrypted, userPerm, enableDirPrivateShare, itemType } = this.props;
|
let { repoEncrypted, userPerm, enableDirPrivateShare, itemType } = this.props;
|
||||||
const enableShareLink = !repoEncrypted && canGenerateShareLink;
|
const enableShareLink = !repoEncrypted && canGenerateShareLink;
|
||||||
const enableUploadLink = !repoEncrypted && canGenerateUploadLink && userPerm == 'rw';
|
const enableUploadLink = !repoEncrypted && canGenerateUploadLink && (userPerm == 'rw' || userPerm == 'admin');
|
||||||
|
|
||||||
// for encrypted repo, 'dir private share' is only enabled for the repo itself,
|
// for encrypted repo, 'dir private share' is only enabled for the repo itself,
|
||||||
// not for the folders in it.
|
// not for the folders in it.
|
||||||
@@ -160,6 +160,7 @@ class ShareDialog extends React.Component {
|
|||||||
repoID={this.props.repoID}
|
repoID={this.props.repoID}
|
||||||
closeShareDialog={this.props.toggleDialog}
|
closeShareDialog={this.props.toggleDialog}
|
||||||
itemType={itemType}
|
itemType={itemType}
|
||||||
|
userPerm={userPerm}
|
||||||
/>
|
/>
|
||||||
</TabPane>
|
</TabPane>
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,700 @@
|
|||||||
|
// 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;
|
@@ -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;
|
@@ -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;
|
@@ -0,0 +1,126 @@
|
|||||||
|
import React, { Fragment } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { gettext } from '../../utils/constants';
|
||||||
|
import { Utils } from '../../utils/utils';
|
||||||
|
import UploadListItem from './upload-list-item';
|
||||||
|
import ForbidUploadListItem from './forbid-upload-list-item';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
uploadBitrate: PropTypes.number.isRequired,
|
||||||
|
totalProgress: PropTypes.number.isRequired,
|
||||||
|
retryFileList: PropTypes.array.isRequired,
|
||||||
|
uploadFileList: PropTypes.array.isRequired,
|
||||||
|
forbidUploadFileList: PropTypes.array.isRequired,
|
||||||
|
onCloseUploadDialog: PropTypes.func.isRequired,
|
||||||
|
onCancelAllUploading: PropTypes.func.isRequired,
|
||||||
|
onUploadCancel: PropTypes.func.isRequired,
|
||||||
|
onUploadRetry: PropTypes.func.isRequired,
|
||||||
|
onUploadRetryAll: PropTypes.func.isRequired,
|
||||||
|
allFilesUploaded: PropTypes.bool.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
class UploadProgressDialog extends React.Component {
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
isMinimized: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
onCancelAllUploading = () => {
|
||||||
|
this.props.onCancelAllUploading();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMinimizeUpload = (e) => {
|
||||||
|
e.nativeEvent.stopImmediatePropagation();
|
||||||
|
this.setState({isMinimized: !this.state.isMinimized});
|
||||||
|
}
|
||||||
|
|
||||||
|
onCloseUpload = (e) => {
|
||||||
|
e.nativeEvent.stopImmediatePropagation();
|
||||||
|
this.props.onCloseUploadDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
|
||||||
|
let uploadBitrate = Utils.formatBitRate(this.props.uploadBitrate);
|
||||||
|
let uploadedMessage = gettext('File Upload');
|
||||||
|
let uploadingMessage = gettext('File Uploading...') + ' ' + this.props.totalProgress + '%' + ' (' + uploadBitrate + ')';
|
||||||
|
|
||||||
|
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, allFilesUploaded, retryFileList } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="uploader-list-view" style={{height: this.state.isMinimized ? '2.25rem' : '20rem'}}>
|
||||||
|
<div className="uploader-list-header">
|
||||||
|
<div className="title">
|
||||||
|
{totalProgress === 100 ? uploadedMessage : uploadingMessage}
|
||||||
|
</div>
|
||||||
|
<div className="uploader-options">
|
||||||
|
{totalProgress === 100 || allFilesUploaded ? uploadedOptions : uploadingOptions}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="uploader-list-content">
|
||||||
|
<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>
|
||||||
|
<tr>
|
||||||
|
<td className="text-right" colSpan={3}>
|
||||||
|
{retryFileList.length > 0 ?
|
||||||
|
<span className="cursor-pointer" onClick={this.props.onUploadRetryAll}>{gettext('Retry All')}</span>
|
||||||
|
:
|
||||||
|
<span className="cursor-pointer disabled-link">{gettext('Retry All')}</span>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td className="text-right" colSpan={1}>
|
||||||
|
{!allFilesUploaded ?
|
||||||
|
<span className="cursor-pointer" onClick={this.onCancelAllUploading}>{gettext('Cancel All')}</span>
|
||||||
|
:
|
||||||
|
<span className="cursor-pointer disabled-link" >{gettext('Cancel All')}</span>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{
|
||||||
|
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>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UploadProgressDialog.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default UploadProgressDialog;
|
@@ -11,10 +11,6 @@ body {
|
|||||||
height: 53px;
|
height: 53px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.title {
|
|
||||||
font-size: 1.4rem;
|
|
||||||
margin-bottom: .5rem;
|
|
||||||
}
|
|
||||||
.shared-dir-view-main {
|
.shared-dir-view-main {
|
||||||
width: calc(100% - 40px);
|
width: calc(100% - 40px);
|
||||||
max-width: 950px;
|
max-width: 950px;
|
||||||
@@ -42,11 +38,14 @@ body {
|
|||||||
background-color: #ccc;
|
background-color: #ccc;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
.zip-btn {
|
.shared-dir-op-btn {
|
||||||
height: 30px;
|
height: 30px;
|
||||||
line-height: 30px;
|
line-height: 30px;
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
}
|
}
|
||||||
|
.shared-dir-upload-btn {
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
}
|
||||||
.grid-item .action-icon {
|
.grid-item .action-icon {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 10px;
|
top: 10px;
|
||||||
|
@@ -3,7 +3,7 @@ import ReactDOM from 'react-dom';
|
|||||||
import { Button, Dropdown, DropdownToggle, DropdownItem } from 'reactstrap';
|
import { Button, Dropdown, DropdownToggle, DropdownItem } from 'reactstrap';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import Account from './components/common/account';
|
import Account from './components/common/account';
|
||||||
import { gettext, siteRoot, mediaUrl, logoPath, logoWidth, logoHeight, siteTitle, thumbnailSizeForOriginal } from './utils/constants';
|
import { isPro, gettext, siteRoot, mediaUrl, logoPath, logoWidth, logoHeight, siteTitle, thumbnailSizeForOriginal } from './utils/constants';
|
||||||
import { Utils } from './utils/utils';
|
import { Utils } from './utils/utils';
|
||||||
import { seafileAPI } from './utils/seafile-api';
|
import { seafileAPI } from './utils/seafile-api';
|
||||||
import Loading from './components/loading';
|
import Loading from './components/loading';
|
||||||
@@ -11,6 +11,7 @@ import toaster from './components/toast';
|
|||||||
import ModalPortal from './components/modal-portal';
|
import ModalPortal from './components/modal-portal';
|
||||||
import ZipDownloadDialog from './components/dialog/zip-download-dialog';
|
import ZipDownloadDialog from './components/dialog/zip-download-dialog';
|
||||||
import ImageDialog from './components/dialog/image-dialog';
|
import ImageDialog from './components/dialog/image-dialog';
|
||||||
|
import FileUploader from './components/shared-link-file-uploader/file-uploader';
|
||||||
|
|
||||||
import './css/shared-dir-view.css';
|
import './css/shared-dir-view.css';
|
||||||
import './css/grid-view.css';
|
import './css/grid-view.css';
|
||||||
@@ -18,7 +19,13 @@ import './css/grid-view.css';
|
|||||||
moment.locale(window.app.config.lang);
|
moment.locale(window.app.config.lang);
|
||||||
|
|
||||||
let loginUser = window.app.pageOptions.name;
|
let loginUser = window.app.pageOptions.name;
|
||||||
const { token, trafficOverLimit, dirName, sharedBy, path, canDownload, mode, thumbnailSize, zipped } = window.shared.pageOptions;
|
const {
|
||||||
|
token, dirName, sharedBy,
|
||||||
|
repoID, path,
|
||||||
|
mode, thumbnailSize, zipped,
|
||||||
|
trafficOverLimit, canDownload,
|
||||||
|
noQuota, canUpload
|
||||||
|
} = window.shared.pageOptions;
|
||||||
|
|
||||||
const showDownloadIcon = !trafficOverLimit && canDownload;
|
const showDownloadIcon = !trafficOverLimit && canDownload;
|
||||||
|
|
||||||
@@ -235,6 +242,28 @@ class SharedDirView extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onUploadFile = (e) => {
|
||||||
|
e.nativeEvent.stopImmediatePropagation();
|
||||||
|
this.uploader.onFileUpload();
|
||||||
|
}
|
||||||
|
|
||||||
|
onFileUploadSuccess = (direntObject) => {
|
||||||
|
const { name, size } = direntObject;
|
||||||
|
const newItem = {
|
||||||
|
isSelected: false,
|
||||||
|
file_name: name,
|
||||||
|
file_path: Utils.joinPath(path, name),
|
||||||
|
is_dir: false,
|
||||||
|
last_modified: moment().format(),
|
||||||
|
size: size
|
||||||
|
};
|
||||||
|
const folderItems = this.state.items.filter(item => { return item.is_dir; });
|
||||||
|
// put the new file as the first file
|
||||||
|
let items = Array.from(this.state.items);
|
||||||
|
items.splice(folderItems.length, 0, newItem);
|
||||||
|
this.setState({items: items});
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const isDesktop = Utils.isDesktop();
|
const isDesktop = Utils.isDesktop();
|
||||||
const modeBaseClass = 'btn btn-secondary btn-icon sf-view-mode-btn';
|
const modeBaseClass = 'btn btn-secondary btn-icon sf-view-mode-btn';
|
||||||
@@ -249,7 +278,7 @@ class SharedDirView extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
<div className="o-auto">
|
<div className="o-auto">
|
||||||
<div className="shared-dir-view-main">
|
<div className="shared-dir-view-main">
|
||||||
<h2 className="title">{dirName}</h2>
|
<h2 className="h3">{dirName}</h2>
|
||||||
<p>{gettext('Shared by: ')}{sharedBy}</p>
|
<p>{gettext('Shared by: ')}{sharedBy}</p>
|
||||||
<div className="d-flex justify-content-between align-items-center op-bar">
|
<div className="d-flex justify-content-between align-items-center op-bar">
|
||||||
<p className="m-0">{gettext('Current path: ')}{this.renderPath()}</p>
|
<p className="m-0">{gettext('Current path: ')}{this.renderPath()}</p>
|
||||||
@@ -260,16 +289,31 @@ class SharedDirView extends React.Component {
|
|||||||
<a href={`?p=${encodeURIComponent(path)}&mode=grid`} className={`${modeBaseClass} sf2-icon-grid-view ${mode == 'grid' ? 'current-mode' : ''}`} title={gettext('Grid')}></a>
|
<a href={`?p=${encodeURIComponent(path)}&mode=grid`} className={`${modeBaseClass} sf2-icon-grid-view ${mode == 'grid' ? 'current-mode' : ''}`} title={gettext('Grid')}></a>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
{canUpload && (
|
||||||
|
<Button disabled={noQuota}
|
||||||
|
title={noQuota ? gettext('The owner of this library has run out of space.') : ''}
|
||||||
|
onClick={this.onUploadFile} className="ml-2 shared-dir-op-btn shared-dir-upload-btn">{gettext('Upload')}</Button>
|
||||||
|
)}
|
||||||
{showDownloadIcon &&
|
{showDownloadIcon &&
|
||||||
<Fragment>
|
<Fragment>
|
||||||
{this.state.items.some(item => item.isSelected) ?
|
{this.state.items.some(item => item.isSelected) ?
|
||||||
<Button color="success" onClick={this.zipDownloadSelectedItems} className="ml-2 zip-btn">{gettext('ZIP Selected Items')}</Button> :
|
<Button color="success" onClick={this.zipDownloadSelectedItems} className="ml-2 shared-dir-op-btn">{gettext('ZIP Selected Items')}</Button> :
|
||||||
<Button color="success" onClick={this.zipDownloadFolder.bind(this, path)} className="ml-2 zip-btn">{gettext('ZIP')}</Button>
|
<Button color="success" onClick={this.zipDownloadFolder.bind(this, path)} className="ml-2 shared-dir-op-btn">{gettext('ZIP')}</Button>
|
||||||
}
|
}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{!noQuota && canUpload && (
|
||||||
|
<FileUploader
|
||||||
|
ref={uploader => this.uploader = uploader}
|
||||||
|
dragAndDrop={false}
|
||||||
|
token={token}
|
||||||
|
path={path}
|
||||||
|
repoID={repoID}
|
||||||
|
onFileUploadSuccess={this.onFileUploadSuccess}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Content
|
<Content
|
||||||
isDesktop={isDesktop}
|
isDesktop={isDesktop}
|
||||||
isLoading={this.state.isLoading}
|
isLoading={this.state.isLoading}
|
||||||
|
@@ -119,57 +119,48 @@ export const Utils = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
getShareLinkPermissionList: function(itemType, permission = null, path, canEdit = null) {
|
getShareLinkPermissionList: function(itemType, permission, path, canEdit) {
|
||||||
// itemType: library, dir, file
|
// itemType: library, dir, file
|
||||||
// permission: rw, r, admin, cloud-edit, preview
|
// permission: rw, r, admin, cloud-edit, preview
|
||||||
|
|
||||||
// if item is library, can preview and download, no need to check
|
|
||||||
// if item is dir, check can download
|
|
||||||
// if item is file, check can download and check can edit
|
|
||||||
|
|
||||||
let editDownloadOption = 'edit_download';
|
|
||||||
let editOnly = 'cloud_edit';
|
|
||||||
let downloadOption = 'preview_download';
|
|
||||||
let permissionOptions = [];
|
let permissionOptions = [];
|
||||||
|
|
||||||
if (itemType === 'library') {
|
if (permission == 'rw' || permission == 'admin' || permission == 'r') {
|
||||||
permissionOptions.push(downloadOption);
|
permissionOptions.push('preview_download');
|
||||||
} else if (itemType === 'dir') {
|
}
|
||||||
if (permission == 'rw' || permission == 'admin' || permission == 'r') {
|
permissionOptions.push('preview_only');
|
||||||
permissionOptions.push(downloadOption);
|
|
||||||
}
|
if (itemType == 'library' || itemType == 'dir') {
|
||||||
} else if (itemType === 'file') {
|
if (permission == 'rw' || permission == 'admin') {
|
||||||
if (permission == 'rw' || permission == 'admin' || permission == 'r') {
|
permissionOptions.push('download_upload');
|
||||||
permissionOptions.push(downloadOption);
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
if (this.isEditableOfficeFile(path) && (permission == 'rw' || permission == 'admin') && canEdit) {
|
if (this.isEditableOfficeFile(path) && (permission == 'rw' || permission == 'admin') && canEdit) {
|
||||||
permissionOptions.push(editDownloadOption);
|
permissionOptions.push('edit_download');
|
||||||
}
|
}
|
||||||
|
|
||||||
// not support
|
// not support
|
||||||
// if (this.isEditableOfficeFile(path) && (permission == 'cloud-edit')) {
|
// if (this.isEditableOfficeFile(path) && (permission == 'cloud-edit')) {
|
||||||
// permissionOptions.push(editOnly);
|
// permissionOptions.push('cloud_edit');
|
||||||
// }
|
// }
|
||||||
|
|
||||||
}
|
}
|
||||||
permissionOptions.push('preview_only');
|
|
||||||
return permissionOptions;
|
return permissionOptions;
|
||||||
},
|
},
|
||||||
|
|
||||||
getShareLinkPermissionStr: function(permissions) {
|
getShareLinkPermissionStr: function(permissions) {
|
||||||
const { can_edit, can_download } = permissions;
|
const { can_edit, can_download, can_upload } = permissions;
|
||||||
if (can_edit) {
|
switch (`${can_edit} ${can_download} ${can_upload}`) {
|
||||||
if (can_download) {
|
case 'false true false':
|
||||||
return 'edit_download';
|
|
||||||
} else {
|
|
||||||
return 'cloud_edit';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (can_download) {
|
|
||||||
return 'preview_download';
|
return 'preview_download';
|
||||||
} else {
|
case 'false false false':
|
||||||
return 'preview_only';
|
return 'preview_only';
|
||||||
}
|
case 'false true true':
|
||||||
|
return 'download_upload';
|
||||||
|
case 'true true false':
|
||||||
|
return 'edit_download';
|
||||||
|
case 'true false false':
|
||||||
|
return 'cloud_edit';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -316,7 +307,7 @@ export const Utils = {
|
|||||||
isSupportUploadFolder: function() {
|
isSupportUploadFolder: function() {
|
||||||
return navigator.userAgent.indexOf('Firefox')!=-1 ||
|
return navigator.userAgent.indexOf('Firefox')!=-1 ||
|
||||||
navigator.userAgent.indexOf('Chrome') > -1 ||
|
navigator.userAgent.indexOf('Chrome') > -1 ||
|
||||||
navigator.userAgent.indexOf("Safari") > -1;
|
navigator.userAgent.indexOf('Safari') > -1;
|
||||||
},
|
},
|
||||||
|
|
||||||
isIEBrower: function() { // is ie <= ie11 not include Edge
|
isIEBrower: function() { // is ie <= ie11 not include Edge
|
||||||
@@ -615,7 +606,8 @@ export const Utils = {
|
|||||||
text: gettext('Preview and download'),
|
text: gettext('Preview and download'),
|
||||||
permissionDetails: {
|
permissionDetails: {
|
||||||
'can_edit': false,
|
'can_edit': false,
|
||||||
"can_download": true
|
'can_download': true,
|
||||||
|
'can_upload': false
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
case 'preview_only':
|
case 'preview_only':
|
||||||
@@ -624,7 +616,18 @@ export const Utils = {
|
|||||||
text: gettext('Preview only'),
|
text: gettext('Preview only'),
|
||||||
permissionDetails: {
|
permissionDetails: {
|
||||||
'can_edit': false,
|
'can_edit': false,
|
||||||
"can_download": false
|
'can_download': false,
|
||||||
|
'can_upload': false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
case 'download_upload':
|
||||||
|
return {
|
||||||
|
value: permission,
|
||||||
|
text: gettext('Download and upload'),
|
||||||
|
permissionDetails: {
|
||||||
|
'can_edit': false,
|
||||||
|
'can_download': true,
|
||||||
|
'can_upload': true
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
case 'edit_download':
|
case 'edit_download':
|
||||||
@@ -633,7 +636,8 @@ export const Utils = {
|
|||||||
text: gettext('Edit on cloud and download'),
|
text: gettext('Edit on cloud and download'),
|
||||||
permissionDetails: {
|
permissionDetails: {
|
||||||
'can_edit': true,
|
'can_edit': true,
|
||||||
"can_download": true
|
'can_download': true,
|
||||||
|
'can_upload': false
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
case 'cloud_edit':
|
case 'cloud_edit':
|
||||||
@@ -642,7 +646,8 @@ export const Utils = {
|
|||||||
text: gettext('Edit on cloud only'),
|
text: gettext('Edit on cloud only'),
|
||||||
permissionDetails: {
|
permissionDetails: {
|
||||||
'can_edit': true,
|
'can_edit': true,
|
||||||
"can_download": false
|
'can_download': false,
|
||||||
|
'can_upload': false
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@@ -21,6 +21,7 @@
|
|||||||
pageOptions: {
|
pageOptions: {
|
||||||
dirName: '{{ dir_name|escapejs }}',
|
dirName: '{{ dir_name|escapejs }}',
|
||||||
sharedBy: '{{ username|email2nickname|escapejs }}',
|
sharedBy: '{{ username|email2nickname|escapejs }}',
|
||||||
|
repoID: '{{repo.id}}',
|
||||||
path: '{{ path|escapejs }}',
|
path: '{{ path|escapejs }}',
|
||||||
zipped: (function() {
|
zipped: (function() {
|
||||||
var list = [];
|
var list = [];
|
||||||
@@ -36,7 +37,9 @@
|
|||||||
mode: '{{ mode }}',
|
mode: '{{ mode }}',
|
||||||
thumbnailSize: {{ thumbnail_size }},
|
thumbnailSize: {{ thumbnail_size }},
|
||||||
trafficOverLimit: {% if traffic_over_limit %}true{% else %}false{% endif %},
|
trafficOverLimit: {% if traffic_over_limit %}true{% else %}false{% endif %},
|
||||||
canDownload: {% if permissions.can_download %}true{% else %}false{% endif %}
|
canDownload: {% if permissions.can_download %}true{% else %}false{% endif %},
|
||||||
|
noQuota: {% if no_quota %}true{% else %}false{% endif %},
|
||||||
|
canUpload: {% if permissions.can_upload %}true{% else %}false{% endif %}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@@ -328,7 +328,10 @@ def view_shared_dir(request, fileshare):
|
|||||||
req_image_path = posixpath.join(req_path, f.obj_name)
|
req_image_path = posixpath.join(req_path, f.obj_name)
|
||||||
src = get_share_link_thumbnail_src(token, thumbnail_size, req_image_path)
|
src = get_share_link_thumbnail_src(token, thumbnail_size, req_image_path)
|
||||||
f.encoded_thumbnail_src = urlquote(src)
|
f.encoded_thumbnail_src = urlquote(src)
|
||||||
|
|
||||||
|
# for 'upload file'
|
||||||
|
no_quota = True if seaserv.check_quota(repo_id) < 0 else False
|
||||||
|
|
||||||
#template = 'view_shared_dir.html'
|
#template = 'view_shared_dir.html'
|
||||||
template = 'view_shared_dir_react.html'
|
template = 'view_shared_dir_react.html'
|
||||||
|
|
||||||
@@ -345,6 +348,7 @@ def view_shared_dir(request, fileshare):
|
|||||||
'dir_list': dir_list,
|
'dir_list': dir_list,
|
||||||
'zipped': zipped,
|
'zipped': zipped,
|
||||||
'traffic_over_limit': traffic_over_limit,
|
'traffic_over_limit': traffic_over_limit,
|
||||||
|
'no_quota': no_quota,
|
||||||
'permissions': permissions,
|
'permissions': permissions,
|
||||||
'ENABLE_THUMBNAIL': ENABLE_THUMBNAIL,
|
'ENABLE_THUMBNAIL': ENABLE_THUMBNAIL,
|
||||||
'mode': mode,
|
'mode': mode,
|
||||||
|
Reference in New Issue
Block a user