diff --git a/frontend/src/app.js b/frontend/src/app.js index 96d4401dca..a383815fba 100644 --- a/frontend/src/app.js +++ b/frontend/src/app.js @@ -19,6 +19,8 @@ import ShareAdminFolders from './pages/share-admin/folders'; import ShareAdminShareLinks from './pages/share-admin/share-links'; import ShareAdminUploadLinks from './pages/share-admin/upload-links'; import SharedLibraries from './pages/shared-libs/shared-libs'; +import ShareWithOCM from './pages/share-with-ocm/shared-with-ocm'; +import OCMRepoDir from './pages/share-with-ocm/remote-dir-view'; import MyLibraries from './pages/my-libs/my-libs'; import MyLibDeleted from './pages/my-libs/my-libs-deleted'; import PublicSharedView from './pages/shared-with-all/public-shared-view'; @@ -38,6 +40,7 @@ const DraftsViewWrapper = MainContentWrapper(DraftsView); const StarredWrapper = MainContentWrapper(Starred); const LinkedDevicesWrapper = MainContentWrapper(LinkedDevices); const SharedLibrariesWrapper = MainContentWrapper(SharedLibraries); +const SharedWithOCMWrapper = MainContentWrapper(ShareWithOCM); const ShareAdminLibrariesWrapper = MainContentWrapper(ShareAdminLibraries); const ShareAdminFoldersWrapper = MainContentWrapper(ShareAdminFolders); const ShareAdminShareLinksWrapper = MainContentWrapper(ShareAdminShareLinks); @@ -257,9 +260,11 @@ class App extends Component { + + } + {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 ( +
+ + + + + + + + + + + {this.props.items.map((item, index) => { + return ( + + ); + })} + +
{gettext('Server URL')}{gettext('User Email')}{gettext('Permission')}
+
+ ); + } +} + +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 ( + + + + + + + + + + + + + + + + + + +
{gettext('Server URL')}{gettext('User Email')}{gettext('Permission')}
+ + + + + + + +
+ +
+ ); + } +} + +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 ( + + + + + + + + + + + + + {direntList.map((dirent, index) => { + return ; + })} + +
    {/*icon*/}{gettext('Name')}{/*operation*/}{gettext('Size')}{gettext('Last Update')}
    +
    + ); + } +} + +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 = ( + + + + + + + + + + + + + {items.map((item, index) => { + return ; + })} + +
    {gettext('Name')}{gettext('Shared from')}{gettext('At site')}{gettext('Time')}{/* operations */}
    + ); + + 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.icon_title} + {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, })