}
+ {enableOCM && itemType === 'library' && this.state.isRepoOwner &&
+
+
+ {gettext('Share to other server')}
+
+
+ }
@@ -190,6 +198,11 @@ class ShareDialog extends React.Component {
}
}
+ {enableOCM && itemType === 'library' && activeTab === 'shareToOtherServer' &&
+
+
+
+ }
diff --git a/frontend/src/components/dialog/share-to-other-server.js b/frontend/src/components/dialog/share-to-other-server.js
new file mode 100644
index 0000000000..b82bb10e3d
--- /dev/null
+++ b/frontend/src/components/dialog/share-to-other-server.js
@@ -0,0 +1,234 @@
+import React, { Fragment } from 'react';
+import PropTypes from 'prop-types';
+import { gettext } from '../../utils/constants';
+import { Input } from 'reactstrap';
+import { Button } from 'reactstrap';
+import { seafileAPI } from '../../utils/seafile-api.js';
+import { Utils } from '../../utils/utils';
+import toaster from '../toast';
+import SharePermissionEditor from '../select-editor/share-permission-editor';
+
+class ShareItem extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ isOperationShow: false
+ };
+ }
+
+ onMouseEnter = () => {
+ this.setState({isOperationShow: true});
+ }
+
+ onMouseLeave = () => {
+ this.setState({isOperationShow: false});
+ }
+
+ deleteShareItem = () => {
+ let item = this.props.item;
+ this.props.deleteShareItem(item);
+ }
+
+ render() {
+ let item = this.props.item;
+ return (
+
+ {item.to_sever_url} |
+ {item.to_user} |
+ {Utils.sharePerms(item.permission)} |
+ {/*
+
+ | */}
+
+
+
+ |
+
+ );
+ }
+}
+
+class ShareList extends React.Component {
+
+ render() {
+ return (
+
+
+
+
+ {gettext('Server URL')} |
+ {gettext('User Email')} |
+ {gettext('Permission')} |
+ |
+
+
+
+ {this.props.items.map((item, index) => {
+ return (
+
+ );
+ })}
+
+
+
+ );
+ }
+}
+
+const propTypes = {
+ isGroupOwnedRepo: PropTypes.bool,
+ itemPath: PropTypes.string.isRequired,
+ itemType: PropTypes.string.isRequired,
+ repoID: PropTypes.string.isRequired,
+ isRepoOwner: PropTypes.bool.isRequired,
+};
+
+class ShareToOtherServer extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ selectedOption: null,
+ errorMsg: [],
+ permission: 'rw',
+ ocmShares: [],
+ toUser: '',
+ toServerURL: '',
+ };
+ this.options = [];
+ this.permissions = ['rw', 'r'];
+ this.UnshareMessage = 'File was unshared';
+
+ }
+
+ handleSelectChange = (option) => {
+ this.setState({selectedOption: option});
+ this.options = [];
+ }
+
+ componentDidMount() {
+ seafileAPI.listOCMSharesPrepare(this.props.repoID).then((res) => {
+ this.setState({ocmShares: res.data.ocm_share_list});
+ }).catch(error => {
+ let errMessage = Utils.getErrorMsg(error);
+ toaster.danger(errMessage);
+ });
+ }
+
+ startOCMShare = () => {
+ let { repoID, itemPath } = this.props;
+ let { toServerURL, toUser, permission } = this.state;
+ if (!toServerURL.endsWith('/')) {
+ toServerURL += '/';
+ }
+ seafileAPI.addOCMSharePrepare(toUser, toServerURL, repoID, itemPath, permission).then((res) => {
+ toaster.success(gettext('share success.'));
+ let ocmShares = this.state.ocmShares;
+ ocmShares.push(res.data);
+ this.setState({ocmShares: ocmShares});
+ }).catch(error => {
+ let errMessage = Utils.getErrorMsg(error);
+ toaster.danger(errMessage);
+ });
+ }
+
+ handleToUserChange = (e) => {
+ this.setState({
+ toUser: e.target.value,
+ });
+ }
+
+ handleURLChange = (e) => {
+ this.setState({
+ toServerURL: e.target.value,
+ });
+ }
+
+ deleteShareItem = (deletedItem) => {
+ let { id } = deletedItem;
+ seafileAPI.deleteOCMSharePrepare(id).then((res) => {
+ toaster.success(gettext('delete success.'));
+ let ocmShares = this.state.ocmShares.filter(item => {
+ return item.id != id;
+ });
+ this.setState({ocmShares: ocmShares});
+ }).catch(error => {
+ let errMessage = Utils.getErrorMsg(error);
+ toaster.danger(errMessage);
+ });
+ }
+
+ setPermission = (permission) => {
+ this.setState({permission: permission});
+ }
+
+
+ render() {
+ let { ocmShares, toUser, toServerURL, permission } = this.state;
+ return (
+
+
+
+
+ );
+ }
+}
+
+ShareToOtherServer.propTypes = propTypes;
+
+export default ShareToOtherServer;
diff --git a/frontend/src/components/main-side-nav.js b/frontend/src/components/main-side-nav.js
index 8727b25be7..dbf3af99fe 100644
--- a/frontend/src/components/main-side-nav.js
+++ b/frontend/src/components/main-side-nav.js
@@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { Link } from '@reach/router';
import { Badge } from 'reactstrap';
-import { gettext, siteRoot, canPublishRepo, canAddRepo, canGenerateShareLink, canGenerateUploadLink, canInvitePeople, dtableWebServer } from '../utils/constants';
+import { gettext, siteRoot, canPublishRepo, canAddRepo, canGenerateShareLink, canGenerateUploadLink, canInvitePeople, dtableWebServer, enableOCM } from '../utils/constants';
import { seafileAPI } from '../utils/seafile-api';
import { Utils } from '../utils/utils';
import toaster from './toast';
@@ -216,6 +216,14 @@ class MainSideNav extends React.Component {
{this.renderSharedGroups()}
+ {enableOCM &&
+
+ this.tabItemClick(e, 'shared-with-ocm')}>
+
+ {gettext('Shared from other servers')}
+
+
+ }
diff --git a/frontend/src/pages/share-with-ocm/remote-dir-content.js b/frontend/src/pages/share-with-ocm/remote-dir-content.js
new file mode 100644
index 0000000000..10c328f839
--- /dev/null
+++ b/frontend/src/pages/share-with-ocm/remote-dir-content.js
@@ -0,0 +1,121 @@
+import React, { Fragment } from 'react';
+import PropTypes from 'prop-types';
+import { Link } from '@reach/router';
+import moment from 'moment';
+import { gettext } from '../../utils/constants';
+import { Utils } from '../../utils/utils';
+import Loading from '../../components/loading';
+
+class DirentItem extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ isOpIconShown: false
+ };
+ }
+
+ handleMouseOver = () => {
+ this.setState({
+ isOpIconShown: true
+ });
+ }
+
+ handleMouseOut = () => {
+ this.setState({
+ isOpIconShown: false
+ });
+ }
+
+ openFolder = () => {
+ this.props.openFolder(this.props.dirent);
+ }
+
+ downloadDirent = (e) => {
+ e.preventDefault();
+ this.props.downloadDirent(this.props.dirent);
+ }
+
+ render () {
+ let { isOpIconShown } = this.state;
+ let { dirent } = this.props;
+ let iconUrl = Utils.getDirentIcon(dirent);
+
+ return (
+
+
+  |
+
+ {dirent.is_file ?
+ dirent.name :
+ {dirent.name}
+ }
+ |
+
+ {isOpIconShown && dirent.is_file &&
+
+ }
+ |
+ {Utils.bytesToSize(dirent.size)} |
+ {moment(dirent.mtime).fromNow()} |
+
+
+ );
+ }
+}
+
+
+const propTypes = {
+ direntList: PropTypes.array.isRequired
+};
+
+class DirContent extends React.Component {
+
+ constructor(props) {
+ super(props);
+ }
+
+ render() {
+ let { loading, errorMsg, direntList } = this.props;
+
+ if (loading) {
+ return ;
+ }
+
+ if (errorMsg) {
+ return {errorMsg}
;
+ }
+
+ return (
+
+
+
+
+ {/*icon*/} |
+ {gettext('Name')} |
+ {/*operation*/} |
+ {gettext('Size')} |
+ {gettext('Last Update')} |
+
+
+
+ {direntList.map((dirent, index) => {
+ return ;
+ })}
+
+
+
+ );
+ }
+}
+
+DirContent.propTypes = propTypes;
+
+export default DirContent;
diff --git a/frontend/src/pages/share-with-ocm/remote-dir-path.js b/frontend/src/pages/share-with-ocm/remote-dir-path.js
new file mode 100644
index 0000000000..6186151961
--- /dev/null
+++ b/frontend/src/pages/share-with-ocm/remote-dir-path.js
@@ -0,0 +1,70 @@
+import React, { Fragment } from 'react';
+import PropTypes from 'prop-types';
+import { Link } from '@reach/router';
+import { siteRoot, gettext } from '../../utils/constants';
+import { Utils } from '../../utils/utils';
+
+const propTypes = {
+ repoName: PropTypes.string.isRequired,
+ currentPath: PropTypes.string.isRequired,
+ onPathClick: PropTypes.func.isRequired,
+ onTabNavClick: PropTypes.func.isRequired,
+ repoID: PropTypes.string.isRequired,
+};
+
+class DirPath extends React.Component {
+
+ onPathClick = (e) => {
+ let path = Utils.getEventData(e, 'path');
+ this.props.onPathClick(path);
+ }
+
+ turnPathToLink = (path) => {
+ path = path[path.length - 1] === '/' ? path.slice(0, path.length - 1) : path;
+ let pathList = path.split('/');
+ let nodePath = '';
+ let pathElem = pathList.map((item, index) => {
+ if (item === '') {
+ return;
+ }
+ if (index === (pathList.length - 1)) {
+ return (
+
+ /
+ {item}
+
+ );
+ } else {
+ nodePath += '/' + item;
+ return (
+
+ /
+ {item}
+
+ );
+ }
+ });
+ return pathElem;
+ }
+
+ render() {
+ let { currentPath, repoName } = this.props;
+ let pathElem = this.turnPathToLink(currentPath);
+
+ return (
+
+
this.props.onTabNavClick('shared-with-ocm')}>{gettext('All')}
+
/
+ {(currentPath === '/' || currentPath === '') ?
+
{repoName}:
+
{repoName}
+ }
+ {pathElem}
+
+ );
+ }
+}
+
+DirPath.propTypes = propTypes;
+
+export default DirPath;
diff --git a/frontend/src/pages/share-with-ocm/remote-dir-topbar.js b/frontend/src/pages/share-with-ocm/remote-dir-topbar.js
new file mode 100644
index 0000000000..c242f57781
--- /dev/null
+++ b/frontend/src/pages/share-with-ocm/remote-dir-topbar.js
@@ -0,0 +1,30 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import Account from '../../components/common/account';
+
+const propTypes = {
+ children: PropTypes.object
+};
+
+class MainPanelTopbar extends Component {
+
+ render() {
+ return (
+
+
+
+
+ {this.props.children}
+
+
+
+
+ );
+ }
+}
+
+MainPanelTopbar.propTypes = propTypes;
+
+export default MainPanelTopbar;
diff --git a/frontend/src/pages/share-with-ocm/remote-dir-view.js b/frontend/src/pages/share-with-ocm/remote-dir-view.js
new file mode 100644
index 0000000000..70a5fe59a5
--- /dev/null
+++ b/frontend/src/pages/share-with-ocm/remote-dir-view.js
@@ -0,0 +1,188 @@
+import React, { Component, Fragment } from 'react';
+import { Button } from 'reactstrap';
+import { post } from 'axios';
+import { Utils } from '../../utils/utils';
+import { seafileAPI } from '../../utils/seafile-api';
+import { loginUrl, siteRoot, gettext } from '../../utils/constants';
+import toaster from '../../components/toast';
+import MainPanelTopbar from './remote-dir-topbar';
+import DirPathBar from './remote-dir-path';
+import DirContent from './remote-dir-content';
+
+class Dirent {
+ constructor(obj) {
+ this.name = obj.name;
+ this.mtime = obj.mtime;
+ this.size = obj.size;
+ this.is_file = obj.type === 'file';
+ }
+
+ isDir() {
+ return !this.is_file;
+ }
+}
+
+class DirView extends Component {
+
+ constructor(props) {
+ super(props);
+ this.fileInput = React.createRef();
+ this.state = {
+ loading: true,
+ errorMsg: '',
+ repoName: '',
+ path: '',
+ direntList: [],
+ isNewFolderDialogOpen: false,
+ userPerm: '',
+ };
+ }
+
+ componentDidMount () {
+ this.loadDirentList('/');
+ }
+
+ onPathClick = (path) => {
+ this.loadDirentList(path);
+ }
+
+ openFolder = (dirent) => {
+ let direntPath = Utils.joinPath(this.state.path, dirent.name);
+ if (!dirent.is_file) {
+ this.loadDirentList(direntPath);
+ }
+ }
+
+ loadDirentList = (path) => {
+ const { providerID, repoID } = this.props;
+ seafileAPI.listOCMRepoDir(providerID, repoID, path).then(res => {
+ const { repo_name: repoName, dirent_list, user_perm } = res.data;
+ let direntList = [];
+ dirent_list.forEach(item => {
+ let dirent = new Dirent(item);
+ direntList.push(dirent);
+ });
+ this.setState({
+ loading: false,
+ repoName: repoName,
+ direntList: direntList,
+ path: path,
+ userPerm: user_perm,
+ }, () => {
+ let url =`${siteRoot}remote-library/${providerID}/${repoID}/${repoName}${Utils.encodePath(path)}`;
+ window.history.replaceState({url: url, path: path}, path, url);
+ });
+ }).catch((error) => {
+ if (error.response) {
+ if (error.response.status == 403) {
+ this.setState({
+ loading: false,
+ errorMsg: gettext('Permission denied')
+ });
+ location.href = `${loginUrl}?next=${encodeURIComponent(location.href)}`;
+ } else {
+ this.setState({
+ loading: false,
+ errorMsg: gettext('Error')
+ });
+ }
+ } else {
+ this.setState({
+ loading: false,
+ errorMsg: gettext('Please check the network.')
+ });
+ }
+ });
+ }
+
+ downloadDirent = (dirent) => {
+ let path = Utils.joinPath(this.state.path, dirent.name);
+ seafileAPI.getOCMRepoDownloadURL(this.props.providerID, this.props.repoID, path).then(res => {
+ location.href = res.data;
+ }).catch((err) => {
+ let errMessage = Utils.getErrorMsg(err);
+ toaster.danger(errMessage);
+ });
+ }
+
+ openFileInput = () => {
+ this.fileInput.current.click();
+ }
+
+ onFileInputChange = () => {
+ if (!this.fileInput.current.files.length) {
+ return;
+ }
+ const file = this.fileInput.current.files[0];
+
+ let { path } = this.state;
+ let { providerID, repoID } = this.props;
+ seafileAPI.getOCMRepoUploadURL(providerID, repoID, path).then(res => {
+ let formData = new FormData();
+ formData.append('parent_dir', path);
+ formData.append('file', file);
+ post(res.data, formData).then(res => {
+ const fileObj = res.data[0];
+ let newDirent = new Dirent({
+ 'type': 'file',
+ 'name': fileObj.name,
+ 'size': fileObj.size,
+ 'mtime': (new Date()).getTime()
+ });
+ let direntList = this.state.direntList;
+ const dirs = direntList.filter(item => { return !item.is_file; });
+ direntList.splice(dirs.length, 0, newDirent);
+ this.setState({
+ direntList: direntList
+ });
+ });
+ }).catch((err) => {
+ let errMessage = Utils.getErrorMsg(err);
+ toaster.danger(errMessage);
+ });
+ }
+
+ render() {
+ const { loading, errorMsg,
+ repoName, direntList, path, userPerm } = this.state;
+ const { repoID } = this.props;
+
+ return (
+
+
+
+
+ {userPerm === 'rw' &&
+
+ }
+
+
+
+
+ );
+ }
+}
+
+export default DirView;
diff --git a/frontend/src/pages/share-with-ocm/shared-with-ocm.js b/frontend/src/pages/share-with-ocm/shared-with-ocm.js
new file mode 100644
index 0000000000..83756190c5
--- /dev/null
+++ b/frontend/src/pages/share-with-ocm/shared-with-ocm.js
@@ -0,0 +1,203 @@
+import React, { Component, Fragment } from 'react';
+import PropTypes from 'prop-types';
+import moment from 'moment';
+import { Link } from '@reach/router';
+import { gettext, siteRoot } from '../../utils/constants';
+import { seafileAPI } from '../../utils/seafile-api';
+import { Utils } from '../../utils/utils';
+import toaster from '../../components/toast';
+import Loading from '../../components/loading';
+import EmptyTip from '../../components/empty-tip';
+
+class Content extends Component {
+
+ render() {
+ const { loading, errorMsg, items } = this.props;
+
+ const emptyTip = (
+
+ {gettext('No libraries have been shared with you')}
+ {gettext('No libraries have been shared directly with you. You can find more shared libraries at "Shared with groups".')}
+
+ );
+
+ if (loading) {
+ return ;
+ } else if (errorMsg) {
+ return {errorMsg}
;
+ } else {
+ const table = (
+
+
+
+ |
+ {gettext('Name')} |
+ {gettext('Shared from')} |
+ {gettext('At site')} |
+ {gettext('Time')} |
+ {/* operations */} |
+
+
+
+ {items.map((item, index) => {
+ return ;
+ })}
+
+
+ );
+
+ return items.length ? table : emptyTip;
+ }
+ }
+}
+
+Content.propTypes = {
+ loading: PropTypes.bool.isRequired,
+ errorMsg: PropTypes.string.isRequired,
+ items: PropTypes.array.isRequired,
+};
+
+class Item extends Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ showOpIcon: false,
+ isOpMenuOpen: false // for mobile
+ };
+ }
+
+ toggleOpMenu = () => {
+ this.setState({
+ isOpMenuOpen: !this.state.isOpMenuOpen
+ });
+ }
+
+ handleMouseOver = () => {
+ this.setState({
+ showOpIcon: true
+ });
+ }
+
+ handleMouseOut = () => {
+ this.setState({
+ showOpIcon: false
+ });
+ }
+
+ deleteShare = () => {
+ this.props.deleteShare(this.props.item);
+ }
+
+ render() {
+ const item = this.props.item;
+
+ item.icon_url = Utils.getLibIconUrl(item);
+ item.icon_title = Utils.getLibIconTitle(item);
+ item.url = `${siteRoot}#shared-libs/lib/${item.repo_id}/`;
+
+ let shareRepoUrl =`${siteRoot}remote-library/${this.props.item.provider_id}/${this.props.item.repo_id}/${Utils.encodePath(this.props.item.repo_name)}/`;
+ let iconVisibility = this.state.showOpIcon ? '' : ' invisible';
+ let deleteIcon = `action-icon sf2-icon-x3 ${iconVisibility ? 'invisible' : ''}`;
+ return (
+
+
+  |
+ {item.repo_name} |
+ {item.from_user} |
+ {item.from_server_url} |
+ {moment(item.ctime).fromNow()} |
+
+
+ |
+
+
+
+ );
+ }
+}
+
+Item.propTypes = {
+ item: PropTypes.object.isRequired
+};
+
+class SharedWithOCM extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ loading: true,
+ errorMsg: '',
+ items: [],
+ };
+ }
+
+ componentDidMount() {
+ seafileAPI.listOCMSharesReceived().then((res) => {
+ this.setState({
+ loading: false,
+ items: res.data.ocm_share_received_list
+ });
+ }).catch((error) => {
+ if (error.response) {
+ if (error.response.status == 403) {
+ this.setState({
+ loading: false,
+ errorMsg: gettext('Permission denied')
+ });
+ } else {
+ this.setState({
+ loading: false,
+ errorMsg: gettext('Error')
+ });
+ }
+ } else {
+ this.setState({
+ loading: false,
+ errorMsg: gettext('Please check the network.')
+ });
+ }
+ });
+ }
+
+ deleteShare = (item) => {
+ let { id } = item;
+ seafileAPI.deleteOCMShareReceived(id).then((res) => {
+ toaster.success(gettext('delete success.'));
+ let items = this.state.items.filter(item => {
+ return item.id != id;
+ });
+ this.setState({items: items});
+ }).catch(error => {
+ let errMessage = Utils.getErrorMsg(error);
+ toaster.danger(errMessage);
+ });
+ }
+
+ render() {
+ return (
+
+
+
+
+
{gettext('Shared from other servers')}
+
+
+
+
+
+
+
+ );
+ }
+}
+
+export default SharedWithOCM;
diff --git a/frontend/src/utils/constants.js b/frontend/src/utils/constants.js
index 28803b77b7..1a4484fe6e 100644
--- a/frontend/src/utils/constants.js
+++ b/frontend/src/utils/constants.js
@@ -65,6 +65,7 @@ export const customNavItems = window.app.pageOptions.customNavItems;
export const enableShowContactEmailWhenSearchUser = window.app.pageOptions.enableShowContactEmailWhenSearchUser;
export const maxUploadFileSize = window.app.pageOptions.maxUploadFileSize;
export const maxNumberOfFilesForFileupload = window.app.pageOptions.maxNumberOfFilesForFileupload;
+export const enableOCM = window.app.pageOptions.enableOCM;
export const curNoteMsg = window.app.pageOptions.curNoteMsg;
export const curNoteID = window.app.pageOptions.curNoteID;
diff --git a/seahub/api2/authentication.py b/seahub/api2/authentication.py
index 723e118bed..6f11ec8d29 100644
--- a/seahub/api2/authentication.py
+++ b/seahub/api2/authentication.py
@@ -12,6 +12,7 @@ from seahub.base.accounts import User
from seahub.api2.models import Token, TokenV2
from seahub.api2.utils import get_client_ip
from seahub.repo_api_tokens.models import RepoAPITokens
+from seahub.ocm.models import OCMShare
from seahub.utils import within_time_range
try:
from seahub.settings import MULTI_TENANCY
@@ -176,7 +177,11 @@ class RepoAPITokenAuthentication(BaseAuthentication):
rat = RepoAPITokens.objects.filter(token=auth[1]).first()
if not rat:
- raise AuthenticationFailed('Token inactive or deleted')
+ rat = OCMShare.objects.filter(shared_secret=auth[1]).first()
+ if not rat:
+ raise AuthenticationFailed('Token inactive or deleted')
+ # if is request by remote server through ocm, use from_user instead of app_name
+ rat.app_name = rat.from_user
request.repo_api_token_obj = rat
return AnonymousUser(), auth[1]
diff --git a/seahub/api2/endpoints/ocm.py b/seahub/api2/endpoints/ocm.py
new file mode 100644
index 0000000000..9536b1c562
--- /dev/null
+++ b/seahub/api2/endpoints/ocm.py
@@ -0,0 +1,517 @@
+import logging
+import random
+import string
+import requests
+import json
+from constance import config
+
+from rest_framework import status
+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 seahub.api2.authentication import TokenAuthentication
+from seahub.api2.throttling import UserRateThrottle
+from seahub.api2.utils import api_error
+
+from seaserv import seafile_api, ccnet_api
+
+from seahub.utils.repo import get_available_repo_perms, get_repo_owner
+from seahub.base.templatetags.seahub_tags import email2nickname
+from seahub.constants import PERMISSION_READ, PERMISSION_READ_WRITE
+from seahub.ocm.models import OCMShareReceived, OCMShare
+from seahub.ocm.settings import ENABLE_OCM, SUPPORTED_OCM_PROTOCOLS, \
+ OCM_SEAFILE_PROTOCOL, OCM_RESOURCE_TYPE_LIBRARY, OCM_API_VERSION, \
+ OCM_SHARE_TYPES, OCM_ENDPOINT, OCM_PROVIDER_ID, OCM_NOTIFICATION_TYPE_LIST, \
+ OCM_NOTIFICATION_SHARE_UNSHARED, OCM_NOTIFICATION_SHARE_DECLINED, OCM_PROTOCOL_URL, \
+ OCM_NOTIFICATION_URL, OCM_CREATE_SHARE_URL
+
+logger = logging.getLogger(__name__)
+
+# Convert seafile permission to ocm protocol standard permission
+SEAFILE_PERMISSION2OCM_PERMISSION = {
+ PERMISSION_READ: ['read'],
+ PERMISSION_READ_WRITE: ['read', 'write'],
+}
+
+
+def gen_shared_secret(length=23):
+ return ''.join(random.choice(string.ascii_lowercase + string.digits) for i in range(length))
+
+
+def get_remote_protocol(url):
+ response = requests.get(url)
+ return json.loads(response.text)
+
+
+def is_valid_url(url):
+ if not url.startswith('https://') and not url.startswith('http://'):
+ return False
+ if not url.endswith('/'):
+ return False
+ return True
+
+
+def check_url_slash(url):
+ if not url.endswith('/'):
+ url += '/'
+ return url
+
+
+class OCMProtocolView(APIView):
+ throttle_classes = (UserRateThrottle,)
+
+ def get(self, request):
+ """
+ return ocm protocol info to remote server
+ """
+ # TODO
+ # currently if ENABLE_OCM is False, return 404 as if ocm protocol is not implemented
+ # ocm protocol is not clear about this, https://github.com/GEANT/OCM-API/pull/37
+ if not ENABLE_OCM:
+ error_msg = 'feature not enabled.'
+ return api_error(status.HTTP_404_NOT_FOUND, error_msg)
+
+ result = {
+ 'enabled': True,
+ 'apiVersion': OCM_API_VERSION,
+ 'endPoint': config.SERVICE_URL + '/' + OCM_ENDPOINT,
+ 'resourceTypes': {
+ 'name': OCM_RESOURCE_TYPE_LIBRARY,
+ 'shareTypes': OCM_SHARE_TYPES,
+ 'protocols': {
+ OCM_SEAFILE_PROTOCOL: OCM_SEAFILE_PROTOCOL,
+ }
+ }
+ }
+ return Response(result)
+
+
+class OCMSharesView(APIView):
+ throttle_classes = (UserRateThrottle,)
+
+ def post(self, request):
+ """
+ create ocm in consumer server
+ """
+
+ # argument check
+ share_with = request.data.get('shareWith', '')
+ if not share_with:
+ error_msg = 'shareWith invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ # curently only support repo share
+ repo_name = request.data.get('name', '')
+ if not repo_name:
+ error_msg = 'name invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ sender = request.data.get('sender', '')
+ if not sender:
+ error_msg = 'sender invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ share_type = request.data.get('shareType', '')
+ if share_type not in OCM_SHARE_TYPES:
+ error_msg = 'shareType %s invalid.' % share_type
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ resource_type = request.data.get('resourceType', '')
+ if resource_type != OCM_RESOURCE_TYPE_LIBRARY:
+ error_msg = 'resourceType %s invalid.' % resource_type
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ provider_id = request.data.get('providerId', '')
+ if not provider_id:
+ error_msg = 'providerId invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+
+ """
+ other ocm protocol fields currently not used
+
+ description = request.data.get('description', '')
+ owner = request.data.get('owner', '')
+ ownerDisplayName = request.data.get('ownerDisplayName', '')
+ senderDisplayName = request.data.get('senderDisplayName', '')
+ """
+
+ protocol = request.data.get('protocol', '')
+ if not protocol:
+ error_msg = 'protocol invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ if 'name' not in protocol.keys():
+ error_msg = 'protocol.name invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ if protocol['name'] not in SUPPORTED_OCM_PROTOCOLS:
+ error_msg = 'protocol %s not support.' % protocol['name']
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ if 'options' not in protocol.keys():
+ error_msg = 'protocol.options invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ if 'sharedSecret' not in protocol['options'].keys():
+ error_msg = 'protocol.options.sharedSecret invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ if 'permissions' not in protocol['options'].keys():
+ error_msg = 'protocol.options.permissions invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ if protocol['name'] == OCM_SEAFILE_PROTOCOL:
+ if 'repoId' not in protocol['options'].keys():
+ error_msg = 'protocol.options.repoId invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ if 'seafileServiceURL' not in protocol['options'].keys():
+ error_msg = 'protocol.options.seafileServiceURL invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ if protocol['name'] == OCM_SEAFILE_PROTOCOL:
+ shared_secret = protocol['options']['sharedSecret']
+ permissions = protocol['options']['permissions']
+ repo_id = protocol['options']['repoId']
+ from_server_url = protocol['options']['seafileServiceURL']
+
+ if OCMShareReceived.objects.filter(
+ from_user=sender,
+ to_user=share_with,
+ from_server_url=from_server_url,
+ repo_id=repo_id,
+ repo_name=repo_name,
+ provider_id=provider_id,
+ ).exists():
+ return api_error(status.HTTP_400_BAD_REQUEST, 'same share already exists.')
+
+ if 'write' in permissions:
+ permission = PERMISSION_READ_WRITE
+ else:
+ permission = PERMISSION_READ
+
+ OCMShareReceived.objects.add(
+ shared_secret=shared_secret,
+ from_user=sender,
+ to_user=share_with,
+ from_server_url=from_server_url,
+ repo_id=repo_id,
+ repo_name=repo_name,
+ permission=permission,
+ provider_id=provider_id,
+ )
+
+ return Response(request.data, status=status.HTTP_201_CREATED)
+
+
+class OCMNotificationsView(APIView):
+ throttle_classes = (UserRateThrottle,)
+
+ def post(self, request):
+ """ Handle notifications from remote server
+ """
+ notification_type = request.data.get('notificationType', '')
+ if not notification_type:
+ error_msg = 'notificationType %s invalid.' % notification_type
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ if notification_type not in OCM_NOTIFICATION_TYPE_LIST:
+ error_msg = 'notificationType %s not supportd.' % notification_type
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ resource_type = request.data.get('resourceType', '')
+ if resource_type != OCM_RESOURCE_TYPE_LIBRARY:
+ error_msg = 'resourceType %s invalid.' % resource_type
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ notification = request.data.get('notification', '')
+ if not notification:
+ error_msg = 'notification invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ shared_secret = notification.get('sharedSecret', '')
+ if not shared_secret:
+ error_msg = 'sharedSecret invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ if notification_type == OCM_NOTIFICATION_SHARE_UNSHARED:
+ """
+ Provider unshared, then delete ocm_share_received record on Consumer
+ """
+ try:
+ ocm_share_received = OCMShareReceived.objects.get(shared_secret=shared_secret)
+ except OCMShareReceived.DoesNotExist:
+ return Response(request.data)
+
+ if ocm_share_received:
+ try:
+ ocm_share_received.delete()
+ except Exception as e:
+ logger.error(e)
+ return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Invernal Server Error')
+
+ elif notification_type == OCM_NOTIFICATION_SHARE_DECLINED:
+ """
+ Consumer declined share, then delete ocm_share record on Provider
+ """
+ try:
+ ocm_share = OCMShare.objects.get(shared_secret=shared_secret)
+ except OCMShareReceived.DoesNotExist:
+ return Response(request.data)
+
+ if ocm_share:
+ try:
+ ocm_share.delete()
+ except Exception as e:
+ logger.error(e)
+ return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Invernal Server Error')
+
+ return Response(request.data)
+
+
+class OCMSharesPrepareView(APIView):
+ authentication_classes = (TokenAuthentication, SessionAuthentication)
+ permission_classes = (IsAuthenticated,)
+ throttle_classes = (UserRateThrottle,)
+
+ def get(self, request):
+ """
+ list ocm shares of request user, filt by repo_id
+ """
+ repo_id = request.GET.get('repo_id', '')
+ if repo_id:
+ ocm_shares = OCMShare.objects.filter(repo_id=repo_id, from_user=request.user.username)
+ else:
+ ocm_shares = OCMShare.objects.filter(from_user=request.user.username)
+
+ ocm_share_list = []
+ for ocm_share in ocm_shares:
+ ocm_share_list.append(ocm_share.to_dict())
+ return Response({'ocm_share_list': ocm_share_list})
+
+ def post(self, request):
+ """
+ prepare provider server info for ocm, and send post request to consumer
+ three step:
+ 1. send get request to remote server, ask if support ocm, and get other info
+ 2. send post request to remote server, remote server create a recored in remote
+ ocm_share_received table
+ 3. store a recored in local ocm_share table
+ """
+
+ # argument check
+ to_user = request.data.get('to_user', '')
+ if not to_user:
+ error_msg = 'to_user invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ to_server_url = request.data.get('to_server_url', '').lower().strip()
+ if not to_server_url or not is_valid_url(to_server_url):
+ error_msg = 'to_server_url %s invalid.' % to_server_url
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ repo_id = request.data.get('repo_id', '')
+ if not repo_id:
+ error_msg = 'repo_id invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ repo = seafile_api.get_repo(repo_id)
+ if not repo:
+ return api_error(status.HTTP_404_NOT_FOUND, 'Library %s not found.' % repo_id)
+
+ path = request.data.get('path', '/')
+
+ # TODO
+ # 1. folder check
+ # 2. encrypted repo check
+ #
+ # if seafile_api.get_dir_id_by_path(repo.id, path) is None:
+ # return api_error(status.HTTP_404_NOT_FOUND, 'Folder %s not found.' % path)
+ #
+ # if repo.encrypted and path != '/':
+ # return api_error(status.HTTP_400_BAD_REQUEST, 'Folder invalid.')
+
+ permission = request.data.get('permission', PERMISSION_READ)
+ if permission not in get_available_repo_perms():
+ return api_error(status.HTTP_400_BAD_REQUEST, 'permission invalid.')
+
+ username = request.user.username
+ repo_owner = get_repo_owner(request, repo_id)
+ if repo_owner != username:
+ return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.')
+
+ if OCMShare.objects.filter(
+ from_user=request.user.username,
+ to_user=to_user,
+ to_server_url=to_server_url,
+ repo_id=repo_id,
+ repo_name=repo.repo_name,
+ path=path,
+ ).exists():
+ return api_error(status.HTTP_400_BAD_REQUEST, 'same share already exists.')
+
+ consumer_protocol = get_remote_protocol(to_server_url + OCM_PROTOCOL_URL)
+
+ shared_secret = gen_shared_secret()
+ from_user = username
+ post_data = {
+ 'shareWith': to_user,
+ 'name': repo.repo_name,
+ 'description': '',
+ 'providerId': OCM_PROVIDER_ID,
+ 'owner': repo_owner,
+ 'sender': from_user,
+ 'ownerDisplayName': email2nickname(repo_owner),
+ 'senderDisplayName': email2nickname(from_user),
+ 'shareType': consumer_protocol['resourceTypes']['shareTypes'][0], # currently only support user type
+ 'resourceType': consumer_protocol['resourceTypes']['name'], # currently only support repo
+ 'protocol': {
+ 'name': OCM_SEAFILE_PROTOCOL,
+ 'options': {
+ 'sharedSecret': shared_secret,
+ 'permissions': SEAFILE_PERMISSION2OCM_PERMISSION[permission],
+ 'repoId': repo_id,
+ 'seafileServiceURL': check_url_slash(config.SERVICE_URL),
+ },
+ },
+ }
+ url = consumer_protocol['endPoint'] + OCM_CREATE_SHARE_URL
+ try:
+ requests.post(url, json=post_data)
+ except Exception as e:
+ logging.error(e)
+ return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error')
+
+ ocm_share = OCMShare.objects.add(
+ shared_secret=shared_secret,
+ from_user=request.user.username,
+ to_user=to_user,
+ to_server_url=to_server_url,
+ repo_id=repo_id,
+ repo_name=repo.repo_name,
+ path=path,
+ permission=permission,
+ )
+
+ return Response(ocm_share.to_dict())
+
+
+class OCMSharePrepareView(APIView):
+ authentication_classes = (TokenAuthentication, SessionAuthentication)
+ permission_classes = (IsAuthenticated,)
+ throttle_classes = (UserRateThrottle,)
+
+ def delete(self, request, pk):
+ """
+ delete an share received record
+ """
+ try:
+ ocm_share = OCMShare.objects.get(pk=pk)
+ except OCMShareReceived.DoesNotExist:
+ error_msg = 'OCMShare %s not found.' % pk
+ return api_error(status.HTTP_404_NOT_FOUND, error_msg)
+
+ if ocm_share.from_user != request.user.username:
+ error_msg = 'permission denied.'
+ return api_error(status.HTTP_403_FORBIDDEN, error_msg)
+
+ to_server_url = ocm_share.to_server_url
+ shared_secret = ocm_share.shared_secret
+
+ consumer_protocol = get_remote_protocol(to_server_url + OCM_PROTOCOL_URL)
+
+ # send unshare notification to consumer
+ post_data = {
+ 'notificationType': OCM_NOTIFICATION_SHARE_UNSHARED,
+ 'resourceType': OCM_RESOURCE_TYPE_LIBRARY,
+ 'providerId': OCM_PROVIDER_ID,
+ 'notification': {
+ 'sharedSecret': shared_secret,
+ 'message': '',
+ },
+ }
+
+ url = consumer_protocol['endPoint'] + OCM_NOTIFICATION_URL
+ try:
+ requests.post(url, json=post_data)
+ except Exception as e:
+ logging.error(e)
+ return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error')
+
+ try:
+ ocm_share.delete()
+ except Exception as e:
+ logger.error(e)
+ return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error')
+
+ return Response({'success': True})
+
+
+class OCMSharesReceivedView(APIView):
+ authentication_classes = (TokenAuthentication, SessionAuthentication)
+ permission_classes = (IsAuthenticated,)
+ throttle_classes = (UserRateThrottle,)
+
+ def get(self, request):
+ """
+ list ocm shares received
+ """
+ ocm_share_received_list = []
+ ocm_shares_received = OCMShareReceived.objects.filter(to_user=request.user.username)
+ for ocm_share_received in ocm_shares_received:
+ ocm_share_received_list.append(ocm_share_received.to_dict())
+ return Response({'ocm_share_received_list': ocm_share_received_list})
+
+
+class OCMShareReceivedView(APIView):
+ authentication_classes = (TokenAuthentication, SessionAuthentication)
+ permission_classes = (IsAuthenticated,)
+ throttle_classes = (UserRateThrottle,)
+
+ def delete(self, request, pk):
+ """
+ delete an share received record
+ """
+ try:
+ ocm_share_received = OCMShareReceived.objects.get(pk=pk)
+ except OCMShareReceived.DoesNotExist:
+ error_msg = 'OCMShareReceived %s not found.' % pk
+ return api_error(status.HTTP_404_NOT_FOUND, error_msg)
+
+ if ocm_share_received.to_user != request.user.username:
+ error_msg = 'permission denied.'
+ return api_error(status.HTTP_403_FORBIDDEN, error_msg)
+
+ from_server_url = ocm_share_received.from_server_url
+ shared_secret = ocm_share_received.shared_secret
+
+ provider_protocol = get_remote_protocol(from_server_url + OCM_PROTOCOL_URL)
+
+ # send unshare notification to consumer
+ post_data = {
+ 'notificationType': OCM_NOTIFICATION_SHARE_DECLINED,
+ 'resourceType': OCM_RESOURCE_TYPE_LIBRARY,
+ 'providerId': OCM_PROVIDER_ID,
+ 'notification': {
+ 'sharedSecret': shared_secret,
+ 'message': '',
+ },
+ }
+
+ url = provider_protocol['endPoint'] + OCM_NOTIFICATION_URL
+ try:
+ requests.post(url, json=post_data)
+ except Exception as e:
+ logging.error(e)
+ return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error')
+
+ try:
+ ocm_share_received.delete()
+ except Exception as e:
+ logger.error(e)
+ return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error')
+
+ return Response({'success': True})
diff --git a/seahub/api2/endpoints/ocm_repos.py b/seahub/api2/endpoints/ocm_repos.py
new file mode 100644
index 0000000000..df060f1c44
--- /dev/null
+++ b/seahub/api2/endpoints/ocm_repos.py
@@ -0,0 +1,140 @@
+import logging
+import requests
+import json
+
+from rest_framework import status
+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 seahub.api2.authentication import TokenAuthentication
+from seahub.api2.throttling import UserRateThrottle
+from seahub.api2.utils import api_error
+from seahub.ocm.models import OCMShareReceived
+from seahub.ocm.settings import VIA_REPO_TOKEN_URL
+from seahub.constants import PERMISSION_READ_WRITE
+
+
+logger = logging.getLogger(__name__)
+
+
+def send_get_request(url, params=None, headers=None):
+ response = requests.get(url, params=params, headers=headers)
+ return json.loads(response.text)
+
+
+class OCMReposDirView(APIView):
+ authentication_classes = (TokenAuthentication, SessionAuthentication)
+ permission_classes = (IsAuthenticated,)
+ throttle_classes = (UserRateThrottle,)
+
+ def get(self, request, provider_id, repo_id):
+ """
+ Send request to Provider to get repo item list
+ """
+
+ path = request.GET.get('path', '/')
+
+ with_thumbnail = request.GET.get('with_thumbnail', 'false')
+ if with_thumbnail not in ('true', 'false'):
+ error_msg = 'with_thumbnail invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ ocm_share_received = OCMShareReceived.objects.filter(provider_id=provider_id, repo_id=repo_id).first()
+ if not ocm_share_received:
+ error_msg = 'Library %s not found.' % repo_id
+ return api_error(status.HTTP_404_NOT_FOUND, error_msg)
+
+ if ocm_share_received.to_user != request.user.username:
+ error_msg = 'permission denied.'
+ return api_error(status.HTTP_403_FORBIDDEN, error_msg)
+
+ url = ocm_share_received.from_server_url + VIA_REPO_TOKEN_URL['DIR']
+ params = {
+ 'path': path,
+ 'with_thumbnail': with_thumbnail,
+ }
+ headers = {'Authorization': 'token ' + ocm_share_received.shared_secret}
+ try:
+ resp = send_get_request(url, params=params, headers=headers)
+ except Exception as e:
+ logging.error(e)
+ return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error')
+
+ return Response(resp)
+
+
+class OCMReposDownloadLinkView(APIView):
+ authentication_classes = (TokenAuthentication, SessionAuthentication)
+ permission_classes = (IsAuthenticated,)
+ throttle_classes = (UserRateThrottle,)
+
+ def get(self, request, provider_id, repo_id):
+ """
+ Send request to Provider to get download link
+ """
+
+ path = request.GET.get('path', '/')
+
+ ocm_share_received = OCMShareReceived.objects.filter(provider_id=provider_id, repo_id=repo_id).first()
+ if not ocm_share_received:
+ error_msg = 'Library %s not found.' % repo_id
+ return api_error(status.HTTP_404_NOT_FOUND, error_msg)
+
+ if ocm_share_received.to_user != request.user.username:
+ error_msg = 'permission denied.'
+ return api_error(status.HTTP_403_FORBIDDEN, error_msg)
+
+ url = ocm_share_received.from_server_url + VIA_REPO_TOKEN_URL['DOWNLOAD_LINK']
+ params = {
+ 'path': path,
+ }
+ headers = {'Authorization': 'token ' + ocm_share_received.shared_secret}
+ try:
+ resp = send_get_request(url, params=params, headers=headers)
+ except Exception as e:
+ logging.error(e)
+ return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error')
+
+ return Response(resp)
+
+
+class OCMReposUploadLinkView(APIView):
+ authentication_classes = (TokenAuthentication, SessionAuthentication)
+ permission_classes = (IsAuthenticated,)
+ throttle_classes = (UserRateThrottle,)
+
+ def get(self, request, provider_id, repo_id):
+ """
+ Send request to Provider to get upload link
+ """
+
+ path = request.GET.get('path', '/')
+
+ ocm_share_received = OCMShareReceived.objects.filter(provider_id=provider_id, repo_id=repo_id).first()
+ if not ocm_share_received:
+ error_msg = 'Library %s not found.' % repo_id
+ return api_error(status.HTTP_404_NOT_FOUND, error_msg)
+
+ if ocm_share_received.to_user != request.user.username:
+ error_msg = 'permission denied.'
+ return api_error(status.HTTP_403_FORBIDDEN, error_msg)
+
+ if ocm_share_received.permission != PERMISSION_READ_WRITE:
+ error_msg = 'permission denied.'
+ return api_error(status.HTTP_403_FORBIDDEN, error_msg)
+
+ url = ocm_share_received.from_server_url + VIA_REPO_TOKEN_URL['UPLOAD_LINK']
+ params = {
+ 'path': path,
+ 'from': 'web',
+ }
+ headers = {'Authorization': 'token ' + ocm_share_received.shared_secret}
+ try:
+ resp = send_get_request(url, params=params, headers=headers)
+ except Exception as e:
+ logging.error(e)
+ return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error')
+
+ return Response(resp)
diff --git a/seahub/api2/endpoints/via_repo_token.py b/seahub/api2/endpoints/via_repo_token.py
index ea4b5a67f9..0dc59d987e 100644
--- a/seahub/api2/endpoints/via_repo_token.py
+++ b/seahub/api2/endpoints/via_repo_token.py
@@ -186,6 +186,7 @@ class ViaRepoDirView(APIView):
response_dict = {}
response_dict["user_perm"] = permission
response_dict["dir_id"] = dir_id
+ response_dict["repo_name"] = repo.repo_name
if request_type == 'f':
response_dict['dirent_list'] = all_file_info_list
diff --git a/seahub/ocm/__init__.py b/seahub/ocm/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/seahub/ocm/migrations/0001_initial.py b/seahub/ocm/migrations/0001_initial.py
new file mode 100644
index 0000000000..83a5af7b16
--- /dev/null
+++ b/seahub/ocm/migrations/0001_initial.py
@@ -0,0 +1,53 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.23 on 2019-12-06 01:43
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='OCMShare',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('shared_secret', models.CharField(db_index=True, max_length=36, unique=True)),
+ ('from_user', models.CharField(db_index=True, max_length=255)),
+ ('to_user', models.CharField(db_index=True, max_length=255)),
+ ('to_server_url', models.URLField(db_index=True)),
+ ('repo_id', models.CharField(db_index=True, max_length=36)),
+ ('repo_name', models.CharField(max_length=255)),
+ ('permission', models.CharField(choices=[('rw', 'read, write'), ('r', 'read')], max_length=50)),
+ ('path', models.TextField()),
+ ('ctime', models.DateTimeField(auto_now_add=True)),
+ ],
+ options={
+ 'db_table': 'ocm_share',
+ },
+ ),
+ migrations.CreateModel(
+ name='OCMShareReceived',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('shared_secret', models.CharField(db_index=True, max_length=36, unique=True)),
+ ('from_user', models.CharField(db_index=True, max_length=255)),
+ ('to_user', models.CharField(db_index=True, max_length=255)),
+ ('from_server_url', models.URLField(db_index=True)),
+ ('repo_id', models.CharField(db_index=True, max_length=36)),
+ ('repo_name', models.CharField(max_length=255)),
+ ('permission', models.CharField(choices=[('rw', 'read, write'), ('r', 'read')], max_length=50)),
+ ('path', models.TextField()),
+ ('provider_id', models.CharField(db_index=True, max_length=40)),
+ ('ctime', models.DateTimeField(auto_now_add=True)),
+ ],
+ options={
+ 'db_table': 'ocm_share_received',
+ },
+ ),
+ ]
diff --git a/seahub/ocm/migrations/__init__.py b/seahub/ocm/migrations/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/seahub/ocm/models.py b/seahub/ocm/models.py
new file mode 100644
index 0000000000..091f8fe963
--- /dev/null
+++ b/seahub/ocm/models.py
@@ -0,0 +1,107 @@
+from _sha1 import sha1
+
+import hmac
+import uuid
+
+from django.db import models
+from django.conf import settings
+from seahub.constants import PERMISSION_READ_WRITE, PERMISSION_READ
+from seahub.utils.timeutils import datetime_to_isoformat_timestr
+
+PERMISSION_CHOICES = (
+ (PERMISSION_READ_WRITE, 'read, write'),
+ (PERMISSION_READ, 'read'),
+)
+
+
+class OCMShareManager(models.Manager):
+
+ def add(self, shared_secret, from_user, to_user, to_server_url, repo_id, repo_name, permission, path='/'):
+ ocm_share = super(OCMShareManager, self).create(shared_secret=shared_secret,
+ from_user=from_user,
+ to_user=to_user,
+ to_server_url=to_server_url,
+ repo_id=repo_id,
+ repo_name=repo_name,
+ path=path,
+ permission=permission)
+ return ocm_share
+
+
+class OCMShare(models.Model):
+ shared_secret = models.CharField(max_length=36, db_index=True, unique=True)
+ from_user = models.CharField(max_length=255, db_index=True)
+ to_user = models.CharField(max_length=255, db_index=True)
+ to_server_url = models.URLField(db_index=True)
+ repo_id = models.CharField(max_length=36, db_index=True)
+ repo_name = models.CharField(max_length=settings.MAX_FILE_NAME)
+ permission = models.CharField(max_length=50, choices=PERMISSION_CHOICES)
+ path = models.TextField()
+ ctime = models.DateTimeField(auto_now_add=True)
+
+ objects = OCMShareManager()
+
+ class Meta:
+ db_table = 'ocm_share'
+
+ def to_dict(self):
+ return {
+ 'id': self.pk,
+ 'shared_secret': self.shared_secret,
+ 'from_user': self.from_user,
+ 'to_user': self.to_user,
+ 'to_sever_url': self.to_server_url,
+ 'repo_id': self.repo_id,
+ 'repo_name': self.repo_name,
+ 'path': self.path,
+ 'permission': self.permission,
+ 'ctime': datetime_to_isoformat_timestr(self.ctime),
+ }
+
+
+class OCMShareReceivedManager(models.Manager):
+
+ def add(self, shared_secret, from_user, to_user, from_server_url, repo_id, repo_name, permission, provider_id, path='/'):
+ ocm_share = super(OCMShareReceivedManager, self).create(shared_secret=shared_secret,
+ from_user=from_user,
+ to_user=to_user,
+ from_server_url=from_server_url,
+ repo_id=repo_id,
+ repo_name=repo_name,
+ path=path,
+ permission=permission,
+ provider_id=provider_id)
+ return ocm_share
+
+
+class OCMShareReceived(models.Model):
+ shared_secret = models.CharField(max_length=36, db_index=True, unique=True)
+ from_user = models.CharField(max_length=255, db_index=True)
+ to_user = models.CharField(max_length=255, db_index=True)
+ from_server_url = models.URLField(db_index=True)
+ repo_id = models.CharField(max_length=36, db_index=True)
+ repo_name = models.CharField(max_length=settings.MAX_FILE_NAME)
+ permission = models.CharField(max_length=50, choices=PERMISSION_CHOICES)
+ path = models.TextField()
+ provider_id = models.CharField(max_length=40, db_index=True)
+ ctime = models.DateTimeField(auto_now_add=True)
+
+ objects = OCMShareReceivedManager()
+
+ class Meta:
+ db_table = 'ocm_share_received'
+
+ def to_dict(self):
+ return {
+ 'id': self.pk,
+ 'shared_secret': self.shared_secret,
+ 'from_user': self.from_user,
+ 'to_user': self.to_user,
+ 'from_server_url': self.from_server_url,
+ 'repo_id': self.repo_id,
+ 'repo_name': self.repo_name,
+ 'path': self.path,
+ 'permission': self.permission,
+ 'provider_id': self.provider_id,
+ 'ctime': datetime_to_isoformat_timestr(self.ctime),
+ }
diff --git a/seahub/ocm/settings.py b/seahub/ocm/settings.py
new file mode 100644
index 0000000000..7a21b43939
--- /dev/null
+++ b/seahub/ocm/settings.py
@@ -0,0 +1,34 @@
+from django.conf import settings
+
+ENABLE_OCM = getattr(settings, 'ENABLE_OCM', False)
+OCM_PROVIDER_ID = getattr(settings, 'OCM_PROVIDER_ID', '')
+OCM_SEAFILE_PROTOCOL = getattr(settings, 'OCM_SEAFILE_PROTOCOL', 'Seafile API')
+OCM_API_VERSION = getattr(settings, 'OCM_API_VERSION', '1.0-proposal1')
+OCM_ENDPOINT = getattr(settings, 'OCM_ENDPOINT', 'api/v2.1/ocm/')
+# consumer delete a share
+OCM_NOTIFICATION_SHARE_DECLINED = getattr(settings, 'OCM_NOTIFICATION_SHARE_DECLINED', 'SHARE_DECLINED')
+# provider delete a share
+OCM_NOTIFICATION_SHARE_UNSHARED = getattr(settings, 'OCM_NOTIFICATION_SHARE_UNSHARED', 'SHARE_UNSHARED')
+
+# protocol urls
+OCM_PROTOCOL_URL = getattr(settings, 'OCM_PROTOCOL_URL', 'ocm-provider/')
+OCM_CREATE_SHARE_URL = getattr(settings, 'OCM_CREATE_SHARE_URL', 'shares/')
+OCM_NOTIFICATION_URL = getattr(settings, 'OCM_PROTOCOL_URL', 'notifications/')
+
+# constants
+OCM_RESOURCE_TYPE_FILE = 'file'
+OCM_RESOURCE_TYPE_LIBRARY = 'library'
+OCM_SHARE_TYPES = ['user']
+SUPPORTED_OCM_PROTOCOLS = (
+ OCM_SEAFILE_PROTOCOL,
+)
+OCM_NOTIFICATION_TYPE_LIST = [
+ OCM_NOTIFICATION_SHARE_UNSHARED,
+ OCM_NOTIFICATION_SHARE_DECLINED,
+]
+
+VIA_REPO_TOKEN_URL = {
+ 'DIR': 'api/v2.1/via-repo-token/dir/',
+ 'UPLOAD_LINK': 'api/v2.1/via-repo-token/upload-link/',
+ 'DOWNLOAD_LINK': 'api/v2.1/via-repo-token/download-link/',
+}
diff --git a/seahub/repo_api_tokens/utils.py b/seahub/repo_api_tokens/utils.py
index fc5b60d9c9..a1c81d69d8 100644
--- a/seahub/repo_api_tokens/utils.py
+++ b/seahub/repo_api_tokens/utils.py
@@ -13,6 +13,7 @@ from seahub.thumbnail.utils import get_thumbnail_src
from seahub.utils import is_pro_version, FILEEXT_TYPE_MAP, IMAGE, XMIND, VIDEO
from seahub.utils.file_tags import get_files_tags_in_dir
from seahub.utils.repo import is_group_repo_staff, is_repo_owner
+from seahub.utils.timeutils import timestamp_to_isoformat_timestr
logger = logging.getLogger(__name__)
json_content_type = 'application/json; charset=utf-8'
@@ -59,7 +60,7 @@ def get_dir_file_recursively(repo_id, path, all_dirs):
entry["parent_dir"] = path
entry["id"] = dirent.obj_id
entry["name"] = dirent.obj_name
- entry["mtime"] = dirent.mtime
+ entry["mtime"] = timestamp_to_isoformat_timestr(dirent.mtime)
all_dirs.append(entry)
@@ -114,7 +115,7 @@ def get_dir_file_info_list(username, request_type, repo_obj, parent_dir,
dir_info["type"] = "dir"
dir_info["id"] = dirent.obj_id
dir_info["name"] = dirent.obj_name
- dir_info["mtime"] = dirent.mtime
+ dir_info["mtime"] = timestamp_to_isoformat_timestr(dirent.mtime)
dir_info["permission"] = dirent.permission
dir_info["parent_dir"] = parent_dir
dir_info_list.append(dir_info)
@@ -157,7 +158,7 @@ def get_dir_file_info_list(username, request_type, repo_obj, parent_dir,
file_info["type"] = "file"
file_info["id"] = file_obj_id
file_info["name"] = file_name
- file_info["mtime"] = dirent.mtime
+ file_info["mtime"] = timestamp_to_isoformat_timestr(dirent.mtime)
file_info["permission"] = dirent.permission
file_info["parent_dir"] = parent_dir
file_info["size"] = dirent.size
diff --git a/seahub/settings.py b/seahub/settings.py
index fa090c4415..3d43f66453 100644
--- a/seahub/settings.py
+++ b/seahub/settings.py
@@ -127,7 +127,6 @@ MIDDLEWARE = [
'seahub.trusted_ip.middleware.LimitIpMiddleware'
]
-
SITE_ROOT_URLCONF = 'seahub.urls'
ROOT_URLCONF = 'seahub.utils.rooturl'
SITE_ROOT = '/'
@@ -254,6 +253,7 @@ INSTALLED_APPS = [
'seahub.repo_api_tokens',
'seahub.abuse_reports',
'seahub.repo_auto_delete',
+ 'seahub.ocm',
]
diff --git a/seahub/templates/base_for_react.html b/seahub/templates/base_for_react.html
index c428fb8db8..284507653b 100644
--- a/seahub/templates/base_for_react.html
+++ b/seahub/templates/base_for_react.html
@@ -92,6 +92,7 @@
thumbnailSizeForOriginal: {{ thumbnail_size_for_original }},
repoPasswordMinLength: {{repo_password_min_length}},
canAddPublicRepo: {% if can_add_public_repo %} true {% else %} false {% endif %},
+ enableOCM: {% if enable_ocm %} true {% else %} false {% endif %},
canInvitePeople: {% if enable_guest_invitation and user.permissions.can_invite_guest %} true {% else %} false {% endif %},
customNavItems: {% if custom_nav_items %} JSON.parse('{{ custom_nav_items | escapejs }}') {% else %} {{'[]'}} {% endif %},
enableShowContactEmailWhenSearchUser: {% if enable_show_contact_email_when_search_user %} true {% else %} false {% endif %},
diff --git a/seahub/urls.py b/seahub/urls.py
index 6b6368f2b9..06e9994622 100644
--- a/seahub/urls.py
+++ b/seahub/urls.py
@@ -97,6 +97,10 @@ from seahub.api2.endpoints.repo_api_tokens import RepoAPITokensView, RepoAPIToke
from seahub.api2.endpoints.via_repo_token import ViaRepoDirView, ViaRepoUploadLinkView, RepoInfoView, \
ViaRepoDownloadLinkView
from seahub.api2.endpoints.abuse_reports import AbuseReportsView
+from seahub.api2.endpoints.ocm import OCMProtocolView, OCMSharesView, OCMNotificationsView, \
+ OCMSharesPrepareView, OCMSharePrepareView, OCMSharesReceivedView, OCMShareReceivedView
+from seahub.api2.endpoints.ocm_repos import OCMReposDirView, OCMReposDownloadLinkView, \
+ OCMReposUploadLinkView
from seahub.api2.endpoints.repo_share_links import RepoShareLinks, RepoShareLink
from seahub.api2.endpoints.repo_upload_links import RepoUploadLinks, RepoUploadLink
@@ -177,6 +181,8 @@ from seahub.api2.endpoints.file_participants import FileParticipantsView, FilePa
from seahub.api2.endpoints.repo_related_users import RepoRelatedUsersView
from seahub.api2.endpoints.repo_auto_delete import RepoAutoDeleteView
+from seahub.ocm.settings import OCM_ENDPOINT
+
urlpatterns = [
url(r'^accounts/', include('seahub.base.registration_urls')),
@@ -246,11 +252,13 @@ urlpatterns = [
url(r'^share-admin-share-links/$', react_fake_view, name="share_admin_share_links"),
url(r'^share-admin-upload-links/$', react_fake_view, name="share_admin_upload_links"),
url(r'^shared-libs/$', react_fake_view, name="shared_libs"),
+ url(r'^shared-with-ocm/$', react_fake_view, name="shared_with_ocm"),
url(r'^my-libs/$', react_fake_view, name="my_libs"),
url(r'^groups/$', react_fake_view, name="groups"),
url(r'^group/(?P\d+)/$', react_fake_view, name="group"),
url(r'^library/(?P[-0-9a-f]{36})/$', react_fake_view, name="library_view"),
url(r'^library/(?P[-0-9a-f]{36})/(?P[^/]+)/(?P.*)$', react_fake_view, name="lib_view"),
+ url(r'^remote-library/(?P[-0-9a-f]{36})/(?P[-0-9a-f]{36})/(?P[^/]+)/(?P.*)$', react_fake_view, name="remote_lib_view"),
url(r'^my-libs/deleted/$', react_fake_view, name="my_libs_deleted"),
url(r'^org/$', react_fake_view, name="org"),
url(r'^invitations/$', react_fake_view, name="invitations"),
@@ -447,6 +455,22 @@ urlpatterns = [
## user::activities
url(r'^api/v2.1/activities/$', ActivitiesView.as_view(), name='api-v2.1-acitvity'),
+ ## user::ocm
+ # ocm inter-server api, interact with other server
+ url(r'ocm-provider/$', OCMProtocolView.as_view(), name='api-v2.1-ocm-protocol'),
+ url(r'' + OCM_ENDPOINT + 'shares/$', OCMSharesView.as_view(), name='api-v2.1-ocm-shares'),
+ url(r'' + OCM_ENDPOINT + 'notifications/$', OCMNotificationsView.as_view(), name='api-v2.1-ocm-notifications'),
+
+ # ocm local api, no interaction with other server
+ url(r'api/v2.1/ocm/shares-prepare/$', OCMSharesPrepareView.as_view(), name='api-v2.1-ocm-shares-prepare'),
+ url(r'api/v2.1/ocm/shares-prepare/(?P\d+)/$', OCMSharePrepareView.as_view(), name='api-v2.1-ocm-share-prepare'),
+ url(r'api/v2.1/ocm/shares-received/$', OCMSharesReceivedView.as_view(), name='api-v2.1-ocm-shares-received'),
+ url(r'api/v2.1/ocm/shares-received/(?P\d+)/$', OCMShareReceivedView.as_view(), name='api-v2.1-ocm-share-received'),
+ # ocm local api, repo related operations
+ url(r'api/v2.1/ocm/providers/(?P[-0-9a-f]{36})/repos/(?P[-0-9a-f]{36})/dir/$', OCMReposDirView.as_view(), name='api-v2.1-ocm-repos-dir'),
+ url(r'api/v2.1/ocm/providers/(?P[-0-9a-f]{36})/repos/(?P[-0-9a-f]{36})/download-link/$', OCMReposDownloadLinkView.as_view(), name='api-v2.1-ocm-repos-dir'),
+ url(r'api/v2.1/ocm/providers/(?P[-0-9a-f]{36})/repos/(?P[-0-9a-f]{36})/upload-link/$', OCMReposUploadLinkView.as_view(), name='api-v2.1-ocm-repos-dir'),
+
# admin: activities
url(r'^api/v2.1/admin/user-activities/$', UserActivitiesView.as_view(), name='api-v2.1-admin-user-activity'),
diff --git a/seahub/views/__init__.py b/seahub/views/__init__.py
index ed46d34fe7..24b88b77a6 100644
--- a/seahub/views/__init__.py
+++ b/seahub/views/__init__.py
@@ -61,6 +61,7 @@ from seahub.settings import AVATAR_FILE_STORAGE, \
from seahub.wopi.settings import ENABLE_OFFICE_WEB_APP
from seahub.onlyoffice.settings import ENABLE_ONLYOFFICE
+from seahub.ocm.settings import ENABLE_OCM
from seahub.constants import HASH_URLS, PERMISSION_READ
from seahub.weixin.settings import ENABLE_WEIXIN
@@ -1169,5 +1170,6 @@ def react_fake_view(request, **kwargs):
'enable_show_contact_email_when_search_user' : settings.ENABLE_SHOW_CONTACT_EMAIL_WHEN_SEARCH_USER,
'additional_share_dialog_note': ADDITIONAL_SHARE_DIALOG_NOTE,
'additional_app_bottom_links': ADDITIONAL_APP_BOTTOM_LINKS,
- 'additional_about_dialog_links': ADDITIONAL_ABOUT_DIALOG_LINKS
+ 'additional_about_dialog_links': ADDITIONAL_ABOUT_DIALOG_LINKS,
+ 'enable_ocm': ENABLE_OCM,
})