1
0
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:
awu0403 2024-07-18 13:44:41 +08:00 committed by GitHub
parent cf7272c274
commit 0981a0dc99
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 714 additions and 15 deletions

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

View File

@ -281,6 +281,7 @@ class DirColumnNav extends React.Component {
<DirOthers
repoID={this.props.repoID}
userPerm={this.props.userPerm}
currentRepoInfo={this.props.currentRepoInfo}
/>
</>
);

View File

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

View 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;
}

View File

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

View File

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

View 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 };

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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