1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-20 02:48:51 +00:00

clean orphan share/upload links (#5383)

* clean orphan share/upload links

* update style

* [share admin - links] fixup & improvements for 'deleted' tip & 'clean orphan links'

---------

Co-authored-by: llj <lingjun.li1@gmail.com>
This commit is contained in:
lian
2023-03-01 16:51:36 +08:00
committed by GitHub
parent bee8dc6fb1
commit 146c79ba44
9 changed files with 257 additions and 27 deletions

View File

@@ -45,8 +45,6 @@ const SharedWithOCMWrapper = MainContentWrapper(ShareWithOCM);
const OCMViaWebdavWrapper = MainContentWrapper(OCMViaWebdav);
const ShareAdminLibrariesWrapper = MainContentWrapper(ShareAdminLibraries);
const ShareAdminFoldersWrapper = MainContentWrapper(ShareAdminFolders);
const ShareAdminShareLinksWrapper = MainContentWrapper(ShareAdminShareLinks);
const ShareAdminUploadLinksWrapper = MainContentWrapper(ShareAdminUploadLinks);
class App extends Component {
@@ -259,8 +257,8 @@ class App extends Component {
<LinkedDevicesWrapper path={siteRoot + 'linked-devices'} onShowSidePanel={this.onShowSidePanel} onSearchedClick={this.onSearchedClick} />
<ShareAdminLibrariesWrapper path={siteRoot + 'share-admin-libs'} onShowSidePanel={this.onShowSidePanel} onSearchedClick={this.onSearchedClick} />
<ShareAdminFoldersWrapper path={siteRoot + 'share-admin-folders'} onShowSidePanel={this.onShowSidePanel} onSearchedClick={this.onSearchedClick} />
<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} />
<ShareAdminShareLinks path={siteRoot + 'share-admin-share-links'} onShowSidePanel={this.onShowSidePanel} onSearchedClick={this.onSearchedClick} />
<ShareAdminUploadLinks 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} />
<OCMViaWebdavWrapper path={siteRoot + 'ocm-via-webdav'} onShowSidePanel={this.onShowSidePanel} onSearchedClick={this.onSearchedClick} />

View File

@@ -0,0 +1,31 @@
import React from 'react';
import PropTypes from 'prop-types';
import CommonToolbar from './common-toolbar';
const propTypes = {
onShowSidePanel: PropTypes.func.isRequired,
onSearchedClick: PropTypes.func.isRequired,
searchPlaceholder: PropTypes.string,
children: PropTypes.object
};
class TopToolbar extends React.Component {
render() {
const { onShowSidePanel, onSearchedClick } = this.props;
return (
<div className="main-panel-north border-left-show">
<div className="cur-view-toolbar">
<span title="Side Nav Menu" onClick={onShowSidePanel} className="sf2-icon-menu side-nav-toggle hidden-md-up d-md-none">
</span>
{this.props.children}
</div>
<CommonToolbar searchPlaceholder={this.props.searchPlaceholder} onSearchedClick={onSearchedClick} />
</div>
);
}
}
TopToolbar.propTypes = propTypes;
export default TopToolbar;

View File

@@ -5,6 +5,7 @@ class ShareLink {
this.repo_name = object.repo_name;
this.path = object.path;
this.obj_name = object.obj_name;
this.obj_id = object.obj_id;
this.is_dir = object.is_dir;
this.can_edit = object.can_edit;
this.repo_folder_permission = object.repo_folder_permission;

View File

@@ -6,6 +6,7 @@ class UploadLink {
this.path = object.path;
this.link = object.link;
this.obj_name = object.obj_name;
this.obj_id = object.obj_id;
this.username = object.username;
this.ctime = object.ctime;
this.token = object.token;

View File

@@ -2,7 +2,7 @@ import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import { Link } from '@gatsbyjs/reach-router';
import moment from 'moment';
import { Dropdown, DropdownToggle, DropdownItem } from 'reactstrap';
import { Dropdown, DropdownToggle, DropdownItem, Button } from 'reactstrap';
import { seafileAPI } from '../../utils/seafile-api';
import { Utils } from '../../utils/utils';
import { isPro, gettext, siteRoot, canGenerateUploadLink } from '../../utils/constants';
@@ -14,6 +14,18 @@ import EmptyTip from '../../components/empty-tip';
import ShareLinkPermissionSelect from '../../components/dialog/share-link-permission-select';
import ShareAdminLink from '../../components/dialog/share-admin-link';
import SortOptionsDialog from '../../components/dialog/sort-options';
import CommonOperationConfirmationDialog from '../../components/dialog/common-operation-confirmation-dialog';
import TopToolbar from '../../components/toolbar/top-toolbar';
const contentPropTypes = {
loading: PropTypes.bool.isRequired,
errorMsg: PropTypes.string.isRequired,
items: PropTypes.array.isRequired,
sortBy: PropTypes.string.isRequired,
sortOrder: PropTypes.string.isRequired,
sortItems: PropTypes.func.isRequired,
onRemoveLink: PropTypes.func.isRequired
};
class Content extends Component {
@@ -88,6 +100,8 @@ class Content extends Component {
}
}
Content.propTypes = contentPropTypes;
const itemPropTypes = {
item: PropTypes.object.isRequired,
isDesktop: PropTypes.bool.isRequired,
@@ -202,6 +216,7 @@ class Item extends Component {
objUrl = `${siteRoot}lib/${item.repo_id}/file${Utils.encodePath(item.path)}`;
}
const deletedTip = item.obj_id === '' ? <span style={{color:'red'}}>{gettext('(deleted)')}</span> : null;
const desktopItem = (
<tr onMouseEnter={this.handleMouseOver} onMouseLeave={this.handleMouseOut} onFocus={this.handleMouseOver}>
<td><img src={iconUrl} width="24" alt="" /></td>
@@ -210,6 +225,7 @@ class Item extends Component {
<Link to={objUrl}>{item.obj_name}</Link> :
<a href={objUrl} target="_blank" rel="noreferrer">{item.obj_name}</a>
}
{deletedTip}
</td>
<td><Link to={`${siteRoot}library/${item.repo_id}/${encodeURIComponent(item.repo_name)}/`}>{item.repo_name}</Link></td>
{isPro &&
@@ -294,11 +310,17 @@ class Item extends Component {
Item.propTypes = itemPropTypes;
const propTypes = {
onShowSidePanel: PropTypes.func.isRequired,
onSearchedClick: PropTypes.func.isRequired
};
class ShareAdminShareLinks extends Component {
constructor(props) {
super(props);
this.state = {
isCleanOrphanShareLinksDialogOpen: false,
loading: true,
errorMsg: '',
items: [],
@@ -366,6 +388,10 @@ class ShareAdminShareLinks extends Component {
}
componentDidMount() {
this.listUserShareLinks();
}
listUserShareLinks() {
seafileAPI.listUserShareLinks().then((res) => {
let items = res.data.map(item => {
return new ShareLink(item);
@@ -402,9 +428,31 @@ class ShareAdminShareLinks extends Component {
});
}
toggleCleanOrphanShareLinksDialog = () => {
this.setState({isCleanOrphanShareLinksDialogOpen: !this.state.isCleanOrphanShareLinksDialogOpen});
}
cleanOrphanShareLinks = () => {
seafileAPI.cleanOrphanShareLinks().then(res => {
const newItems = this.state.items.filter(item => item.obj_id !== '');
this.setState({items: newItems});
toaster.success(gettext('Successfully cleaned orphan share links.'));
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
}
render() {
return (
<Fragment>
<TopToolbar
onShowSidePanel={this.props.onShowSidePanel}
onSearchedClick={this.props.onSearchedClick}
>
<Button className="operation-item d-none d-md-block" onClick={this.toggleCleanOrphanShareLinksDialog}>{gettext('Clean orphan share links')}</Button>
</TopToolbar>
<div className="main-panel-center">
<div className="cur-view-container">
<div className="cur-view-path share-upload-nav">
@@ -440,9 +488,20 @@ class ShareAdminShareLinks extends Component {
sortItems={this.sortItems}
/>
}
{this.state.isCleanOrphanShareLinksDialogOpen &&
<CommonOperationConfirmationDialog
title={gettext('Clean orphan share links')}
message={gettext('Are you sure you want to clean orphan share links?')}
executeOperation={this.cleanOrphanShareLinks}
confirmBtnText={gettext('Clean')}
toggleDialog={this.toggleCleanOrphanShareLinksDialog}
/>
}
</Fragment>
);
}
}
ShareAdminShareLinks.propTypes = propTypes;
export default ShareAdminShareLinks;

View File

@@ -1,7 +1,8 @@
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import { Link } from '@gatsbyjs/reach-router';
import moment from 'moment';
import { Dropdown, DropdownToggle, DropdownItem } from 'reactstrap';
import { Dropdown, DropdownToggle, DropdownItem, Button } from 'reactstrap';
import { gettext, siteRoot, canGenerateShareLink } from '../../utils/constants';
import { seafileAPI } from '../../utils/seafile-api';
import { Utils } from '../../utils/utils';
@@ -10,6 +11,15 @@ import Loading from '../../components/loading';
import EmptyTip from '../../components/empty-tip';
import UploadLink from '../../models/upload-link';
import ShareAdminLink from '../../components/dialog/share-admin-link';
import CommonOperationConfirmationDialog from '../../components/dialog/common-operation-confirmation-dialog';
import TopToolbar from '../../components/toolbar/top-toolbar';
const contentPropTypes = {
loading: PropTypes.bool.isRequired,
errorMsg: PropTypes.string.isRequired,
items: PropTypes.array.isRequired,
onRemoveLink: PropTypes.func.isRequired
};
class Content extends Component {
@@ -63,6 +73,14 @@ class Content extends Component {
}
}
Content.propTypes = contentPropTypes;
const itemPropTypes = {
isDesktop: PropTypes.bool.isRequired,
item: PropTypes.object.isRequired,
onRemoveLink: PropTypes.func.isRequired
};
class Item extends Component {
constructor(props) {
@@ -122,10 +140,11 @@ class Item extends Component {
const repoUrl = `${siteRoot}library/${item.repo_id}/${encodeURIComponent(item.repo_name)}`;
const objUrl = `${repoUrl}${Utils.encodePath(item.path)}`;
const deletedTip = item.obj_id === '' ? <span style={{color:'red'}}>{gettext('(deleted)')}</span> : null;
const desktopItem = (
<tr onMouseOver={this.handleMouseOver} onMouseOut={this.handleMouseOut} onFocus={this.handleMouseOver}>
<td><img src={iconUrl} alt="" width="24" /></td>
<td><Link to={objUrl}>{item.obj_name}</Link></td>
<td><Link to={objUrl}>{item.obj_name}</Link>{deletedTip}</td>
<td><Link to={repoUrl}>{item.repo_name}</Link></td>
<td>{item.view_cnt}</td>
<td>{this.renderExpiration()}</td>
@@ -180,11 +199,19 @@ class Item extends Component {
}
}
Item.propTypes = itemPropTypes;
const propTypes = {
onShowSidePanel: PropTypes.func.isRequired,
onSearchedClick: PropTypes.func.isRequired
};
class ShareAdminUploadLinks extends Component {
constructor(props) {
super(props);
this.state = {
isCleanOrphanUploadLinksDialogOpen: false,
loading: true,
errorMsg: '',
items: []
@@ -192,6 +219,10 @@ class ShareAdminUploadLinks extends Component {
}
componentDidMount() {
this.listUserUploadLinks();
}
listUserUploadLinks() {
seafileAPI.listUserUploadLinks().then((res) => {
let items = res.data.map(item => {
return new UploadLink(item);
@@ -222,30 +253,64 @@ class ShareAdminUploadLinks extends Component {
});
}
toggleCleanOrphanUploadLinksDialog = () => {
this.setState({isCleanOrphanUploadLinksDialogOpen: !this.state.isCleanOrphanUploadLinksDialogOpen});
}
cleanOrphanUploadLinks = () => {
seafileAPI.cleanOrphanUploadLinks().then(res => {
const newItems = this.state.items.filter(item => item.obj_id !== '');
this.setState({items: newItems});
toaster.success(gettext('Successfully cleaned orphan upload links.'));
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
}
render() {
return (
<div className="main-panel-center">
<div className="cur-view-container">
<div className="cur-view-path share-upload-nav">
<ul className="nav">
{canGenerateShareLink && (
<li className="nav-item"><Link to={`${siteRoot}share-admin-share-links/`} className="nav-link">{gettext('Share Links')}</Link></li>
)}
<li className="nav-item"><Link to={`${siteRoot}share-admin-upload-links/`} className="nav-link active">{gettext('Upload Links')}</Link></li>
</ul>
</div>
<div className="cur-view-content">
<Content
loading={this.state.loading}
errorMsg={this.state.errorMsg}
items={this.state.items}
onRemoveLink={this.onRemoveLink}
/>
<Fragment>
<TopToolbar
onShowSidePanel={this.props.onShowSidePanel}
onSearchedClick={this.props.onSearchedClick}
>
<Button className="operation-item d-none d-md-block" onClick={this.toggleCleanOrphanUploadLinksDialog}>{gettext('Clean orphan upload links')}</Button>
</TopToolbar>
<div className="main-panel-center">
<div className="cur-view-container">
<div className="cur-view-path share-upload-nav">
<ul className="nav">
{canGenerateShareLink && (
<li className="nav-item"><Link to={`${siteRoot}share-admin-share-links/`} className="nav-link">{gettext('Share Links')}</Link></li>
)}
<li className="nav-item"><Link to={`${siteRoot}share-admin-upload-links/`} className="nav-link active">{gettext('Upload Links')}</Link></li>
</ul>
</div>
<div className="cur-view-content">
<Content
loading={this.state.loading}
errorMsg={this.state.errorMsg}
items={this.state.items}
onRemoveLink={this.onRemoveLink}
/>
</div>
</div>
</div>
</div>
{this.state.isCleanOrphanUploadLinksDialogOpen &&
<CommonOperationConfirmationDialog
title={gettext('Clean orphan upload links')}
message={gettext('Are you sure you want to clean orphan upload links?')}
executeOperation={this.cleanOrphanUploadLinks}
confirmBtnText={gettext('Clean')}
toggleDialog={this.toggleCleanOrphanUploadLinksDialog}
/>
}
</Fragment>
);
}
}
ShareAdminUploadLinks.propTypes = propTypes;
export default ShareAdminUploadLinks;