diff --git a/frontend/config/webpack.config.dev.js b/frontend/config/webpack.config.dev.js
index 9f1d21e9d2..0d47dcc1f3 100644
--- a/frontend/config/webpack.config.dev.js
+++ b/frontend/config/webpack.config.dev.js
@@ -204,6 +204,11 @@ module.exports = {
require.resolve('react-dev-utils/webpackHotDevClient'),
paths.appSrc + "/repo-history.js",
],
+ repoSnapshot: [
+ require.resolve('./polyfills'),
+ require.resolve('react-dev-utils/webpackHotDevClient'),
+ paths.appSrc + "/repo-snapshot.js",
+ ],
repoFolderTrash: [
require.resolve('./polyfills'),
require.resolve('react-dev-utils/webpackHotDevClient'),
diff --git a/frontend/config/webpack.config.prod.js b/frontend/config/webpack.config.prod.js
index 3468f4f578..c56d323b3e 100644
--- a/frontend/config/webpack.config.prod.js
+++ b/frontend/config/webpack.config.prod.js
@@ -89,6 +89,7 @@ module.exports = {
viewFileUnknown: [require.resolve('./polyfills'), paths.appSrc + "/view-file-unknown.js"],
settings: [require.resolve('./polyfills'), paths.appSrc + "/settings.js"],
repoHistory: [require.resolve('./polyfills'), paths.appSrc + "/repo-history.js"],
+ repoSnapshot: [require.resolve('./polyfills'), paths.appSrc + "/repo-snapshot.js"],
repoFolderTrash: [require.resolve('./polyfills'), paths.appSrc + "/repo-folder-trash.js"],
orgAdmin: [require.resolve('./polyfills'), paths.appSrc + "/pages/org-admin"],
sysAdmin: [require.resolve('./polyfills'), paths.appSrc + "/pages/sys-admin"],
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index fd012dfb4f..8754b71214 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -15921,9 +15921,9 @@
}
},
"seafile-js": {
- "version": "0.2.91",
- "resolved": "https://registry.npmjs.org/seafile-js/-/seafile-js-0.2.91.tgz",
- "integrity": "sha512-QNm629wX+NmCUzKiqOh1h/LWRwnRvwKwRXU6qOdOHg2CyubgGXmzk6U9NPMW1oK3p0takfz/1xypPQxNIAsXSw==",
+ "version": "0.2.92",
+ "resolved": "https://registry.npmjs.org/seafile-js/-/seafile-js-0.2.92.tgz",
+ "integrity": "sha512-GySMa7cr8LXWj+uyIOG9JAIzUlzuGHZ1dLbx0tKrUcJ9vY10xVnDPCmJz3jEnG59wXNSlqkwcEaQxvaANfdgeg==",
"requires": {
"axios": "^0.18.0",
"form-data": "^2.3.2",
diff --git a/frontend/package.json b/frontend/package.json
index 73ec5e61fc..89e5fe4272 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -37,7 +37,7 @@
"react-responsive": "^6.1.1",
"react-select": "^2.4.1",
"reactstrap": "^6.4.0",
- "seafile-js": "^0.2.91",
+ "seafile-js": "^0.2.92",
"socket.io-client": "^2.2.0",
"sw-precache-webpack-plugin": "0.11.4",
"unified": "^7.0.0",
diff --git a/frontend/src/components/dialog/confirm-restore-repo.js b/frontend/src/components/dialog/confirm-restore-repo.js
new file mode 100644
index 0000000000..e20fe08da7
--- /dev/null
+++ b/frontend/src/components/dialog/confirm-restore-repo.js
@@ -0,0 +1,46 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import { Modal, ModalHeader, ModalBody, ModalFooter, Button } from 'reactstrap';
+import { gettext } from '../../utils/constants';
+
+const propTypes = {
+ restoreRepo: PropTypes.func.isRequired,
+ toggle: PropTypes.func.isRequired
+};
+
+class ConfirmRestoreRepo extends Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ btnDisabled: false
+ };
+ }
+
+ action = () => {
+ this.setState({
+ btnDisabled: true
+ });
+ this.props.restoreRepo();
+ }
+
+ render() {
+ const {formActionURL, csrfToken, toggle} = this.props;
+ return (
+
+ {gettext('Restore Library')}
+
+ {gettext('Are you sure you want to restore this library?')}
+
+
+
+
+
+
+ );
+ }
+}
+
+ConfirmRestoreRepo.propTypes = propTypes;
+
+export default ConfirmRestoreRepo;
diff --git a/frontend/src/components/dir-view-mode/dir-column-nav.js b/frontend/src/components/dir-view-mode/dir-column-nav.js
index 845b4df570..d952464176 100644
--- a/frontend/src/components/dir-view-mode/dir-column-nav.js
+++ b/frontend/src/components/dir-view-mode/dir-column-nav.js
@@ -33,6 +33,7 @@ const propTypes = {
onItemMove: PropTypes.func.isRequired,
onItemCopy: PropTypes.func.isRequired,
selectedDirentList: PropTypes.array.isRequired,
+ onItemsMove: PropTypes.func.isRequired,
};
class DirColumnNav extends React.Component {
@@ -276,6 +277,7 @@ class DirColumnNav extends React.Component {
onItemMove={this.props.onItemMove}
currentRepoInfo={this.props.currentRepoInfo}
selectedDirentList={this.props.selectedDirentList}
+ onItemsMove={this.props.onItemsMove}
/>)
}
diff --git a/frontend/src/components/dir-view-mode/dir-column-view.js b/frontend/src/components/dir-view-mode/dir-column-view.js
index 12ef40d4ef..65fa1c7017 100644
--- a/frontend/src/components/dir-view-mode/dir-column-view.js
+++ b/frontend/src/components/dir-view-mode/dir-column-view.js
@@ -171,6 +171,7 @@ class DirColumnView extends React.Component {
onItemMove={this.props.onItemMove}
onItemCopy={this.props.onItemCopy}
selectedDirentList={this.props.selectedDirentList}
+ onItemsMove={this.props.onItemsMove}
/>
diff --git a/frontend/src/components/dirent-detail/dirent-details.js b/frontend/src/components/dirent-detail/dirent-details.js
index a2d9334d13..b4a27e0833 100644
--- a/frontend/src/components/dirent-detail/dirent-details.js
+++ b/frontend/src/components/dirent-detail/dirent-details.js
@@ -189,7 +189,7 @@ class DirentDetail extends React.Component {
}
render() {
- let { dirent } = this.props;
+ let { dirent, repoID, path } = this.props;
let { folderDirent } = this.state;
if (!dirent && !folderDirent) {
return '';
@@ -198,7 +198,7 @@ class DirentDetail extends React.Component {
let bigIconUrl = dirent ? Utils.getDirentIcon(dirent, true) : Utils.getDirentIcon(folderDirent, true);
const isImg = dirent ? Utils.imageCheck(dirent.name) : Utils.imageCheck(folderDirent.name);
if (isImg) {
- bigIconUrl = siteRoot + 'thumbnail/' + this.props.repoID + '/1024/' + dirent.name;
+ bigIconUrl = `${siteRoot}thumbnail/${repoID}/1024` + Utils.encodePath(`${path === '/' ? '' : path}/${dirent.name}`);
}
let direntName = dirent ? dirent.name : folderDirent.name;
diff --git a/frontend/src/components/dirent-list-view/dirent-list-item.js b/frontend/src/components/dirent-list-view/dirent-list-item.js
index 59338ef9d8..81822b790f 100644
--- a/frontend/src/components/dirent-list-view/dirent-list-item.js
+++ b/frontend/src/components/dirent-list-view/dirent-list-item.js
@@ -49,6 +49,7 @@ const propTypes = {
onFileTagChanged: PropTypes.func,
enableDirPrivateShare: PropTypes.bool.isRequired,
showDirentDetail: PropTypes.func.isRequired,
+ onItemsMove: PropTypes.func.isRequired,
};
class DirentListItem extends React.Component {
@@ -349,15 +350,28 @@ class DirentListItem extends React.Component {
if (Utils.isIEBrower()) {
return false;
}
- let nodeRootPath = '';
- nodeRootPath = this.props.path === '/' ? `${this.props.path}${this.props.dirent.name}` : `${this.props.path}/${this.props.dirent.name}`;
- let dragStartItemData = {nodeDirent: this.props.dirent, nodeParentPath: this.props.path, nodeRootPath: nodeRootPath};
- dragStartItemData = JSON.stringify(dragStartItemData);
e.dataTransfer.effectAllowed = 'move';
if (e.dataTransfer && e.dataTransfer.setDragImage) {
e.dataTransfer.setDragImage(this.refs.drag_icon, 15, 15);
}
+
+ let { selectedDirentList } = this.props;
+ if (selectedDirentList.length > 0 && selectedDirentList.includes(this.props.dirent)) { // drag items and selectedDirentList include item
+ let selectedList = selectedDirentList.map(item => {
+ let nodeRootPath = this.getDirentPath(item);
+ let dragStartItemData = {nodeDirent: item, nodeParentPath: this.props.path, nodeRootPath: nodeRootPath};
+ return dragStartItemData;
+ });
+ selectedList = JSON.stringify(selectedList);
+ e.dataTransfer.setData('applicaiton/drag-item-info', selectedList);
+ return ;
+ }
+
+ let nodeRootPath = this.getDirentPath(this.props.dirent);
+ let dragStartItemData = {nodeDirent: this.props.dirent, nodeParentPath: this.props.path, nodeRootPath: nodeRootPath};
+ dragStartItemData = JSON.stringify(dragStartItemData);
+
e.dataTransfer.setData('applicaiton/drag-item-info', dragStartItemData);
}
@@ -395,7 +409,22 @@ class DirentListItem extends React.Component {
}
let dragStartItemData = e.dataTransfer.getData('applicaiton/drag-item-info');
dragStartItemData = JSON.parse(dragStartItemData);
- let {nodeDirent, nodeParentPath, nodeRootPath} = dragStartItemData;
+ if (Array.isArray(dragStartItemData)) { //move items
+ let direntPaths = dragStartItemData.map(draggedItem => {
+ return draggedItem.nodeRootPath
+ });
+
+ let selectedPath = Utils.joinPath(this.props.path, this.props.dirent.name);
+
+ if (direntPaths.some(direntPath => { return direntPath === selectedPath;})) { //eg; A/B, A/C --> A/B
+ return;
+ }
+
+ this.props.onItemsMove(this.props.currentRepoInfo, selectedPath);
+ return ;
+ }
+
+ let { nodeDirent, nodeParentPath, nodeRootPath } = dragStartItemData;
let dropItemData = this.props.dirent;
if (nodeDirent.name === dropItemData.name) {
diff --git a/frontend/src/components/dirent-list-view/dirent-list-view.js b/frontend/src/components/dirent-list-view/dirent-list-view.js
index a032e38998..84d2107539 100644
--- a/frontend/src/components/dirent-list-view/dirent-list-view.js
+++ b/frontend/src/components/dirent-list-view/dirent-list-view.js
@@ -577,6 +577,11 @@ class DirentListView extends React.Component {
let {nodeDirent, nodeParentPath, nodeRootPath} = dragStartItemData;
if (e.target.className === 'table-container table-drop-active') {
+
+ if (Array.isArray(dragStartItemData)) { //selected items
+ return;
+ }
+
if (nodeRootPath === this.props.path || nodeParentPath === this.props.path) {
return;
}
@@ -664,6 +669,7 @@ class DirentListView extends React.Component {
onFileTagChanged={this.props.onFileTagChanged}
getDirentItemMenuList={this.getDirentItemMenuList}
showDirentDetail={this.props.showDirentDetail}
+ onItemsMove={this.props.onItemsMove}
/>
);
})}
diff --git a/frontend/src/components/file-uploader/file-uploader.js b/frontend/src/components/file-uploader/file-uploader.js
index 26d4b21eee..da30a6ad22 100644
--- a/frontend/src/components/file-uploader/file-uploader.js
+++ b/frontend/src/components/file-uploader/file-uploader.js
@@ -417,8 +417,13 @@ class FileUploader extends React.Component {
let repoID = this.props.repoID;
seafileAPI.getUploadLink(repoID, this.props.path).then(res => {
this.resumable.opts.target = res.data;
- this.uploadInput.current.click();
+ if (Utils.isIEBrower()) {
+ this.uploadInput.current.click();
+ }
});
+ if (!Utils.isIEBrower()) {
+ this.uploadInput.current.click();
+ }
}
onFolderUpload = () => {
@@ -426,8 +431,13 @@ class FileUploader extends React.Component {
let repoID = this.props.repoID;
seafileAPI.getUploadLink(repoID, this.props.path).then(res => {
this.resumable.opts.target = res.data;
- this.uploadInput.current.click();
+ if (Utils.isIEBrower()) {
+ this.uploadInput.current.click();
+ }
});
+ if (!Utils.isIEBrower()) {
+ this.uploadInput.current.click();
+ }
}
onDragStart = () => {
diff --git a/frontend/src/components/tree-view/tree-view.js b/frontend/src/components/tree-view/tree-view.js
index c3560ed5bc..fb04d36907 100644
--- a/frontend/src/components/tree-view/tree-view.js
+++ b/frontend/src/components/tree-view/tree-view.js
@@ -18,6 +18,7 @@ const propTypes = {
onItemMove: PropTypes.func,
currentRepoInfo: PropTypes.object,
selectedDirentList: PropTypes.array,
+ onItemsMove: PropTypes.func,
};
const PADDING_LEFT = 20;
@@ -91,6 +92,20 @@ class TreeView extends React.Component {
let {nodeDirent, nodeParentPath, nodeRootPath} = dragStartNodeData;
let dropNodeData = node;
+ if (Array.isArray(dragStartNodeData)) { //move items
+ if (!dropNodeData) { //move items to root
+ if (dragStartNodeData[0].nodeParentPath === '/') {
+ this.setState({isTreeViewDropTipShow: false});
+ return;
+ }
+ this.props.onItemsMove(this.props.currentRepoInfo, '/');
+ this.setState({isTreeViewDropTipShow: false});
+ return;
+ }
+ this.onMoveItems(dragStartNodeData, dropNodeData, this.props.currentRepoInfo, dropNodeData.path);
+ return;
+ }
+
if (!dropNodeData) {
if (nodeParentPath === '/') {
this.setState({isTreeViewDropTipShow: false});
@@ -119,7 +134,8 @@ class TreeView extends React.Component {
// copy the dirent to it's child. eg: A/B -> A/B/C
if (dropNodeData.object.type === 'dir' && nodeDirent.type === 'dir') {
if (dropNodeData.parentNode.path !== nodeParentPath) {
- if (dropNodeData.path.indexOf(nodeRootPath) !== -1) {
+ let paths = Utils.getPaths(dropNodeData.path);
+ if (paths.includes(nodeRootPath)) {
return;
}
}
@@ -128,6 +144,39 @@ class TreeView extends React.Component {
this.onItemMove(this.props.currentRepoInfo, nodeDirent, dropNodeData.path, nodeParentPath);
}
+ onMoveItems = (dragStartNodeData, dropNodeData, destRepo, destDirentPath) => {
+ let direntPaths = [];
+ let paths = Utils.getPaths(destDirentPath);
+ dragStartNodeData.forEach(dirent => {
+ let path = dirent.nodeRootPath;
+ direntPaths.push(path);
+ });
+
+ if (dropNodeData.object.type !== 'dir') {
+ return;
+ }
+
+ // move dirents to one of them. eg: A/B, A/C -> A/B
+ if (direntPaths.some(direntPath => { return direntPath === destDirentPath;})) {
+ return;
+ }
+
+ // move dirents to current path
+ if (dragStartNodeData[0].nodeParentPath && dragStartNodeData[0].nodeParentPath === dropNodeData.path ) {
+ return;
+ }
+
+ // move dirents to one of their child. eg: A/B, A/D -> A/B/C
+ let isChildPath = direntPaths.some(direntPath => {
+ return paths.includes(direntPath);
+ });
+ if (isChildPath) {
+ return;
+ }
+
+ this.props.onItemsMove(destRepo, destDirentPath);
+ }
+
freezeItem = () => {
this.setState({isItemFreezed: true});
}
diff --git a/frontend/src/css/repo-snapshot.css b/frontend/src/css/repo-snapshot.css
new file mode 100644
index 0000000000..cf63ab3952
--- /dev/null
+++ b/frontend/src/css/repo-snapshot.css
@@ -0,0 +1,37 @@
+body {
+ overflow: hidden;
+}
+#wrapper {
+ height: 100%;
+}
+.top-header {
+ background: #f4f4f7;
+ border-bottom: 1px solid #e8e8e8;
+ padding: .5rem 1rem;
+ flex-shrink: 0;
+}
+.go-back {
+ color: #c0c0c0;
+ font-size: 1.75rem;
+ position: absolute;
+ left: -40px;
+ top: -5px;
+}
+.op-bar {
+ padding: 9px 10px;
+ background: #f2f2f2;
+ border-radius: 2px;
+}
+.op-bar-btn {
+ border-color: #ccc;
+ border-radius: 2px;
+ height: 30px;
+ line-height: 28px;
+ font-weight: normal;
+ padding: 0 0.5rem;
+ min-width: 55px;
+}
+.heading-commit-time {
+ font-weight: normal;
+ font-size: 60%;
+}
diff --git a/frontend/src/repo-history.js b/frontend/src/repo-history.js
index 697f9e8a47..f391653748 100644
--- a/frontend/src/repo-history.js
+++ b/frontend/src/repo-history.js
@@ -294,7 +294,7 @@ class Item extends React.Component {
{userPerm == 'rw' && (
item.isFirstCommit ?
{gettext('Current Version')} :
-
{gettext('View Snapshot')}
+
{gettext('View Snapshot')}
)}
diff --git a/frontend/src/repo-snapshot.js b/frontend/src/repo-snapshot.js
new file mode 100644
index 0000000000..b0750bfa55
--- /dev/null
+++ b/frontend/src/repo-snapshot.js
@@ -0,0 +1,337 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { navigate } from '@reach/router';
+import moment from 'moment';
+import { Utils } from './utils/utils';
+import { gettext, loginUrl, siteRoot, mediaUrl, logoPath, logoWidth, logoHeight, siteTitle } from './utils/constants';
+import { seafileAPI } from './utils/seafile-api';
+import Loading from './components/loading';
+import ModalPortal from './components/modal-portal';
+import toaster from './components/toast';
+import CommonToolbar from './components/toolbar/common-toolbar';
+import ConfirmRestoreRepo from './components/dialog/confirm-restore-repo';
+
+import './css/toolbar.css';
+import './css/search.css';
+
+import './css/repo-snapshot.css';
+
+const {
+ repoID, repoName, isRepoOwner,
+ commitID, commitTime, commitDesc, commitRelativeTime,
+ showAuthor, authorAvatarURL, authorName, authorNickName
+} = window.app.pageOptions;
+
+class RepoSnapshot extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ isLoading: true,
+ errorMsg: '',
+ folderPath: '/',
+ folderItems: [],
+ isConfirmDialogOpen: false
+ };
+ }
+
+ componentDidMount() {
+ this.renderFolder(this.state.folderPath);
+ }
+
+ toggleDialog = () => {
+ this.setState({
+ isConfirmDialogOpen: !this.state.isConfirmDialogOpen
+ });
+ }
+
+ onSearchedClick = (selectedItem) => {
+ if (selectedItem.is_dir === true) {
+ let url = siteRoot + 'library/' + selectedItem.repo_id + '/' + selectedItem.repo_name + selectedItem.path;
+ navigate(url, {repalce: true});
+ } else {
+ let url = siteRoot + 'lib/' + selectedItem.repo_id + '/file' + Utils.encodePath(selectedItem.path);
+ let newWindow = window.open('about:blank');
+ newWindow.location.href = url;
+ }
+ }
+
+ goBack = (e) => {
+ e.preventDefault();
+ window.history.back();
+ }
+
+ renderFolder = (folderPath) => {
+ this.setState({
+ folderPath: folderPath,
+ folderItems: [],
+ isLoading: true
+ });
+
+ seafileAPI.listCommitDir(repoID, commitID, folderPath).then((res) => {
+ this.setState({
+ isLoading: false,
+ folderItems: res.data.dirent_list
+ });
+ }).catch((error) => {
+ if (error.response) {
+ if (error.response.status == 403) {
+ this.setState({
+ isLoading: false,
+ errorMsg: gettext('Permission denied')
+ });
+ location.href = `${loginUrl}?next=${encodeURIComponent(location.href)}`;
+ } else {
+ this.setState({
+ isLoading: false,
+ errorMsg: gettext('Error')
+ });
+ }
+ } else {
+ this.setState({
+ isLoading: false,
+ errorMsg: gettext('Please check the network.')
+ });
+ }
+ });
+ }
+
+ clickFolderPath = (folderPath, e) => {
+ e.preventDefault();
+ this.renderFolder(folderPath);
+ }
+
+ renderPath = () => {
+ const path = this.state.folderPath;
+ const pathList = path.split('/');
+
+ if (path == '/') {
+ return repoName;
+ }
+
+ return (
+
+ {repoName}
+ /
+ {pathList.map((item, index) => {
+ if (index > 0 && index != pathList.length - 1) {
+ return (
+
+ {pathList[index]}
+ /
+
+ );
+ }
+ }
+ )}
+ {pathList[pathList.length - 1]}
+
+ );
+ }
+
+ restoreRepo = () => {
+ seafileAPI.revertRepo(repoID, commitID).then((res) => {
+ this.toggleDialog();
+ toaster.success(gettext('Successfully restored the library.'));
+ }).catch((error) => {
+ let errorMsg = '';
+ if (error.response) {
+ if (error.response.data && error.response.data['error_msg']) {
+ errorMsg = error.response.data['error_msg'];
+ } else {
+ errorMsg = gettext('Error');
+ }
+ } else {
+ errorMsg = gettext('Please check the network.');
+ }
+ this.toggleDialog();
+ toaster.danger(errorMsg);
+ });
+ }
+
+ render() {
+ const { isConfirmDialogOpen, folderPath } = this.state;
+
+ return (
+
+
+
+
+
+
+
(${commitTime})`}}>
+
+
+
+ {folderPath == '/' && (
+
+ )}
+
+
{gettext('Current path: ')}{this.renderPath()}
+ {(folderPath == '/' && isRepoOwner) &&
+
+ }
+
+
+
+
+
+
+ {isConfirmDialogOpen &&
+
+
+
+ }
+
+ );
+ }
+}
+
+class Content extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.theadData = [
+ {width: '5%', text: ''},
+ {width: '55%', text: gettext('Name')},
+ {width: '20%', text: gettext('Size')},
+ {width: '20%', text: ''}
+ ];
+ }
+
+ render() {
+ const { isLoading, errorMsg, folderPath, folderItems } = this.props.data;
+
+ if (isLoading) {
+ return
;
+ }
+
+ if (errorMsg) {
+ return
{errorMsg}
;
+ }
+
+ return (
+
+
+
+ {this.theadData.map((item, index) => {
+ return {item.text} | ;
+ })}
+
+
+
+ {folderItems.map((item, index) => {
+ return ;
+ })
+ }
+
+
+ );
+ }
+}
+
+class FolderItem extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ isIconShown: false
+ };
+ }
+
+ handleMouseOver = () => {
+ this.setState({isIconShown: true});
+ }
+
+ handleMouseOut = () => {
+ this.setState({isIconShown: false});
+ }
+
+ restoreItem = (e) => {
+ e.preventDefault();
+
+ const item = this.props.item;
+ const path = Utils.joinPath(this.props.folderPath, item.name);
+ const request = item.type == 'dir' ?
+ seafileAPI.revertFolder(repoID, path, commitID):
+ seafileAPI.revertFile(repoID, path, commitID);
+ request.then((res) => {
+ toaster.success(gettext('Successfully restored 1 item.'));
+ }).catch((error) => {
+ let errorMsg = '';
+ if (error.response) {
+ errorMsg = error.response.data.error_msg || gettext('Error');
+ } else {
+ errorMsg = gettext('Please check the network.');
+ }
+ toaster.danger(errorMsg);
+ });
+ }
+
+ renderFolder = (e) => {
+ e.preventDefault();
+
+ const item = this.props.item;
+ const { folderPath } = this.props;
+ this.props.renderFolder(Utils.joinPath(folderPath, item.name));
+ }
+
+ render() {
+ const item = this.props.item;
+ const { isIconShown } = this.state;
+ const { folderPath } = this.props;
+
+ return item.type == 'dir' ? (
+
+ }) |
+ {item.name} |
+ |
+
+
+ |
+
+ ) : (
+
+ }) |
+ {item.name} |
+ {Utils.bytesToSize(item.size)} |
+
+
+
+ |
+
+ );
+ }
+}
+
+ReactDOM.render(
+
,
+ document.getElementById('wrapper')
+);
diff --git a/frontend/src/settings.js b/frontend/src/settings.js
index 0f51415233..3d58039034 100644
--- a/frontend/src/settings.js
+++ b/frontend/src/settings.js
@@ -116,7 +116,7 @@ class Settings extends React.Component {
-
+
diff --git a/seahub/api2/endpoints/deleted_repos.py b/seahub/api2/endpoints/deleted_repos.py
index 1010743dd4..95229d1338 100644
--- a/seahub/api2/endpoints/deleted_repos.py
+++ b/seahub/api2/endpoints/deleted_repos.py
@@ -6,8 +6,6 @@ from rest_framework.response import Response
from rest_framework import status
from seaserv import seafile_api
-from pysearpc import SearpcError
-
from seahub.signals import repo_restored
from seahub.api2.throttling import UserRateThrottle
from seahub.api2.authentication import TokenAuthentication
@@ -71,7 +69,7 @@ class DeletedRepos(APIView):
try:
seafile_api.restore_repo_from_trash(repo_id)
repo_restored.send(sender=None, repo_id=repo_id, operator=username)
- except SearpcError as e:
+ except Exception as e:
logger.error(e)
error_msg = "Internal Server Error"
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
diff --git a/seahub/api2/endpoints/repo_commit_dir.py b/seahub/api2/endpoints/repo_commit_dir.py
index 645f3c550b..7498a0b6af 100644
--- a/seahub/api2/endpoints/repo_commit_dir.py
+++ b/seahub/api2/endpoints/repo_commit_dir.py
@@ -61,7 +61,7 @@ class RepoCommitDirView(APIView):
commit = seafile_api.get_commit(repo.id, repo.version, commit_id)
if not commit:
- error_msg = 'Commit %s not found.' % commit
+ error_msg = 'Commit %s not found.' % commit_id
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
dir_id = seafile_api.get_dir_id_by_commit_and_path(repo_id, commit_id, path)
diff --git a/seahub/api2/endpoints/repo_commit_revert.py b/seahub/api2/endpoints/repo_commit_revert.py
new file mode 100644
index 0000000000..31a061be26
--- /dev/null
+++ b/seahub/api2/endpoints/repo_commit_revert.py
@@ -0,0 +1,70 @@
+# Copyright (c) 2012-2019 Seafile Ltd.
+# encoding: utf-8
+
+import logging
+
+from django.utils.translation import ugettext as _
+from rest_framework.authentication import SessionAuthentication
+from rest_framework.permissions import IsAuthenticated
+from rest_framework.response import Response
+from rest_framework.views import APIView
+from rest_framework import status
+
+from seahub.api2.throttling import UserRateThrottle
+from seahub.api2.authentication import TokenAuthentication
+from seahub.api2.utils import api_error
+from seahub.views import check_folder_permission
+from seaserv import seafile_api
+from seahub.utils.repo import is_repo_owner
+from seahub.constants import PERMISSION_READ_WRITE
+
+logger = logging.getLogger(__name__)
+
+
+class RepoCommitRevertView(APIView):
+ authentication_classes = (TokenAuthentication, SessionAuthentication)
+ permission_classes = (IsAuthenticated,)
+ throttle_classes = (UserRateThrottle,)
+
+ def post(self, request, repo_id, commit_id, format=None):
+ """ revert commit in repo history
+
+ Permission checking:
+ 1. only repo owner can perform this action.
+ """
+ username = request.user.username
+
+ # resource check
+ repo = seafile_api.get_repo(repo_id)
+ if not repo:
+ error_msg = 'Library %s not found.' % repo_id
+ return api_error(status.HTTP_404_NOT_FOUND, error_msg)
+
+ commit = seafile_api.get_commit(repo.id, repo.version, commit_id)
+ if not commit:
+ error_msg = 'Commit %s not found.' % commit_id
+ return api_error(status.HTTP_404_NOT_FOUND, error_msg)
+
+ # permission check
+ if not is_repo_owner(request, repo_id, username) or \
+ check_folder_permission(request, repo_id, '/') != PERMISSION_READ_WRITE:
+ error_msg = 'Permission denied.'
+ return api_error(status.HTTP_403_FORBIDDEN, error_msg)
+
+ # main
+ if repo.encrypted:
+ ret = seafile_api.is_password_set(repo_id, username)
+ is_decrypted = False if ret == 0 else True
+
+ if not is_decrypted:
+ error_msg = _('This library has not been decrypted.')
+ return api_error(status.HTTP_403_FORBIDDEN, error_msg)
+
+ try:
+ seafile_api.revert_repo(repo_id, commit_id, username)
+ except Exception as e:
+ logger.error(e)
+ error_msg = 'Internal Server Error'
+ return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
+
+ return Response({'success': True})
diff --git a/seahub/api2/endpoints/repos_batch.py b/seahub/api2/endpoints/repos_batch.py
index 72b444b39b..dae4fed4c7 100644
--- a/seahub/api2/endpoints/repos_batch.py
+++ b/seahub/api2/endpoints/repos_batch.py
@@ -29,7 +29,7 @@ from seahub.utils import is_org_context, send_perm_audit_msg, \
normalize_dir_path, get_folder_permission_recursively, \
normalize_file_path, check_filename_with_rename
from seahub.utils.repo import get_repo_owner, get_available_repo_perms, \
- parse_repo_perm
+ parse_repo_perm, get_locked_files_by_dir
from seahub.views import check_folder_permission
from seahub.settings import MAX_PATH
@@ -1244,9 +1244,18 @@ class ReposAsyncBatchMoveItemView(APIView):
error_msg = 'Permission denied.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
- result = {}
+ # check locked files
username = request.user.username
+ locked_files = get_locked_files_by_dir(request, src_repo_id, src_parent_dir)
+ for dirent in src_dirents:
+ # file is locked and lock owner is not current user
+ if dirent in locked_files.keys() and \
+ locked_files[dirent] != username:
+ error_msg = _(u'File %s is locked.') % dirent
+ return api_error(status.HTTP_403_FORBIDDEN, error_msg)
+ # move file
+ result = {}
formated_src_dirents = [dirent.strip('/') for dirent in src_dirents]
src_multi = "\t".join(formated_src_dirents)
dst_multi = "\t".join(formated_src_dirents)
@@ -1445,9 +1454,18 @@ class ReposSyncBatchMoveItemView(APIView):
error_msg = 'Permission denied.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
- result = {}
+ # check locked files
username = request.user.username
+ locked_files = get_locked_files_by_dir(request, src_repo_id, src_parent_dir)
+ for dirent in src_dirents:
+ # file is locked and lock owner is not current user
+ if dirent in locked_files.keys() and \
+ locked_files[dirent] != username:
+ error_msg = _(u'File %s is locked.') % dirent
+ return api_error(status.HTTP_403_FORBIDDEN, error_msg)
+ # move file
+ result = {}
formated_src_dirents = [dirent.strip('/') for dirent in src_dirents]
src_multi = "\t".join(formated_src_dirents)
dst_multi = "\t".join(formated_src_dirents)
diff --git a/seahub/handlers.py b/seahub/handlers.py
index c327179ac1..6cca286d96 100644
--- a/seahub/handlers.py
+++ b/seahub/handlers.py
@@ -17,16 +17,17 @@ try:
repo_id = kwargs['repo_id']
repo_name = kwargs['repo_name']
-
# Move here to avoid model import during Django setup.
- # TODO: Don't register signal/hanlders during Seahub start.
+ # TODO: Don't register signal/handlers during Seahub start.
+
if org_id > 0:
related_users = seafile_api.org_get_shared_users_by_repo(org_id, repo_id)
else:
related_users = seafile_api.get_shared_users_by_repo(repo_id)
org_id = -1
- related_users.append(creator)
+ if creator not in related_users:
+ related_users.append(creator)
record = {
'op_type':'create',
@@ -78,7 +79,8 @@ try:
related_users = seafile_api.get_shared_users_by_repo(repo_id)
org_id = -1
- related_users.append(repo_owner)
+ if repo_owner not in related_users:
+ related_users.append(repo_owner)
record = {
'op_type':'delete',
@@ -113,7 +115,9 @@ try:
related_users = seafile_api.get_shared_users_by_repo(repo_id)
org_id = -1
- related_users.append(repo_owner)
+ if repo_owner not in related_users:
+ related_users.append(repo_owner)
+
record = {
'op_type':'clean-up-trash',
'obj_type':'repo',
@@ -144,7 +148,8 @@ try:
related_users = seafile_api.get_shared_users_by_repo(repo_id)
repo_owner = seafile_api.get_repo_owner(repo_id)
- related_users.append(repo_owner)
+ if repo_owner not in related_users:
+ related_users.append(repo_owner)
record = {
'op_type':'recover',
@@ -154,7 +159,7 @@ try:
'repo_name': repo.repo_name,
'path': '/',
'op_user': operator,
- 'related_users': [related_users],
+ 'related_users': related_users,
'org_id': org_id,
}
diff --git a/seahub/notifications/management/commands/send_work_weixin_notifications.py b/seahub/notifications/management/commands/send_work_weixin_notifications.py
new file mode 100644
index 0000000000..6b1bb091ba
--- /dev/null
+++ b/seahub/notifications/management/commands/send_work_weixin_notifications.py
@@ -0,0 +1,183 @@
+# Copyright (c) 2012-2019 Seafile Ltd.
+# encoding: utf-8
+from datetime import datetime
+import logging
+import re
+import requests
+
+from django.core.management.base import BaseCommand
+from django.core.urlresolvers import reverse
+from django.utils import translation
+from django.utils.translation import ungettext
+
+from seahub.base.models import CommandsLastCheck
+from seahub.notifications.models import UserNotification
+from seahub.utils import get_site_scheme_and_netloc, get_site_name
+from seahub.auth.models import SocialAuthUser
+from seahub.work_weixin.utils import work_weixin_notifications_check, \
+ get_work_weixin_access_token, handler_work_weixin_api_response
+from seahub.work_weixin.settings import WORK_WEIXIN_NOTIFICATIONS_URL, \
+ WORK_WEIXIN_PROVIDER, WORK_WEIXIN_UID_PREFIX, WORK_WEIXIN_AGENT_ID
+
+# Get an instance of a logger
+logger = logging.getLogger(__name__)
+
+
+# https://work.weixin.qq.com/api/doc#90000/90135/90236/
+# from social_django.models import UserSocialAuth
+
+########## Utility Functions ##########
+def wrap_div(s):
+ """
+ Replace
xx to xx and wrap content with
.
+ """
+ patt = '
(.+?)'
+
+ def repl(matchobj):
+ return matchobj.group(1)
+
+ return '' + re.sub(patt, repl, s) + '
'
+
+
+class CommandLogMixin(object):
+ def println(self, msg):
+ self.stdout.write('[%s] %s\n' % (str(datetime.now()), msg))
+
+ def log_error(self, msg):
+ logger.error(msg)
+ self.println(msg)
+
+ def log_info(self, msg):
+ logger.info(msg)
+ self.println(msg)
+
+ def log_debug(self, msg):
+ logger.debug(msg)
+ self.println(msg)
+
+
+#######################################
+
+class Command(BaseCommand, CommandLogMixin):
+ """ send work weixin notifications
+ """
+
+ help = 'Send WeChat Work msg to user if he/she has unseen notices every '
+ 'period of time.'
+ label = "notifications_send_wxwork_notices"
+
+ def handle(self, *args, **options):
+ self.log_debug('Start sending work weixin msg...')
+ self.do_action()
+ self.log_debug('Finish sending work weixin msg.\n')
+
+ def send_work_weixin_msg(self, uid, title, content):
+
+ self.log_info('Send wechat msg to user: %s, msg: %s' % (uid, content))
+
+ data = {
+ "touser": uid,
+ "agentid": WORK_WEIXIN_AGENT_ID,
+ 'msgtype': 'textcard',
+ 'textcard': {
+ 'title': title,
+ 'description': content,
+ 'url': self.detail_url,
+ },
+ }
+
+ api_response = requests.post(self.work_weixin_notifications_url, json=data)
+ api_response_dic = handler_work_weixin_api_response(api_response)
+ if api_response_dic:
+ self.log_info(api_response_dic)
+ else:
+ self.log_error('can not get work weixin notifications API response')
+
+ def do_action(self):
+ # check before start
+ if not work_weixin_notifications_check():
+ self.log_error('work weixin notifications settings check failed')
+ return
+
+ access_token = get_work_weixin_access_token()
+ if not access_token:
+ self.log_error('can not get access_token')
+
+ self.work_weixin_notifications_url = WORK_WEIXIN_NOTIFICATIONS_URL + '?access_token=' + access_token
+ self.detail_url = get_site_scheme_and_netloc().rstrip('/') + reverse('user_notification_list')
+ site_name = get_site_name()
+
+ # start
+ now = datetime.now()
+ today = datetime.now().replace(hour=0).replace(minute=0).replace(
+ second=0).replace(microsecond=0)
+
+ # 1. get all users who are connected work weixin
+ socials = SocialAuthUser.objects.filter(provider=WORK_WEIXIN_PROVIDER, uid__contains=WORK_WEIXIN_UID_PREFIX)
+ users = [(x.username, x.uid[len(WORK_WEIXIN_UID_PREFIX):]) for x in socials]
+ self.log_info('Found %d users' % len(users))
+ if not users:
+ return
+
+ user_uid_map = {}
+ for username, uid in users:
+ user_uid_map[username] = uid
+
+ # 2. get previous time that command last runs
+ try:
+ cmd_last_check = CommandsLastCheck.objects.get(command_type=self.label)
+ self.log_debug('Last check time is %s' % cmd_last_check.last_check)
+
+ last_check_dt = cmd_last_check.last_check
+
+ cmd_last_check.last_check = now
+ cmd_last_check.save()
+ except CommandsLastCheck.DoesNotExist:
+ last_check_dt = today
+ self.log_debug('Create new last check time: %s' % now)
+ CommandsLastCheck(command_type=self.label, last_check=now).save()
+
+ # 3. get all unseen notices for those users
+ qs = UserNotification.objects.filter(
+ timestamp__gt=last_check_dt
+ ).filter(seen=False).filter(
+ to_user__in=user_uid_map.keys()
+ )
+ self.log_info('Found %d notices' % qs.count())
+ if qs.count() == 0:
+ return
+
+ user_notices = {}
+ for q in qs:
+ if q.to_user not in user_notices:
+ user_notices[q.to_user] = [q]
+ else:
+ user_notices[q.to_user].append(q)
+
+ # save current language
+ cur_language = translation.get_language()
+ # active zh-cn
+ translation.activate('zh-cn')
+ self.log_info('the language is set to zh-cn')
+
+ # 4. send msg to users
+ for username, uid in users:
+ notices = user_notices.get(username, [])
+ count = len(notices)
+ if count == 0:
+ continue
+
+ title = ungettext(
+ "\n"
+ "You've got 1 new notice on %(site_name)s:\n",
+ "\n"
+ "You've got %(num)s new notices on %(site_name)s:\n",
+ count
+ ) % {'num': count, 'site_name': site_name, }
+
+ content = ''.join([wrap_div(x.format_msg()) for x in notices])
+ self.send_work_weixin_msg(uid, title, content)
+
+ # reset language
+ translation.activate(cur_language)
+ self.log_info('reset language success')
diff --git a/seahub/notifications/management/commands/send_wxwork_notices.py b/seahub/notifications/management/commands/send_wxwork_notices.py
index c6e6125086..537f8660fc 100644
--- a/seahub/notifications/management/commands/send_wxwork_notices.py
+++ b/seahub/notifications/management/commands/send_wxwork_notices.py
@@ -51,6 +51,9 @@ class CommandLogMixin(object):
#######################################
class Command(BaseCommand, CommandLogMixin):
+ """ please use send_work_weixin_notifications.py
+ """
+
help = 'Send WeChat Work msg to user if he/she has unseen notices every '
'period of time.'
label = "notifications_send_wxwork_notices"
diff --git a/seahub/settings.py b/seahub/settings.py
index 3ad42873f0..206511865d 100644
--- a/seahub/settings.py
+++ b/seahub/settings.py
@@ -306,6 +306,8 @@ ENABLE_WATERMARK = False
ENABLE_WORK_WEIXIN_OAUTH = False
# allow seafile admin import user from work weixin
ENABLE_WORK_WEIXIN_DEPARTMENTS = False
+# allow send unread msg to work weixin
+ENABLE_WORK_WEIXIN_NOTIFICATIONS = False
# allow user to clean library trash
ENABLE_USER_CLEAN_TRASH = True
diff --git a/seahub/templates/repo_snapshot_react.html b/seahub/templates/repo_snapshot_react.html
new file mode 100644
index 0000000000..ffd22535a1
--- /dev/null
+++ b/seahub/templates/repo_snapshot_react.html
@@ -0,0 +1,34 @@
+{% extends 'base_for_react.html' %}
+{% load seahub_tags i18n avatar_tags %}
+{% load render_bundle from webpack_loader %}
+
+{% block sub_title %}{% trans "Snapshot" %} - {% endblock %}
+
+{% block extra_style %}
+{% render_bundle 'repoSnapshot' 'css' %}
+{% endblock %}
+
+{% block extra_script %}
+
+{% render_bundle 'repoSnapshot' 'js' %}
+{% endblock %}
diff --git a/seahub/urls.py b/seahub/urls.py
index 106ee0f328..ba519177db 100644
--- a/seahub/urls.py
+++ b/seahub/urls.py
@@ -14,7 +14,7 @@ from seahub.views.file import view_history_file, view_trash_file,\
text_diff, view_raw_file, download_file, view_lib_file, \
file_access, view_lib_file_via_smart_link, view_media_file_via_share_link, \
view_media_file_via_public_wiki
-from seahub.views.repo import repo_history_view, view_shared_dir, \
+from seahub.views.repo import repo_history_view, repo_snapshot, view_shared_dir, \
view_shared_upload_link, view_lib_as_wiki
from notifications.views import notification_list
from seahub.views.wiki import personal_wiki, personal_wiki_pages, \
@@ -55,6 +55,7 @@ from seahub.api2.endpoints.file_tag import FileTagView
from seahub.api2.endpoints.file_tag import FileTagsView
from seahub.api2.endpoints.repo_trash import RepoTrash
from seahub.api2.endpoints.repo_commit_dir import RepoCommitDirView
+from seahub.api2.endpoints.repo_commit_revert import RepoCommitRevertView
from seahub.api2.endpoints.deleted_repos import DeletedRepos
from seahub.api2.endpoints.repo_history import RepoHistory
from seahub.api2.endpoints.repo_set_password import RepoSetPassword
@@ -172,6 +173,7 @@ urlpatterns = [
url(r'^repo/text_diff/(?P[-0-9a-f]{36})/$', text_diff, name='text_diff'),
url(r'^repo/history/(?P[-0-9a-f]{36})/$', repo_history, name='repo_history'),
url(r'^repo/history/view/(?P[-0-9a-f]{36})/$', repo_history_view, name='repo_history_view'),
+ url(r'^repo/(?P[-0-9a-f]{36})/snapshot/$', repo_snapshot, name="repo_snapshot"),
url(r'^repo/recycle/(?P[-0-9a-f]{36})/$', repo_recycle_view, name='repo_recycle_view'),
url(r'^dir/recycle/(?P[-0-9a-f]{36})/$', dir_recycle_view, name='dir_recycle_view'),
url(r'^repo/(?P[-0-9a-f]{36})/trash/$', repo_folder_trash, name="repo_folder_trash"),
@@ -336,6 +338,7 @@ urlpatterns = [
url(r'^api/v2.1/repos/(?P[-0-9a-f]{36})/file/new_history/$', NewFileHistoryView.as_view(), name='api-v2.1-new-file-history-view'),
url(r'^api/v2.1/repos/(?P[-0-9a-f]{36})/dir/$', DirView.as_view(), name='api-v2.1-dir-view'),
url(r'^api/v2.1/repos/(?P[-0-9a-f]{36})/commits/(?P[0-9a-f]{40})/dir/$', RepoCommitDirView.as_view(), name='api-v2.1-repo-commit-dir'),
+ url(r'^api/v2.1/repos/(?P[-0-9a-f]{36})/commits/(?P[0-9a-f]{40})/revert/$', RepoCommitRevertView.as_view(), name='api-v2.1-repo-commit-revert'),
url(r'^api/v2.1/repos/(?P[-0-9a-f]{36})/dir/detail/$', DirDetailView.as_view(), name='api-v2.1-dir-detail-view'),
url(r'^api/v2.1/repos/(?P[-0-9a-f]{36})/trash/$', RepoTrash.as_view(), name='api-v2.1-repo-trash'),
url(r'^api/v2.1/repos/(?P[-0-9a-f]{36})/history/$', RepoHistory.as_view(), name='api-v2.1-repo-history'),
diff --git a/seahub/views/repo.py b/seahub/views/repo.py
index e7908c6531..027cc54ad3 100644
--- a/seahub/views/repo.py
+++ b/seahub/views/repo.py
@@ -153,6 +153,50 @@ def repo_history_view(request, repo_id):
'referer': referer,
})
+@login_required
+def repo_snapshot(request, repo_id):
+ """View repo in history.
+ """
+ repo = get_repo(repo_id)
+ if not repo:
+ raise Http404
+
+ username = request.user.username
+ user_perm = check_folder_permission(request, repo.id, '/')
+ if user_perm is None:
+ return render_error(request, _(u'Permission denied'))
+
+ try:
+ server_crypto = UserOptions.objects.is_server_crypto(username)
+ except CryptoOptionNotSetError:
+ # Assume server_crypto is ``False`` if this option is not set.
+ server_crypto = False
+
+ reverse_url = reverse('lib_view', args=[repo_id, repo.name, ''])
+ if repo.encrypted and \
+ (repo.enc_version == 1 or (repo.enc_version == 2 and server_crypto)) \
+ and not is_password_set(repo.id, username):
+ return render(request, 'decrypt_repo_form.html', {
+ 'repo': repo,
+ 'next': get_next_url_from_request(request) or reverse_url,
+ })
+
+ commit_id = request.GET.get('commit_id', None)
+ if commit_id is None:
+ return HttpResponseRedirect(reverse_url)
+ current_commit = get_commit(repo.id, repo.version, commit_id)
+ if not current_commit:
+ current_commit = get_commit(repo.id, repo.version, repo.head_cmmt_id)
+
+ repo_owner = seafile_api.get_repo_owner(repo.id)
+ is_repo_owner = True if username == repo_owner else False
+
+ return render(request, 'repo_snapshot_react.html', {
+ 'repo': repo,
+ "is_repo_owner": is_repo_owner,
+ 'current_commit': current_commit,
+ })
+
@login_required
def view_lib_as_wiki(request, repo_id, path):
diff --git a/seahub/wopi/views.py b/seahub/wopi/views.py
index b42c482ff5..e8692aad09 100644
--- a/seahub/wopi/views.py
+++ b/seahub/wopi/views.py
@@ -213,7 +213,7 @@ class WOPIFilesView(APIView):
# new file creation feature is not implemented on wopi host(seahub)
# hide save as button on view/edit file page
- result['UserCanNotWriteRelative'] = False
+ result['UserCanNotWriteRelative'] = True
return HttpResponse(json.dumps(result), status=200,
content_type=json_content_type)
diff --git a/seahub/work_weixin/settings.py b/seahub/work_weixin/settings.py
index 3a5045f8d9..f3623a8e3a 100644
--- a/seahub/work_weixin/settings.py
+++ b/seahub/work_weixin/settings.py
@@ -19,7 +19,7 @@ WORK_WEIXIN_DEPARTMENT_MEMBERS_URL = getattr(settings, 'WORK_WEIXIN_DEPARTMENT_M
WORK_WEIXIN_AGENT_ID = getattr(settings, 'WORK_WEIXIN_AGENT_ID', '')
ENABLE_WORK_WEIXIN_OAUTH = getattr(settings, 'ENABLE_WORK_WEIXIN_OAUTH', False)
WORK_WEIXIN_UID_PREFIX = WORK_WEIXIN_CORP_ID + '_'
-AUTO_UPDATE_WORK_WEIXIN_USER_INFO = getattr(settings, 'AUTO_UPDATE_WORK_WEIXIN_USER_INFO', False)
+WORK_WEIXIN_USER_INFO_AUTO_UPDATE = getattr(settings, 'WORK_WEIXIN_USER_INFO_AUTO_UPDATE', True)
WORK_WEIXIN_AUTHORIZATION_URL = getattr(settings, 'WORK_WEIXIN_AUTHORIZATION_URL',
'https://open.work.weixin.qq.com/wwopen/sso/qrConnect')
WORK_WEIXIN_GET_USER_INFO_URL = getattr(settings, 'WORK_WEIXIN_GET_USER_INFO_URL',
@@ -27,6 +27,11 @@ WORK_WEIXIN_GET_USER_INFO_URL = getattr(settings, 'WORK_WEIXIN_GET_USER_INFO_URL
WORK_WEIXIN_GET_USER_PROFILE_URL = getattr(settings, 'WORK_WEIXIN_GET_USER_PROFILE_URL',
'https://qyapi.weixin.qq.com/cgi-bin/user/get')
+# # work weixin notifications
+ENABLE_WORK_WEIXIN_NOTIFICATIONS = getattr(settings, 'ENABLE_WORK_WEIXIN_NOTIFICATIONS', False)
+WORK_WEIXIN_NOTIFICATIONS_URL = getattr(settings, 'WORK_WEIXIN_NOTIFICATIONS_URL',
+ 'https://qyapi.weixin.qq.com/cgi-bin/message/send')
+
# # constants
WORK_WEIXIN_PROVIDER = 'work-weixin'
diff --git a/seahub/work_weixin/utils.py b/seahub/work_weixin/utils.py
index c2e8a59dd2..feb27c9258 100644
--- a/seahub/work_weixin/utils.py
+++ b/seahub/work_weixin/utils.py
@@ -11,7 +11,8 @@ from seahub.work_weixin.settings import WORK_WEIXIN_CORP_ID, WORK_WEIXIN_AGENT_S
WORK_WEIXIN_ACCESS_TOKEN_URL, ENABLE_WORK_WEIXIN_DEPARTMENTS, \
WORK_WEIXIN_DEPARTMENTS_URL, WORK_WEIXIN_DEPARTMENT_MEMBERS_URL, \
ENABLE_WORK_WEIXIN_OAUTH, WORK_WEIXIN_AGENT_ID, WORK_WEIXIN_AUTHORIZATION_URL, \
- WORK_WEIXIN_GET_USER_INFO_URL, WORK_WEIXIN_GET_USER_PROFILE_URL
+ WORK_WEIXIN_GET_USER_INFO_URL, WORK_WEIXIN_GET_USER_PROFILE_URL, \
+ ENABLE_WORK_WEIXIN_NOTIFICATIONS, WORK_WEIXIN_NOTIFICATIONS_URL
from seahub.profile.models import Profile
logger = logging.getLogger(__name__)
@@ -23,6 +24,8 @@ WORK_WEIXIN_ACCESS_TOKEN_CACHE_KEY = 'WORK_WEIXIN_ACCESS_TOKEN'
def get_work_weixin_access_token():
+ """ get global work weixin access_token
+ """
cache_key = normalize_cache_key(WORK_WEIXIN_ACCESS_TOKEN_CACHE_KEY)
access_token = cache.get(cache_key, None)
@@ -45,6 +48,8 @@ def get_work_weixin_access_token():
def handler_work_weixin_api_response(response):
+ """ handler work_weixin response and errcode
+ """
try:
response = response.json()
except ValueError:
@@ -59,6 +64,8 @@ def handler_work_weixin_api_response(response):
def work_weixin_base_check():
+ """ work weixin base check
+ """
if not WORK_WEIXIN_CORP_ID or not WORK_WEIXIN_AGENT_SECRET or not WORK_WEIXIN_ACCESS_TOKEN_URL:
logger.error('work weixin base relevant settings invalid.')
logger.error('WORK_WEIXIN_CORP_ID: %s' % WORK_WEIXIN_CORP_ID)
@@ -69,13 +76,14 @@ def work_weixin_base_check():
def work_weixin_oauth_check():
- if not work_weixin_base_check():
- return False
-
+ """ use for work weixin login and profile bind
+ """
if not ENABLE_WORK_WEIXIN_OAUTH:
- logger.error('work weixin oauth not enabled.')
return False
else:
+ if not work_weixin_base_check():
+ return False
+
if not WORK_WEIXIN_AGENT_ID \
or not WORK_WEIXIN_GET_USER_INFO_URL \
or not WORK_WEIXIN_AUTHORIZATION_URL \
@@ -91,13 +99,14 @@ def work_weixin_oauth_check():
def admin_work_weixin_departments_check():
- if not work_weixin_base_check():
- return False
-
+ """ use for admin work weixin departments
+ """
if not ENABLE_WORK_WEIXIN_DEPARTMENTS:
- logger.error('admin work weixin departments not enabled.')
return False
else:
+ if not work_weixin_base_check():
+ return False
+
if not WORK_WEIXIN_DEPARTMENTS_URL \
or not WORK_WEIXIN_DEPARTMENT_MEMBERS_URL:
logger.error('admin work weixin departments relevant settings invalid.')
@@ -108,12 +117,48 @@ def admin_work_weixin_departments_check():
return True
+def work_weixin_notifications_check():
+ """ use for send work weixin notifications
+ """
+ if not ENABLE_WORK_WEIXIN_NOTIFICATIONS:
+ return False
+ else:
+ if not work_weixin_base_check():
+ return False
+
+ if not WORK_WEIXIN_AGENT_ID \
+ or not WORK_WEIXIN_NOTIFICATIONS_URL:
+ logger.error('work weixin notifications relevant settings invalid.')
+ logger.error('WORK_WEIXIN_AGENT_ID: %s' % WORK_WEIXIN_AGENT_ID)
+ logger.error('WORK_WEIXIN_NOTIFICATIONS_URL: %s' % WORK_WEIXIN_NOTIFICATIONS_URL)
+ return False
+
+ return True
+
+
def update_work_weixin_user_info(api_user):
- email = api_user.get('username')
- try:
- # update additional user info
- nickname = api_user.get("name", None)
- if nickname is not None:
- Profile.objects.add_or_update(email, nickname)
- except Exception as e:
- logger.error(e)
+ """ update user profile from work weixin
+
+ use for work weixin departments, login, profile bind
+ """
+ # update additional user info
+ username = api_user.get('username')
+ nickname = api_user.get('name')
+ contact_email = api_user.get('contact_email')
+
+ # make sure the contact_email is unique
+ if contact_email and Profile.objects.get_profile_by_contact_email(contact_email):
+ logger.warning('contact email %s already exists' % contact_email)
+ contact_email = ''
+
+ profile_kwargs = {}
+ if nickname:
+ profile_kwargs['nickname'] = nickname
+ if contact_email:
+ profile_kwargs['contact_email'] = contact_email
+
+ if profile_kwargs:
+ try:
+ Profile.objects.add_or_update(username, **profile_kwargs)
+ except Exception as e:
+ logger.error(e)
diff --git a/seahub/work_weixin/views.py b/seahub/work_weixin/views.py
index eb24c39248..133fded218 100644
--- a/seahub/work_weixin/views.py
+++ b/seahub/work_weixin/views.py
@@ -17,7 +17,7 @@ from seahub.base.accounts import User
from seahub.work_weixin.settings import WORK_WEIXIN_AUTHORIZATION_URL, WORK_WEIXIN_CORP_ID, \
WORK_WEIXIN_AGENT_ID, WORK_WEIXIN_PROVIDER, \
WORK_WEIXIN_GET_USER_INFO_URL, WORK_WEIXIN_GET_USER_PROFILE_URL, WORK_WEIXIN_UID_PREFIX, \
- AUTO_UPDATE_WORK_WEIXIN_USER_INFO
+ WORK_WEIXIN_USER_INFO_AUTO_UPDATE
from seahub.work_weixin.utils import work_weixin_oauth_check, get_work_weixin_access_token, \
handler_work_weixin_api_response, update_work_weixin_user_info
from seahub.utils.auth import gen_user_virtual_id, VIRTUAL_ID_EMAIL_DOMAIN
@@ -100,8 +100,8 @@ def work_weixin_oauth_callback(request):
return render_error(
request, _('Error, new user registration is not allowed, please contact administrator.'))
- if is_new_user or AUTO_UPDATE_WORK_WEIXIN_USER_INFO:
- # update user info
+ # update user info
+ if is_new_user or WORK_WEIXIN_USER_INFO_AUTO_UPDATE:
user_info_data = {
'access_token': access_token,
'userid': user_id,
@@ -193,8 +193,8 @@ def work_weixin_oauth_connect_callback(request):
SocialAuthUser.objects.add(email, WORK_WEIXIN_PROVIDER, uid)
- if AUTO_UPDATE_WORK_WEIXIN_USER_INFO:
- # update user info
+ # update user info
+ if WORK_WEIXIN_USER_INFO_AUTO_UPDATE:
user_info_data = {
'access_token': access_token,
'userid': user_id,
diff --git a/tests/api/endpoints/test_repo_commit_revert.py b/tests/api/endpoints/test_repo_commit_revert.py
new file mode 100644
index 0000000000..9426f27daf
--- /dev/null
+++ b/tests/api/endpoints/test_repo_commit_revert.py
@@ -0,0 +1,111 @@
+import os
+import json
+
+from django.core.urlresolvers import reverse
+
+from seaserv import seafile_api
+from seahub.test_utils import BaseTestCase
+
+
+class RepoCommitRevertTest(BaseTestCase):
+
+ def setUp(self):
+ self.user_name = self.user.username
+ self.admin_name = self.admin.username
+
+ self.repo_id = self.repo.id
+ self.repo_name = self.repo.repo_name
+
+ self.file_path = self.file
+ self.file_name = os.path.basename(self.file_path)
+
+ self.enc_repo_id = self.enc_repo.id
+ self.enc_repo_name = self.enc_repo
+
+ self.folder_path = self.folder
+ self.folder_name = os.path.basename(self.folder.rstrip('/'))
+
+ def tearDown(self):
+ self.remove_repo()
+ self.remove_group()
+
+ def test_post(self):
+ # delete a file first
+ seafile_api.del_file(self.repo_id, '/', self.file_name, self.user_name)
+
+ self.login_as(self.user)
+
+ # get commit id form trash
+ trash_url = reverse('api-v2.1-repo-trash', args=[self.repo_id])
+ trash_resp = self.client.get(trash_url)
+ self.assertEqual(200, trash_resp.status_code)
+ trash_json_resp = json.loads(trash_resp.content)
+ assert trash_json_resp['data'][0]['obj_name'] == self.file_name
+ assert not trash_json_resp['data'][0]['is_dir']
+ assert trash_json_resp['data'][0]['commit_id']
+ commit_id = trash_json_resp['data'][0]['commit_id']
+
+ dir_url = reverse('api-v2.1-dir-view', args=[self.repo_id])
+ url = reverse('api-v2.1-repo-commit-revert', args=[self.repo_id, commit_id])
+
+ # test can post
+ resp = self.client.get(dir_url)
+ self.assertEqual(200, resp.status_code)
+ json_resp = json.loads(resp.content)
+ assert len(json_resp['dirent_list']) == 1
+
+ resp = self.client.post(url)
+ self.assertEqual(200, resp.status_code)
+
+ resp = self.client.get(dir_url)
+ self.assertEqual(200, resp.status_code)
+ json_resp = json.loads(resp.content)
+ assert len(json_resp['dirent_list']) == 2
+
+ # test_can_not_post_with_invalid_repo_permission
+ self.logout()
+ self.login_as(self.admin)
+
+ resp = self.client.post(url)
+ self.assertEqual(403, resp.status_code)
+
+ def test_enc_repo_post(self):
+ # delete a file first
+ seafile_api.del_file(self.enc_repo_id, '/', self.file_name, self.user_name)
+
+ self.login_as(self.user)
+
+ # get commit id form trash
+ trash_url = reverse('api-v2.1-repo-trash', args=[self.enc_repo_id])
+ trash_resp = self.client.get(trash_url)
+ self.assertEqual(200, trash_resp.status_code)
+ trash_json_resp = json.loads(trash_resp.content)
+ assert trash_json_resp['data'][0]['obj_name'] == self.file_name
+ assert not trash_json_resp['data'][0]['is_dir']
+ assert trash_json_resp['data'][0]['commit_id']
+ commit_id = trash_json_resp['data'][0]['commit_id']
+
+ dir_url = reverse('api-v2.1-dir-view', args=[self.enc_repo_id])
+ url = reverse('api-v2.1-repo-commit-revert', args=[self.enc_repo_id, commit_id])
+
+ # test can not post without repo decrypted
+ resp = self.client.post(url)
+ self.assertEqual(403, resp.status_code)
+
+ resp = self.client.get(dir_url)
+ self.assertEqual(200, resp.status_code)
+ json_resp = json.loads(resp.content)
+ assert len(json_resp['dirent_list']) == 0
+
+ # test can post with repo decrypted
+ decrypted_url = reverse('api-v2.1-repo-set-password', args=[self.enc_repo_id])
+ resp = self.client.post(decrypted_url, data={'password': '123'})
+ self.assertEqual(200, resp.status_code)
+
+ resp = self.client.post(url)
+ self.assertEqual(200, resp.status_code)
+
+ resp = self.client.get(dir_url)
+ self.assertEqual(200, resp.status_code)
+ json_resp = json.loads(resp.content)
+ assert len(json_resp['dirent_list']) == 1
diff --git a/tests/api/endpoints/test_repos_batch.py b/tests/api/endpoints/test_repos_batch.py
index 489da5a48f..10036efab2 100644
--- a/tests/api/endpoints/test_repos_batch.py
+++ b/tests/api/endpoints/test_repos_batch.py
@@ -994,6 +994,36 @@ class ReposAsyncBatchMoveItemView(BaseTestCase):
resp = self.client.post(self.url, json.dumps(data), 'application/json')
self.assertEqual(403, resp.status_code)
+ def test_move_with_locked_file(self):
+
+ if not LOCAL_PRO_DEV_ENV:
+ return
+
+ self.login_as(self.user)
+
+ # share admin's tmp repo to user with 'r' permission
+ admin_repo_id = self.create_new_repo(self.admin_name)
+ seafile_api.share_repo(admin_repo_id, self.admin_name,
+ self.user_name, 'rw')
+
+ # admin lock file
+ admin_file_name = randstring(6)
+ seafile_api.post_empty_file(admin_repo_id, '/', admin_file_name,
+ self.admin_name)
+ seafile_api.lock_file(admin_repo_id, admin_file_name, self.admin_name, 0)
+
+ # user move locked file
+ data = {
+ "src_repo_id": admin_repo_id,
+ "src_parent_dir": '/',
+ "src_dirents":[admin_file_name],
+ "dst_repo_id": self.dst_repo_id,
+ "dst_parent_dir": '/',
+ }
+ resp = self.client.post(self.url, json.dumps(data), 'application/json')
+ self.assertEqual(403, resp.status_code)
+ json_resp = json.loads(resp.content)
+ assert json_resp['error_msg'] == 'File %s is locked.' % admin_file_name
class ReposSyncBatchCopyItemView(BaseTestCase):
@@ -1539,3 +1569,34 @@ class ReposSyncBatchMoveItemView(BaseTestCase):
}
resp = self.client.post(self.url, json.dumps(data), 'application/json')
self.assertEqual(403, resp.status_code)
+
+ def test_move_with_locked_file(self):
+
+ if not LOCAL_PRO_DEV_ENV:
+ return
+
+ self.login_as(self.user)
+
+ # share admin's tmp repo to user with 'r' permission
+ admin_repo_id = self.create_new_repo(self.admin_name)
+ seafile_api.share_repo(admin_repo_id, self.admin_name,
+ self.user_name, 'rw')
+
+ # admin lock file
+ admin_file_name = randstring(6)
+ seafile_api.post_empty_file(admin_repo_id, '/', admin_file_name,
+ self.admin_name)
+ seafile_api.lock_file(admin_repo_id, admin_file_name, self.admin_name, 0)
+
+ # user move locked file
+ data = {
+ "src_repo_id": admin_repo_id,
+ "src_parent_dir": '/',
+ "src_dirents":[admin_file_name],
+ "dst_repo_id": self.dst_repo_id,
+ "dst_parent_dir": '/',
+ }
+ resp = self.client.post(self.url, json.dumps(data), 'application/json')
+ self.assertEqual(403, resp.status_code)
+ json_resp = json.loads(resp.content)
+ assert json_resp['error_msg'] == 'File %s is locked.' % admin_file_name