1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-05-10 00:47:19 +00:00
* ocm share local and inter-server api, ocm share page ()

* ocm share

* optimize code

* ocm repo ()

* ocm repo

* hide share if not repo owner

* optimize code

* fix All refresh page, hide upload if is r perm

* update permission check

* update return status code

* update code

* add receive user drop share
This commit is contained in:
Leo 2020-09-24 10:57:45 +08:00 committed by GitHub
parent f6efead60b
commit 267e9f525c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 1767 additions and 9 deletions

View File

@ -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 {
<ShareAdminShareLinksWrapper path={siteRoot + 'share-admin-share-links'} onShowSidePanel={this.onShowSidePanel} onSearchedClick={this.onSearchedClick} />
<ShareAdminUploadLinksWrapper path={siteRoot + 'share-admin-upload-links'} onShowSidePanel={this.onShowSidePanel} onSearchedClick={this.onSearchedClick} />
<SharedLibrariesWrapper path={siteRoot + 'shared-libs'} onShowSidePanel={this.onShowSidePanel} onSearchedClick={this.onSearchedClick} />
<SharedWithOCMWrapper path={siteRoot + 'shared-with-ocm'} onShowSidePanel={this.onShowSidePanel} onSearchedClick={this.onSearchedClick} />
<MyLibraries path={siteRoot + 'my-libs'} onShowSidePanel={this.onShowSidePanel} onSearchedClick={this.onSearchedClick} />
<MyLibDeleted path={siteRoot + 'my-libs/deleted/'} onSearchedClick={this.onSearchedClick} />
<LibContentView path={siteRoot + 'library/:repoID/*'} pathPrefix={this.state.pathPrefix} onMenuClick={this.onShowSidePanel} onTabNavClick={this.tabItemClick}/>
<OCMRepoDir path={siteRoot + 'remote-library/:providerID/:repoID/*'} pathPrefix={this.state.pathPrefix} onMenuClick={this.onShowSidePanel} onTabNavClick={this.tabItemClick}/>
<Groups path={siteRoot + 'groups'} onShowSidePanel={this.onShowSidePanel} onSearchedClick={this.onSearchedClick}/>
<Group
path={siteRoot + 'group/:groupID'}

View File

@ -1,12 +1,13 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { Modal, ModalHeader, ModalBody, TabContent, TabPane, Nav, NavItem, NavLink } from 'reactstrap';
import { gettext, username, canGenerateShareLink, canGenerateUploadLink, canInvitePeople, additionalShareDialogNote } from '../../utils/constants';
import { Modal, ModalHeader, ModalBody, TabContent, TabPane, Nav, NavItem, NavLink } from 'reactstrap';
import { gettext, username, canGenerateShareLink, canGenerateUploadLink, canInvitePeople, additionalShareDialogNote, enableOCM } from '../../utils/constants';
import ShareToUser from './share-to-user';
import ShareToGroup from './share-to-group';
import ShareToInvitePeople from './share-to-invite-people';
import GenerateShareLink from './generate-share-link';
import GenerateUploadLink from './generate-upload-link';
import ShareToOtherServer from './share-to-other-server';
import InternalLink from './internal-link';
import { seafileAPI } from '../../utils/seafile-api';
import Loading from '../loading';
@ -141,6 +142,13 @@ class ShareDialog extends React.Component {
}
</Fragment>
}
{enableOCM && itemType === 'library' && this.state.isRepoOwner &&
<NavItem>
<NavLink className={activeTab === 'shareToOtherServer' ? 'active' : ''} onClick={this.toggle.bind(this, 'shareToOtherServer')}>
{gettext('Share to other server')}
</NavLink>
</NavItem>
}
</Nav>
</div>
<div className="share-dialog-main">
@ -190,6 +198,11 @@ class ShareDialog extends React.Component {
}
</Fragment>
}
{enableOCM && itemType === 'library' && activeTab === 'shareToOtherServer' &&
<TabPane tabId="shareToOtherServer">
<ShareToOtherServer itemType={this.props.itemType} isGroupOwnedRepo={this.props.isGroupOwnedRepo} itemPath={this.props.itemPath} repoID={this.props.repoID} isRepoOwner={this.state.isRepoOwner} />
</TabPane>
}
</TabContent>
</div>
</Fragment>

View File

@ -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 (
<tr onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
<td>{item.to_sever_url}</td>
<td className="name">{item.to_user}</td>
<td>{Utils.sharePerms(item.permission)}</td>
{/* <td>
<SharePermissionEditor
isTextMode={true}
isEditIconShow={this.state.isOperationShow}
currentPermission={currentPermission}
permissions={this.props.permissions}
onPermissionChanged={this.onChangeUserPermission}
/>
</td> */}
<td>
<span
className={`sf2-icon-x3 action-icon ${this.state.isOperationShow ? '' : 'hide'}`}
onClick={this.deleteShareItem}
title={gettext('Delete')}
>
</span>
</td>
</tr>
);
}
}
class ShareList extends React.Component {
render() {
return (
<div className="share-list-container">
<table className="table-thead-hidden">
<thead>
<tr>
<th width="40%">{gettext('Server URL')}</th>
<th width="25%">{gettext('User Email')}</th>
<th width="20%">{gettext('Permission')}</th>
<th width="15%"></th>
</tr>
</thead>
<tbody>
{this.props.items.map((item, index) => {
return (
<ShareItem
key={index}
item={item}
deleteShareItem={this.props.deleteShareItem}
/>
);
})}
</tbody>
</table>
</div>
);
}
}
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 (
<Fragment>
<table>
<thead>
<tr>
<th width="40%">{gettext('Server URL')}</th>
<th width="25%">{gettext('User Email')}</th>
<th width="20%">{gettext('Permission')}</th>
<th width="15%"></th>
</tr>
</thead>
<tbody>
<tr>
<td>
<Input
value={toServerURL}
onChange={this.handleURLChange}
/>
</td>
<td>
<Input
value={toUser}
onChange={this.handleToUserChange}
/>
</td>
<td>
<SharePermissionEditor
isTextMode={false}
isEditIconShow={false}
currentPermission={permission}
permissions={this.permissions}
onPermissionChanged={this.setPermission}
/>
</td>
<td>
<Button onClick={this.startOCMShare}>{gettext('Submit')}</Button>
</td>
</tr>
</tbody>
</table>
<ShareList
items={ocmShares}
deleteShareItem={this.deleteShareItem}
/>
</Fragment>
);
}
}
ShareToOtherServer.propTypes = propTypes;
export default ShareToOtherServer;

View File

@ -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 {
</a>
{this.renderSharedGroups()}
</li>
{enableOCM &&
<li className="nav-item">
<Link to={siteRoot + 'shared-with-ocm/'} className={`nav-link ellipsis ${this.getActiveClass('shared-with-ocm')}`} title={gettext('Shared from other servers')} onClick={(e) => this.tabItemClick(e, 'shared-with-ocm')}>
<span className="sf2-icon-share" aria-hidden="true"></span>
<span className="nav-text">{gettext('Shared from other servers')}</span>
</Link>
</li>
}
</ul>

View File

@ -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 (
<Fragment>
<tr onMouseEnter={this.handleMouseOver} onMouseLeave={this.handleMouseOut}>
<td className="text-center"><img src={iconUrl} width="24" alt='' /></td>
<td>
{dirent.is_file ?
dirent.name :
<Link to="#" onClick={this.openFolder}>{dirent.name}</Link>
}
</td>
<td>
{isOpIconShown && dirent.is_file &&
<a href="#" className="op-icon sf2-icon-download" title={gettext('Download')} onClick={this.downloadDirent}></a>
}
</td>
<td>{Utils.bytesToSize(dirent.size)}</td>
<td>{moment(dirent.mtime).fromNow()}</td>
</tr>
</Fragment>
);
}
}
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 <Loading />;
}
if (errorMsg) {
return <p className="error text-center mt-4">{errorMsg}</p>;
}
return (
<Fragment>
<table className="table-hover">
<thead>
<tr>
<th width="5%">{/*icon*/}</th>
<th width="55%">{gettext('Name')}</th>
<th width="10%">{/*operation*/}</th>
<th width="15%">{gettext('Size')}</th>
<th width="15%">{gettext('Last Update')}</th>
</tr>
</thead>
<tbody>
{direntList.map((dirent, index) => {
return <DirentItem
key={index}
dirent={dirent}
openFolder={this.props.openFolder}
deleteDirent={this.props.deleteDirent}
downloadDirent={this.props.downloadDirent}
fromSystemRepo={this.props.fromSystemRepo}
/>;
})}
</tbody>
</table>
</Fragment>
);
}
}
DirContent.propTypes = propTypes;
export default DirContent;

View File

@ -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 (
<Fragment key={index}>
<span className="path-split">/</span>
<span className="path-file-name">{item}</span>
</Fragment>
);
} else {
nodePath += '/' + item;
return (
<Fragment key={index} >
<span className="path-split">/</span>
<a className="path-link" data-path={nodePath} onClick={this.onPathClick}>{item}</a>
</Fragment>
);
}
});
return pathElem;
}
render() {
let { currentPath, repoName } = this.props;
let pathElem = this.turnPathToLink(currentPath);
return (
<div className="path-container">
<Link to={siteRoot + 'shared-with-ocm/'} className="normal" onClick={(e) => this.props.onTabNavClick('shared-with-ocm')}>{gettext('All')}</Link>
<span className="path-split">/</span>
{(currentPath === '/' || currentPath === '') ?
<span className="path-repo-name">{repoName}</span>:
<a className="path-link" data-path="/" onClick={this.onPathClick}>{repoName}</a>
}
{pathElem}
</div>
);
}
}
DirPath.propTypes = propTypes;
export default DirPath;

View File

@ -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 (
<div className={`main-panel-north ${this.props.children ? 'border-left-show' : ''}`}>
<div className="cur-view-toolbar">
<span className="sf2-icon-menu side-nav-toggle hidden-md-up d-md-none" title="Side Nav Menu"></span>
<div className="operation">
{this.props.children}
</div>
</div>
<div className="common-toolbar">
<Account isAdminPanel={false} />
</div>
</div>
);
}
}
MainPanelTopbar.propTypes = propTypes;
export default MainPanelTopbar;

View File

@ -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 (
<Fragment>
<MainPanelTopbar>
<Fragment>
<input className="d-none" type="file" onChange={this.onFileInputChange} ref={this.fileInput} />
{userPerm === 'rw' &&
<Button className="operation-item" onClick={this.openFileInput}>{gettext('Upload')}</Button>
}
</Fragment>
</MainPanelTopbar>
<div className="main-panel-center flex-row">
<div className="cur-view-container">
<div className="cur-view-path align-items-center">
<DirPathBar
repoID={repoID}
repoName={repoName}
currentPath={path}
onPathClick={this.onPathClick}
onTabNavClick={this.props.onTabNavClick}
/>
</div>
<div className="cur-view-content">
<DirContent
loading={loading}
errorMsg={errorMsg}
direntList={direntList}
openFolder={this.openFolder}
deleteDirent={this.deleteDirent}
downloadDirent={this.downloadDirent}
/>
</div>
</div>
</div>
</Fragment>
);
}
}
export default DirView;

View File

@ -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 = (
<EmptyTip>
<h2>{gettext('No libraries have been shared with you')}</h2>
<p>{gettext('No libraries have been shared directly with you. You can find more shared libraries at "Shared with groups".')}</p>
</EmptyTip>
);
if (loading) {
return <Loading />;
} else if (errorMsg) {
return <p className="error text-center">{errorMsg}</p>;
} else {
const table = (
<table>
<thead>
<tr>
<th width="4%"></th>
<th width="20%">{gettext('Name')}</th>
<th width="20%">{gettext('Shared from')}</th>
<th width="26%">{gettext('At site')}</th>
<th width="20%">{gettext('Time')}</th>
<th width="10%">{/* operations */}</th>
</tr>
</thead>
<tbody>
{items.map((item, index) => {
return <Item
key={index}
item={item}
deleteShare={this.props.deleteShare}
/>;
})}
</tbody>
</table>
);
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 (
<Fragment>
<tr onMouseOver={this.handleMouseOver} onMouseOut={this.handleMouseOut}>
<td><img src={item.icon_url} title={item.icon_title} alt={item.icon_title} width="24" /></td>
<td><Link to={shareRepoUrl}>{item.repo_name}</Link></td>
<td>{item.from_user}</td>
<td>{item.from_server_url}</td>
<td title={moment(item.last_modified).format('llll')}>{moment(item.ctime).fromNow()}</td>
<td>
<a href="#" className={deleteIcon} title={gettext('Remove')} onClick={this.deleteShare}></a>
</td>
</tr>
</Fragment>
);
}
}
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 (
<Fragment>
<div className="main-panel-center">
<div className="cur-view-container">
<div className="cur-view-path">
<h3 className="sf-heading m-0">{gettext('Shared from other servers')}</h3>
</div>
<div className="cur-view-content">
<Content
loading={this.state.loading}
errorMsg={this.state.errorMsg}
items={this.state.items}
deleteShare={this.deleteShare}
/>
</div>
</div>
</div>
</Fragment>
);
}
}
export default SharedWithOCM;

View File

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

View File

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

View File

@ -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})

View File

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

View File

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

0
seahub/ocm/__init__.py Normal file
View File

View File

@ -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',
},
),
]

View File

107
seahub/ocm/models.py Normal file
View File

@ -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),
}

34
seahub/ocm/settings.py Normal file
View File

@ -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/',
}

View File

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

View File

@ -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',
]

View File

@ -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 %},

View File

@ -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<group_id>\d+)/$', react_fake_view, name="group"),
url(r'^library/(?P<repo_id>[-0-9a-f]{36})/$', react_fake_view, name="library_view"),
url(r'^library/(?P<repo_id>[-0-9a-f]{36})/(?P<repo_name>[^/]+)/(?P<path>.*)$', react_fake_view, name="lib_view"),
url(r'^remote-library/(?P<provider_id>[-0-9a-f]{36})/(?P<repo_id>[-0-9a-f]{36})/(?P<repo_name>[^/]+)/(?P<path>.*)$', 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<pk>\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<pk>\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<provider_id>[-0-9a-f]{36})/repos/(?P<repo_id>[-0-9a-f]{36})/dir/$', OCMReposDirView.as_view(), name='api-v2.1-ocm-repos-dir'),
url(r'api/v2.1/ocm/providers/(?P<provider_id>[-0-9a-f]{36})/repos/(?P<repo_id>[-0-9a-f]{36})/download-link/$', OCMReposDownloadLinkView.as_view(), name='api-v2.1-ocm-repos-dir'),
url(r'api/v2.1/ocm/providers/(?P<provider_id>[-0-9a-f]{36})/repos/(?P<repo_id>[-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'),

View File

@ -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,
})