mirror of
https://github.com/haiwen/seahub.git
synced 2025-07-31 22:57:47 +00:00
update repo trash (#6148)
* update repo trash * update code * select trash * update * update * merge clean trash * fix-uni-test-and-code-optimize * Update mysql.sql * code-optimize * update select * update sql * update UI * change trash dialog style * optimize code * fix code format * Update repo_trash.py * update * add clean trash Command * update * optimize code * support page * support frontend page * update * Update __init__.py * Update clean_repo_trash.py * Update clean_repo_trash.py * Update clean_repo_trash.py * Update trash-dialog.js * Update clean_repo_trash.py * set default by 90 * Update clean_repo_trash.py * update --------- Co-authored-by: 孙永强 <11704063+s-yongqiang@user.noreply.gitee.com> Co-authored-by: r350178982 <32759763+r350178982@users.noreply.github.com> Co-authored-by: Michael An <2331806369@qq.com>
This commit is contained in:
parent
cf7272c274
commit
0981a0dc99
432
frontend/src/components/dialog/trash-dialog.js
Normal file
432
frontend/src/components/dialog/trash-dialog.js
Normal file
@ -0,0 +1,432 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { navigate } from '@gatsbyjs/reach-router';
|
||||
import { Modal, ModalHeader, ModalBody } from 'reactstrap';
|
||||
import moment from 'moment';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import {gettext, siteRoot, enableUserCleanTrash, username} from '../../utils/constants';
|
||||
import { seafileAPI } from '../../utils/seafile-api';
|
||||
import { repotrashAPI } from '../../utils/repo-trash-api';
|
||||
import ModalPortal from '../../components/modal-portal';
|
||||
import toaster from '../../components/toast';
|
||||
import CleanTrash from '../../components/dialog/clean-trash';
|
||||
import Paginator from '../paginator';
|
||||
|
||||
import '../../css/toolbar.css';
|
||||
import '../../css/search.css';
|
||||
import '../../css/trash-dialog.css';
|
||||
|
||||
const propTypes = {
|
||||
repoID: PropTypes.string.isRequired,
|
||||
currentRepoInfo: PropTypes.object.isRequired,
|
||||
showTrashDialog: PropTypes.bool.isRequired,
|
||||
toggleTrashDialog: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
class TrashDialog extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isLoading: true,
|
||||
errorMsg: '',
|
||||
items: [],
|
||||
scanStat: null,
|
||||
more: false,
|
||||
isCleanTrashDialogOpen: false,
|
||||
trashType: 0,
|
||||
isOldTrashDialogOpen: false,
|
||||
currentPage: 1,
|
||||
perPage: 100,
|
||||
hasNextPage: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.getItems2();
|
||||
}
|
||||
|
||||
getItems2 = (page) => {
|
||||
repotrashAPI.getRepoFolderTrash2(this.props.repoID, page, this.state.perPage).then((res) => {
|
||||
const { items, total_count } = res.data;
|
||||
if (!page){
|
||||
page = 1;
|
||||
}
|
||||
this.setState({
|
||||
currentPage: page,
|
||||
hasNextPage: total_count - page * this.state.perPage > 0,
|
||||
isLoading: false,
|
||||
items: items,
|
||||
more: false
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
onSearchedClick = (selectedItem) => {
|
||||
if (selectedItem.is_dir === true) {
|
||||
let url = siteRoot + 'library/' + selectedItem.repo_id + '/' + selectedItem.repo_name + selectedItem.path;
|
||||
navigate(url, {repalce: true});
|
||||
} else {
|
||||
let url = siteRoot + 'lib/' + selectedItem.repo_id + '/file' + Utils.encodePath(selectedItem.path);
|
||||
let newWindow = window.open('about:blank');
|
||||
newWindow.location.href = url;
|
||||
}
|
||||
};
|
||||
|
||||
resetPerPage = (perPage) => {
|
||||
this.setState({
|
||||
perPage: perPage
|
||||
}, () => {
|
||||
this.getItems2(1);
|
||||
});
|
||||
};
|
||||
cleanTrash = () => {
|
||||
this.toggleCleanTrashDialog();
|
||||
};
|
||||
|
||||
toggleCleanTrashDialog = () => {
|
||||
this.setState({
|
||||
isCleanTrashDialogOpen: !this.state.isCleanTrashDialogOpen
|
||||
});
|
||||
};
|
||||
|
||||
refreshTrash2 = () => {
|
||||
this.setState({
|
||||
isLoading: true,
|
||||
errorMsg: '',
|
||||
items: [],
|
||||
scanStat: null,
|
||||
more: false,
|
||||
showFolder: false
|
||||
});
|
||||
this.getItems2();
|
||||
};
|
||||
|
||||
renderFolder = (commitID, baseDir, folderPath) => {
|
||||
this.setState({
|
||||
showFolder: true,
|
||||
commitID: commitID,
|
||||
baseDir: baseDir,
|
||||
folderPath: folderPath,
|
||||
folderItems: [],
|
||||
isLoading: true
|
||||
});
|
||||
|
||||
seafileAPI.listCommitDir(this.props.repoID, commitID, `${baseDir.substr(0, baseDir.length - 1)}${folderPath}`).then((res) => {
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
folderItems: res.data.dirent_list
|
||||
});
|
||||
}).catch((error) => {
|
||||
if (error.response) {
|
||||
if (error.response.status == 403) {
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
errorMsg: gettext('Permission denied')
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
errorMsg: gettext('Error')
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
errorMsg: gettext('Please check the network.')
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { showTrashDialog, toggleTrashDialog } = this.props;
|
||||
const { isCleanTrashDialogOpen, showFolder } = this.state;
|
||||
const isRepoAdmin = this.props.currentRepoInfo.owner_email === username || this.props.currentRepoInfo.is_admin;
|
||||
const repoFolderName = this.props.currentRepoInfo.repo_name;
|
||||
const oldTrashUrl = siteRoot + 'repo/' + this.props.repoID + '/trash/';
|
||||
let title = gettext('{placeholder} Trash');
|
||||
title = title.replace('{placeholder}', '<span class="op-target text-truncate mx-1">' + Utils.HTMLescape(repoFolderName) + '</span>');
|
||||
|
||||
return (
|
||||
<Modal className="trash-dialog" isOpen={showTrashDialog} toggle={toggleTrashDialog}>
|
||||
<ModalHeader
|
||||
close={
|
||||
<>
|
||||
<a className="trash-dialog-old-page" href={oldTrashUrl}>{gettext('Visit old version page')}</a>
|
||||
{(enableUserCleanTrash && !showFolder && isRepoAdmin) &&
|
||||
<button className="btn btn-secondary clean flex-shrink-0 ml-4" onClick={this.cleanTrash}>{gettext('Clean')}</button>
|
||||
}
|
||||
<span aria-hidden="true" className="trash-dialog-close-icon sf3-font sf3-font-x-01 ml-4" onClick={toggleTrashDialog}></span>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div dangerouslySetInnerHTML={{__html: title}}></div>
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<Content
|
||||
data={this.state}
|
||||
repoID={this.props.repoID}
|
||||
getMore={this.getMore}
|
||||
currentPage={this.state.currentPage}
|
||||
curPerPage={this.state.perPage}
|
||||
hasNextPage={this.state.hasNextPage}
|
||||
renderFolder={this.renderFolder}
|
||||
getListByPage={this.getItems2}
|
||||
resetPerPage={this.resetPerPage}
|
||||
/>
|
||||
{isCleanTrashDialogOpen &&
|
||||
<ModalPortal>
|
||||
<CleanTrash
|
||||
repoID={this.props.repoID}
|
||||
refreshTrash={this.refreshTrash2}
|
||||
toggleDialog={this.toggleCleanTrashDialog}
|
||||
/>
|
||||
</ModalPortal>
|
||||
}
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Content extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.theadData = [
|
||||
{width: '5%', text: ''},
|
||||
{width: '20%', text: gettext('Name')},
|
||||
{width: '40%', text: gettext('Original path')},
|
||||
{width: '12%', text: gettext('Delete Time')},
|
||||
{width: '13%', text: gettext('Size')},
|
||||
{width: '10%', text: ''}
|
||||
];
|
||||
}
|
||||
|
||||
getPreviousPage = () => {
|
||||
this.props.getListByPage(this.props.currentPage - 1);
|
||||
};
|
||||
|
||||
getNextPage = () => {
|
||||
this.props.getListByPage(this.props.currentPage + 1);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { items, showFolder, commitID, baseDir, folderPath, folderItems } = this.props.data;
|
||||
const {
|
||||
curPerPage, currentPage, hasNextPage
|
||||
} = this.props;
|
||||
return (
|
||||
<React.Fragment>
|
||||
<table className="table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
{this.theadData.map((item, index) => {
|
||||
return <th key={index} width={item.width}>{item.text}</th>;
|
||||
})}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{showFolder ?
|
||||
folderItems.map((item, index) => {
|
||||
return <FolderItem
|
||||
key={index}
|
||||
item={item}
|
||||
repoID={this.props.repoID}
|
||||
commitID={commitID}
|
||||
baseDir={baseDir}
|
||||
folderPath={folderPath}
|
||||
renderFolder={this.props.renderFolder}
|
||||
/>;
|
||||
}) :
|
||||
items.map((item, index) => {
|
||||
return <Item
|
||||
key={index}
|
||||
repoID={this.props.repoID}
|
||||
item={item}
|
||||
renderFolder={this.props.renderFolder}
|
||||
/>;
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
<Paginator
|
||||
gotoPreviousPage={this.getPreviousPage}
|
||||
gotoNextPage={this.getNextPage}
|
||||
currentPage={currentPage}
|
||||
hasNextPage={hasNextPage}
|
||||
curPerPage={curPerPage}
|
||||
resetPerPage={this.props.resetPerPage}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Content.propTypes = {
|
||||
data: PropTypes.object.isRequired,
|
||||
getMore: PropTypes.func,
|
||||
renderFolder: PropTypes.func.isRequired,
|
||||
repoID: PropTypes.string.isRequired,
|
||||
getListByPage: PropTypes.func.isRequired,
|
||||
resetPerPage: PropTypes.func.isRequired,
|
||||
currentPage: PropTypes.number.isRequired,
|
||||
curPerPage: PropTypes.number.isRequired,
|
||||
hasNextPage: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
|
||||
class Item extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
restored: false,
|
||||
isIconShown: false
|
||||
};
|
||||
}
|
||||
|
||||
handleMouseOver = () => {
|
||||
this.setState({isIconShown: true});
|
||||
};
|
||||
|
||||
handleMouseOut = () => {
|
||||
this.setState({isIconShown: false});
|
||||
};
|
||||
|
||||
restoreItem = (e) => {
|
||||
e.preventDefault();
|
||||
const item = this.props.item;
|
||||
const { commit_id, parent_dir, obj_name } = item;
|
||||
const path = parent_dir + obj_name;
|
||||
const request = item.is_dir ?
|
||||
seafileAPI.restoreFolder(this.props.repoID, commit_id, path) :
|
||||
seafileAPI.restoreFile(this.props.repoID, commit_id, path);
|
||||
request.then((res) => {
|
||||
this.setState({
|
||||
restored: true
|
||||
});
|
||||
toaster.success(gettext('Successfully restored 1 item.'));
|
||||
}).catch((error) => {
|
||||
let errorMsg = '';
|
||||
if (error.response) {
|
||||
errorMsg = error.response.data.error_msg || gettext('Error');
|
||||
} else {
|
||||
errorMsg = gettext('Please check the network.');
|
||||
}
|
||||
toaster.danger(errorMsg);
|
||||
});
|
||||
};
|
||||
|
||||
renderFolder = (e) => {
|
||||
e.preventDefault();
|
||||
const item = this.props.item;
|
||||
this.props.renderFolder(item.commit_id, item.parent_dir, Utils.joinPath('/', item.obj_name));
|
||||
};
|
||||
|
||||
render() {
|
||||
const item = this.props.item;
|
||||
const { restored, isIconShown } = this.state;
|
||||
|
||||
if (restored) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return item.is_dir ? (
|
||||
<tr onMouseOver={this.handleMouseOver} onMouseOut={this.handleMouseOut} onFocus={this.handleMouseOver}>
|
||||
<td className="text-center"><img src={Utils.getFolderIconUrl()} alt={gettext('Directory')} width="24" /></td>
|
||||
<td><a href="#" onClick={this.renderFolder}>{item.obj_name}</a></td>
|
||||
<td>{item.parent_dir}</td>
|
||||
<td title={moment(item.deleted_time).format('LLLL')}>{moment(item.deleted_time).format('YYYY-MM-DD')}</td>
|
||||
<td></td>
|
||||
<td>
|
||||
<a href="#" className={isIconShown ? '': 'invisible'} onClick={this.restoreItem} role="button">{gettext('Restore')}</a>
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
<tr onMouseOver={this.handleMouseOver} onMouseOut={this.handleMouseOut} onFocus={this.handleMouseOver}>
|
||||
<td className="text-center"><img src={Utils.getFileIconUrl(item.obj_name)} alt={gettext('File')} width="24" /></td>
|
||||
<td><a href={`${siteRoot}repo/${this.props.repoID}/trash/files/?obj_id=${item.obj_id}&commit_id=${item.commit_id}&base=${encodeURIComponent(item.parent_dir)}&p=${encodeURIComponent('/' + item.obj_name)}`} target="_blank" rel="noreferrer">{item.obj_name}</a></td>
|
||||
<td>{item.parent_dir}</td>
|
||||
<td title={moment(item.deleted_time).format('LLLL')}>{moment(item.deleted_time).format('YYYY-MM-DD')}</td>
|
||||
<td>{Utils.bytesToSize(item.size)}</td>
|
||||
<td>
|
||||
<a href="#" className={isIconShown ? '': 'invisible'} onClick={this.restoreItem} role="button">{gettext('Restore')}</a>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Item.propTypes = {
|
||||
item: PropTypes.object.isRequired,
|
||||
renderFolder: PropTypes.func.isRequired,
|
||||
repoID: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
class FolderItem extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isIconShown: false
|
||||
};
|
||||
}
|
||||
|
||||
handleMouseOver = () => {
|
||||
this.setState({isIconShown: true});
|
||||
};
|
||||
|
||||
handleMouseOut = () => {
|
||||
this.setState({isIconShown: false});
|
||||
};
|
||||
|
||||
renderFolder = (e) => {
|
||||
e.preventDefault();
|
||||
const item = this.props.item;
|
||||
const { commitID, baseDir, folderPath } = this.props;
|
||||
this.props.renderFolder(commitID, baseDir, Utils.joinPath(folderPath, item.name));
|
||||
};
|
||||
|
||||
render() {
|
||||
const item = this.props.item;
|
||||
const { commitID, baseDir, folderPath } = this.props;
|
||||
|
||||
return item.type == 'dir' ? (
|
||||
<tr onMouseOver={this.handleMouseOver} onMouseOut={this.handleMouseOut}>
|
||||
<td className="text-center"><img src={Utils.getFolderIconUrl()} alt={gettext('Directory')} width="24" /></td>
|
||||
<td><a href="#" onClick={this.renderFolder}>{item.name}</a></td>
|
||||
<td>{item.parent_dir}</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
) : (
|
||||
<tr onMouseOver={this.handleMouseOver} onMouseOut={this.handleMouseOut}>
|
||||
<td className="text-center">
|
||||
<img src={Utils.getFileIconUrl(item.name)} alt={gettext('File')} width="24" />
|
||||
</td>
|
||||
<td>
|
||||
<a href={`${siteRoot}repo/${this.props.repoID}/trash/files/?obj_id=${item.obj_id}&commit_id=${commitID}&base=${encodeURIComponent(baseDir)}&p=${encodeURIComponent(Utils.joinPath(folderPath, item.name))}`} target="_blank" rel="noreferrer">{item.name}</a>
|
||||
</td>
|
||||
<td>{item.parent_dir}</td>
|
||||
<td></td>
|
||||
<td>{Utils.bytesToSize(item.size)}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FolderItem.propTypes = {
|
||||
item: PropTypes.object.isRequired,
|
||||
commitID: PropTypes.string.isRequired,
|
||||
repoID: PropTypes.string.isRequired,
|
||||
baseDir: PropTypes.string.isRequired,
|
||||
folderPath: PropTypes.string.isRequired,
|
||||
renderFolder: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
TrashDialog.propTypes = propTypes;
|
||||
|
||||
export default TrashDialog;
|
@ -281,6 +281,7 @@ class DirColumnNav extends React.Component {
|
||||
<DirOthers
|
||||
repoID={this.props.repoID}
|
||||
userPerm={this.props.userPerm}
|
||||
currentRepoInfo={this.props.currentRepoInfo}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
@ -1,20 +1,23 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { gettext, siteRoot } from '../../utils/constants';
|
||||
import TreeSection from '../tree-section';
|
||||
import TrashDialog from '../dialog/trash-dialog';
|
||||
|
||||
const DirOthers = ({ userPerm, repoID }) => {
|
||||
|
||||
const DirOthers = ({ userPerm, repoID, currentRepoInfo }) => {
|
||||
const [showTrashDialog, setShowTrashDialog] = useState(false);
|
||||
let trashUrl = null;
|
||||
const historyUrl = siteRoot + 'repo/history/' + repoID + '/';
|
||||
if (userPerm === 'rw') {
|
||||
trashUrl = siteRoot + 'repo/' + repoID + '/trash/';
|
||||
}
|
||||
|
||||
const toggleTrashDialog = () => {
|
||||
setShowTrashDialog(!showTrashDialog);
|
||||
};
|
||||
return (
|
||||
<TreeSection title={gettext('Others')} className="dir-others">
|
||||
{trashUrl &&
|
||||
<div className='tree-node-inner text-nowrap' title={gettext('Trash')} onClick={() => location.href = trashUrl}>
|
||||
<div className='tree-node-inner text-nowrap' title={gettext('Trash')} onClick={toggleTrashDialog}>
|
||||
<div className="tree-node-text">{gettext('Trash')}</div>
|
||||
<div className="left-icon">
|
||||
<div className="tree-node-icon">
|
||||
@ -31,6 +34,14 @@ const DirOthers = ({ userPerm, repoID }) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{showTrashDialog && (
|
||||
<TrashDialog
|
||||
repoID={repoID}
|
||||
currentRepoInfo={currentRepoInfo}
|
||||
showTrashDialog={showTrashDialog}
|
||||
toggleTrashDialog={toggleTrashDialog}
|
||||
/>
|
||||
)}
|
||||
</TreeSection>
|
||||
);
|
||||
};
|
||||
@ -38,6 +49,7 @@ const DirOthers = ({ userPerm, repoID }) => {
|
||||
DirOthers.propTypes = {
|
||||
userPerm: PropTypes.string,
|
||||
repoID: PropTypes.string,
|
||||
currentRepoInfo: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default DirOthers;
|
||||
|
44
frontend/src/css/trash-dialog.css
Normal file
44
frontend/src/css/trash-dialog.css
Normal file
@ -0,0 +1,44 @@
|
||||
.trash-dialog {
|
||||
max-width: 1100px;
|
||||
}
|
||||
|
||||
.trash-dialog .modal-header {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.trash-dialog .modal-header .trash-dialog-old-page {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.trash-dialog .modal-header .trash-dialog-close-icon {
|
||||
color: #000;
|
||||
opacity: 0.5;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.trash-dialog .modal-header .trash-dialog-close-icon:hover {
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.trash-dialog .modal-header .clean {
|
||||
height: 30px;
|
||||
line-height: 28px;
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
|
||||
.trash-dialog .modal-body {
|
||||
height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.trash-dialog .modal-body .more {
|
||||
background: #efefef;
|
||||
border: 0;
|
||||
color: #777;
|
||||
}
|
||||
|
||||
.trash-dialog .modal-body .more:hover {
|
||||
color: #000;
|
||||
background: #dfdfdf;
|
||||
}
|
@ -21,7 +21,7 @@ const {
|
||||
repoID,
|
||||
repoFolderName,
|
||||
path,
|
||||
enableClean,
|
||||
enableUserCleanTrash,
|
||||
isRepoAdmin
|
||||
} = window.app.pageOptions;
|
||||
|
||||
@ -35,7 +35,7 @@ class RepoFolderTrash extends React.Component {
|
||||
items: [],
|
||||
scanStat: null,
|
||||
more: false,
|
||||
isCleanTrashDialogOpen: false
|
||||
isCleanTrashDialogOpen: false,
|
||||
};
|
||||
}
|
||||
|
||||
@ -204,7 +204,7 @@ class RepoFolderTrash extends React.Component {
|
||||
</a>
|
||||
<div className="d-flex justify-content-between align-items-center op-bar">
|
||||
<p className="m-0 text-truncate d-flex"><span className="mr-1">{gettext('Current path: ')}</span>{showFolder ? this.renderFolderPath() : <span className="text-truncate" title={repoFolderName}>{repoFolderName}</span>}</p>
|
||||
{(path == '/' && enableClean && !showFolder && isRepoAdmin) &&
|
||||
{(path == '/' && enableUserCleanTrash && !showFolder && isRepoAdmin) &&
|
||||
<button className="btn btn-secondary clean flex-shrink-0 ml-4" onClick={this.cleanTrash}>{gettext('Clean')}</button>
|
||||
}
|
||||
</div>
|
||||
|
@ -70,6 +70,7 @@ export const maxFileName = window.app.pageOptions.maxFileName;
|
||||
export const canPublishRepo = window.app.pageOptions.canPublishRepo;
|
||||
export const enableEncryptedLibrary = window.app.pageOptions.enableEncryptedLibrary;
|
||||
export const enableRepoHistorySetting = window.app.pageOptions.enableRepoHistorySetting;
|
||||
export const enableUserCleanTrash = window.app.pageOptions.enableUserCleanTrash;
|
||||
export const isSystemStaff = window.app.pageOptions.isSystemStaff;
|
||||
export const thumbnailSizeForOriginal = window.app.pageOptions.thumbnailSizeForOriginal;
|
||||
export const repoPasswordMinLength = window.app.pageOptions.repoPasswordMinLength;
|
||||
|
51
frontend/src/utils/repo-trash-api.js
Normal file
51
frontend/src/utils/repo-trash-api.js
Normal file
@ -0,0 +1,51 @@
|
||||
import axios from 'axios';
|
||||
import cookie from 'react-cookies';
|
||||
import { siteRoot } from './constants';
|
||||
|
||||
class RepotrashAPI {
|
||||
|
||||
init({ server, username, password, token }) {
|
||||
this.server = server;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.token = token; //none
|
||||
if (this.token && this.server) {
|
||||
this.req = axios.create({
|
||||
baseURL: this.server,
|
||||
headers: { 'Authorization': 'Token ' + this.token },
|
||||
});
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
initForSeahubUsage({ siteRoot, xcsrfHeaders }) {
|
||||
if (siteRoot && siteRoot.charAt(siteRoot.length - 1) === '/') {
|
||||
var server = siteRoot.substring(0, siteRoot.length - 1);
|
||||
this.server = server;
|
||||
} else {
|
||||
this.server = siteRoot;
|
||||
}
|
||||
|
||||
this.req = axios.create({
|
||||
headers: {
|
||||
'X-CSRFToken': xcsrfHeaders,
|
||||
}
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
getRepoFolderTrash2(repoID, page, per_page) {
|
||||
const url = this.server + '/api/v2.1/repos/' + repoID + '/trash2/';
|
||||
let params = {
|
||||
page: page || 1,
|
||||
per_page: per_page
|
||||
};
|
||||
return this.req.get(url, {params: params});
|
||||
}
|
||||
}
|
||||
|
||||
let repotrashAPI = new RepotrashAPI();
|
||||
let xcsrfHeaders = cookie.load('sfcsrftoken');
|
||||
repotrashAPI.initForSeahubUsage({ siteRoot, xcsrfHeaders });
|
||||
|
||||
export { repotrashAPI };
|
@ -1,6 +1,8 @@
|
||||
# Copyright (c) 2012-2016 Seafile Ltd.
|
||||
import stat
|
||||
import logging
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
from rest_framework.authentication import SessionAuthentication
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
@ -13,6 +15,7 @@ from seahub.api2.authentication import TokenAuthentication
|
||||
from seahub.api2.utils import api_error
|
||||
|
||||
from seahub.signals import clean_up_repo_trash
|
||||
from seahub.utils import get_trash_records
|
||||
from seahub.utils.timeutils import timestamp_to_isoformat_timestr
|
||||
from seahub.utils.repo import get_repo_owner, is_repo_admin
|
||||
from seahub.views import check_folder_permission
|
||||
@ -24,7 +27,7 @@ from pysearpc import SearpcError
|
||||
from constance import config
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
SHOW_REPO_TRASH_DAYS = 90
|
||||
|
||||
class RepoTrash(APIView):
|
||||
|
||||
@ -303,3 +306,88 @@ class RepoTrashRevertDirents(APIView):
|
||||
})
|
||||
|
||||
return Response(result)
|
||||
|
||||
|
||||
class RepoTrash2(APIView):
|
||||
|
||||
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
||||
permission_classes = (IsAuthenticated, )
|
||||
throttle_classes = (UserRateThrottle, )
|
||||
|
||||
def get_item_info(self, trash_item):
|
||||
|
||||
item_info = {
|
||||
'parent_dir': '/' if trash_item.path == '/' else trash_item.path,
|
||||
'obj_name': trash_item.obj_name,
|
||||
'deleted_time': timestamp_to_isoformat_timestr(int(trash_item.delete_time.timestamp())),
|
||||
'commit_id': trash_item.commit_id,
|
||||
}
|
||||
|
||||
if trash_item.obj_type == 'dir':
|
||||
is_dir = True
|
||||
else:
|
||||
is_dir = False
|
||||
|
||||
item_info['is_dir'] = is_dir
|
||||
item_info['size'] = trash_item.size if not is_dir else ''
|
||||
item_info['obj_id'] = trash_item.obj_id if not is_dir else ''
|
||||
|
||||
return item_info
|
||||
|
||||
def get(self, request, repo_id):
|
||||
""" Return deleted files/dirs of a repo/folder
|
||||
|
||||
Permission checking:
|
||||
1. all authenticated user can perform this action.
|
||||
"""
|
||||
|
||||
path = '/'
|
||||
# resource check
|
||||
repo = seafile_api.get_repo(repo_id)
|
||||
if not repo:
|
||||
error_msg = 'Library %s not found.' % repo_id
|
||||
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||
|
||||
try:
|
||||
current_page = int(request.GET.get('page', '1'))
|
||||
per_page = int(request.GET.get('per_page', '100'))
|
||||
except ValueError:
|
||||
current_page = 1
|
||||
per_page = 100
|
||||
start = (current_page - 1) * per_page
|
||||
limit = per_page
|
||||
try:
|
||||
dir_id = seafile_api.get_dir_id_by_path(repo_id, path)
|
||||
except SearpcError as e:
|
||||
logger.error(e)
|
||||
error_msg = 'Internal Server Error'
|
||||
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||
|
||||
if not dir_id:
|
||||
error_msg = 'Folder %s not found.' % path
|
||||
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||
|
||||
# permission check
|
||||
if check_folder_permission(request, repo_id, path) is None:
|
||||
error_msg = 'Permission denied.'
|
||||
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
|
||||
|
||||
try:
|
||||
deleted_entries, total_count = get_trash_records(repo_id, SHOW_REPO_TRASH_DAYS, start, limit)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
error_msg = 'Internal Server Error'
|
||||
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||
|
||||
items = []
|
||||
if len(deleted_entries) >= 1:
|
||||
for item in deleted_entries:
|
||||
item_info = self.get_item_info(item)
|
||||
items.append(item_info)
|
||||
|
||||
result = {
|
||||
'items': items,
|
||||
'total_count': total_count
|
||||
}
|
||||
|
||||
return Response(result)
|
||||
|
41
seahub/base/management/commands/clean_repo_trash.py
Normal file
41
seahub/base/management/commands/clean_repo_trash.py
Normal file
@ -0,0 +1,41 @@
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from seahub.utils import SeafEventsSession
|
||||
from seafevents import seafevents_api
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Clear repo trash within the specified time'
|
||||
label = 'clean_repo_trash'
|
||||
|
||||
def print_msg(self, msg):
|
||||
self.stdout.write('[%s] %s\n' % (datetime.now(), msg))
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('--keep-days', help='keep days', type=int, default=90)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
days = options.get('keep_days')
|
||||
if days < 0:
|
||||
self.print_msg('keep-days cannot be set to nagative number')
|
||||
return
|
||||
logger.info('Start clean repo trash...')
|
||||
self.print_msg('Start clean repo trash...')
|
||||
self.do_action(days)
|
||||
self.print_msg('Finish clean repo trash.\n')
|
||||
logger.info('Finish clean repo trash.\n')
|
||||
|
||||
def do_action(self, days):
|
||||
try:
|
||||
session = SeafEventsSession()
|
||||
seafevents_api.clean_up_all_repo_trash(session, days)
|
||||
except Exception as e:
|
||||
logger.debug('Clean up repo trash error: %s' % e)
|
||||
self.print_msg('Clean up repo trash error: %s' % e)
|
||||
return
|
||||
|
||||
logger.info('Successfully cleared repo trash older than %s days' % days)
|
||||
self.print_msg('Successfully cleared repo trash older than %s days' % days)
|
@ -129,7 +129,12 @@ try:
|
||||
|
||||
from .utils import SeafEventsSession
|
||||
session = SeafEventsSession()
|
||||
seafevents_api.save_user_activity(session, record)
|
||||
try:
|
||||
seafevents_api.save_user_activity(session, record)
|
||||
seafevents_api.clean_up_repo_trash(session, repo_id, days)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
|
||||
session.close()
|
||||
|
||||
def repo_restored_cb(sender, **kwargs):
|
||||
|
@ -109,6 +109,7 @@
|
||||
canPublishRepo: {% if user.permissions.can_publish_repo %} true {% else %} false {% endif %},
|
||||
enableEncryptedLibrary: {% if enable_encrypted_library %} true {% else %} false {% endif %},
|
||||
enableRepoHistorySetting: {% if enable_repo_history_setting %} true {% else %} false {% endif %},
|
||||
enableUserCleanTrash: {% if enable_user_clean_trash %} true {% else %} false {% endif %},
|
||||
isSystemStaff: {% if request.user.is_staff %} true {% else %} false {% endif %},
|
||||
thumbnailSizeForOriginal: {{ thumbnail_size_for_original }},
|
||||
repoPasswordMinLength: {{repo_password_min_length}},
|
||||
|
@ -15,7 +15,7 @@ window.app.pageOptions = {
|
||||
repoID: '{{repo.id}}',
|
||||
repoFolderName: '{{repo_folder_name|escapejs}}',
|
||||
path: '{{path|escapejs}}',
|
||||
enableClean: {% if enable_clean %} true {% else %} false {% endif %},
|
||||
enableUserCleanTrash: {% if enable_user_clean_trash %} true {% else %} false {% endif %},
|
||||
isRepoAdmin: {% if is_repo_admin %} true {% else %} false {% endif %}
|
||||
};
|
||||
</script>
|
||||
|
@ -64,7 +64,7 @@ from seahub.api2.endpoints.file_history import FileHistoryView, NewFileHistoryVi
|
||||
from seahub.api2.endpoints.dir import DirView, DirDetailView
|
||||
from seahub.api2.endpoints.file_tag import FileTagView
|
||||
from seahub.api2.endpoints.file_tag import FileTagsView
|
||||
from seahub.api2.endpoints.repo_trash import RepoTrash, RepoTrashRevertDirents
|
||||
from seahub.api2.endpoints.repo_trash import RepoTrash, RepoTrashRevertDirents, RepoTrash2
|
||||
from seahub.api2.endpoints.repo_commit import RepoCommitView
|
||||
from seahub.api2.endpoints.repo_commit_dir import RepoCommitDirView
|
||||
from seahub.api2.endpoints.repo_commit_revert import RepoCommitRevertView
|
||||
@ -430,6 +430,7 @@ urlpatterns = [
|
||||
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/commits/(?P<commit_id>[0-9a-f]{40})/revert/$', RepoCommitRevertView.as_view(), name='api-v2.1-repo-commit-revert'),
|
||||
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/dir/detail/$', DirDetailView.as_view(), name='api-v2.1-dir-detail-view'),
|
||||
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/trash/$', RepoTrash.as_view(), name='api-v2.1-repo-trash'),
|
||||
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/trash2/$', RepoTrash2.as_view(), name='api-v2.1-repo-trash2'),
|
||||
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/trash/revert-dirents/$', RepoTrashRevertDirents.as_view(), name='api-v2.1-repo-trash-revert-dirents'),
|
||||
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/history/$', RepoHistory.as_view(), name='api-v2.1-repo-history'),
|
||||
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/set-password/$', RepoSetPassword.as_view(), name="api-v2.1-repo-set-password"),
|
||||
|
@ -397,7 +397,7 @@ def get_user_repos(username, org_id=None):
|
||||
r.id = r.repo_id
|
||||
r.name = r.repo_name
|
||||
r.last_modify = r.last_modified
|
||||
|
||||
|
||||
return (owned_repos, shared_repos, groups_repos, public_repos)
|
||||
|
||||
def get_conf_text_ext():
|
||||
@ -812,6 +812,11 @@ if EVENTS_CONFIG_FILE:
|
||||
|
||||
def get_file_history_suffix():
|
||||
return seafevents_api.get_file_history_suffix(parsed_events_conf)
|
||||
|
||||
def get_trash_records(repo_id, show_time, start, limit):
|
||||
with _get_seafevents_session() as session:
|
||||
res, total_count = seafevents_api.get_delete_records(session, repo_id, show_time, start, limit)
|
||||
return res, total_count
|
||||
|
||||
else:
|
||||
parsed_events_conf = None
|
||||
@ -874,6 +879,8 @@ else:
|
||||
pass
|
||||
def get_user_activities_by_timestamp():
|
||||
pass
|
||||
def get_trash_records():
|
||||
pass
|
||||
|
||||
|
||||
def calc_file_path_hash(path, bits=12):
|
||||
@ -881,7 +888,6 @@ def calc_file_path_hash(path, bits=12):
|
||||
path = path.encode('UTF-8')
|
||||
|
||||
path_hash = hashlib.md5(urllib.parse.quote(path)).hexdigest()[:bits]
|
||||
|
||||
return path_hash
|
||||
|
||||
def get_service_url():
|
||||
|
@ -317,7 +317,7 @@ def repo_folder_trash(request, repo_id):
|
||||
'repo': repo,
|
||||
'repo_folder_name': name,
|
||||
'path': path,
|
||||
'enable_clean': config.ENABLE_USER_CLEAN_TRASH,
|
||||
'enable_user_clean_trash': config.ENABLE_USER_CLEAN_TRASH,
|
||||
'is_repo_admin': repo_admin
|
||||
})
|
||||
|
||||
@ -1095,6 +1095,7 @@ def react_fake_view(request, **kwargs):
|
||||
'upload_link_expire_days_max': UPLOAD_LINK_EXPIRE_DAYS_MAX,
|
||||
'enable_encrypted_library': config.ENABLE_ENCRYPTED_LIBRARY,
|
||||
'enable_repo_history_setting': config.ENABLE_REPO_HISTORY_SETTING,
|
||||
'enable_user_clean_trash': config.ENABLE_USER_CLEAN_TRASH,
|
||||
'enable_reset_encrypted_repo_password': ENABLE_RESET_ENCRYPTED_REPO_PASSWORD,
|
||||
'is_email_configured': IS_EMAIL_CONFIGURED,
|
||||
'can_add_public_repo': request.user.permissions.can_add_public_repo(),
|
||||
|
@ -1479,3 +1479,18 @@ CREATE TABLE `base_clientssotoken` (
|
||||
KEY `base_clientssotoken_updated_at_591fc2cd` (`updated_at`),
|
||||
KEY `base_clientssotoken_accessed_at_cdc66bf3` (`accessed_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `FileTrash` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`user` varchar(255) NOT NULL,
|
||||
`obj_type` varchar(10) NOT NULL,
|
||||
`obj_id` varchar(40) NOT NULL,
|
||||
`obj_name` varchar(255) NOT NULL,
|
||||
`delete_time` datetime NOT NULL,
|
||||
`repo_id` varchar(36) NOT NULL,
|
||||
`commit_id` varchar(40) DEFAULT NULL,
|
||||
`path` text NOT NULL,
|
||||
`size` bigint(20) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `ix_FileTrash_repo_id` (`repo_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8_general_ci;
|
||||
|
Loading…
Reference in New Issue
Block a user