mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-20 10:58:33 +00:00
Merge branch '10.0'
This commit is contained in:
@@ -36,7 +36,6 @@ class SelectEditor extends React.Component {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
document.addEventListener('click', this.onHideSelect);
|
||||
this.setOptions();
|
||||
}
|
||||
|
||||
@@ -78,10 +77,6 @@ class SelectEditor extends React.Component {
|
||||
this.setOptions();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('click', this.onHideSelect);
|
||||
}
|
||||
|
||||
onEditPermission = (e) => {
|
||||
e.preventDefault();
|
||||
e.nativeEvent.stopImmediatePropagation();
|
||||
@@ -102,7 +97,7 @@ class SelectEditor extends React.Component {
|
||||
e.nativeEvent.stopImmediatePropagation();
|
||||
}
|
||||
|
||||
onHideSelect = () => {
|
||||
onMenuClose = () => {
|
||||
this.setState({isEditing: false});
|
||||
this.props.toggleItemFreezed && this.props.toggleItemFreezed(false);
|
||||
}
|
||||
@@ -149,6 +144,7 @@ class SelectEditor extends React.Component {
|
||||
menuPosition={'fixed'}
|
||||
menuPortalTarget={document.querySelector('#wrapper')}
|
||||
styles={MenuSelectStyle}
|
||||
onMenuClose={this.onMenuClose}
|
||||
menuShouldScrollIntoView
|
||||
/>
|
||||
}
|
||||
|
@@ -43,22 +43,32 @@ class LinkItem extends React.Component {
|
||||
return link.slice(0, 9) + '...' + link.slice(length-5);
|
||||
}
|
||||
|
||||
onDeleteIconClicked = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.toggleDeleteShareLinkDialog();
|
||||
}
|
||||
|
||||
toggleDeleteShareLinkDialog = () => {
|
||||
this.setState({isDeleteShareLinkDialogOpen: !this.state.isDeleteShareLinkDialogOpen});
|
||||
}
|
||||
|
||||
copyLink = (e) => {
|
||||
onCopyIconClicked = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const { item } = this.props;
|
||||
copy(item.link);
|
||||
toaster.success(gettext('Share link is copied to the clipboard.'));
|
||||
}
|
||||
|
||||
viewDetails = (e) => {
|
||||
e.preventDefault();
|
||||
clickItem = (e) => {
|
||||
this.props.showLinkDetails(this.props.item);
|
||||
}
|
||||
|
||||
onCheckboxClicked = (e) => {
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
toggleSelectLink = (e) => {
|
||||
const { item } = this.props;
|
||||
this.props.toggleSelectLink(item, e.target.checked);
|
||||
@@ -76,12 +86,23 @@ class LinkItem extends React.Component {
|
||||
const currentPermission = Utils.getShareLinkPermissionStr(permissions);
|
||||
return (
|
||||
<Fragment>
|
||||
<tr onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut} className={isSelected ? 'tr-highlight' : ''}>
|
||||
<tr
|
||||
onClick={this.clickItem}
|
||||
onMouseOver={this.onMouseOver}
|
||||
onMouseOut={this.onMouseOut}
|
||||
className={`cursor-pointer ${isSelected ? 'tr-highlight' : ''}`}
|
||||
>
|
||||
<td className="text-center">
|
||||
<input type="checkbox" checked={isSelected} onChange={this.toggleSelectLink} className="vam" />
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isSelected}
|
||||
className="vam"
|
||||
onClick={this.onCheckboxClicked}
|
||||
onChange={this.toggleSelectLink}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<a href="#" onClick={this.viewDetails} className="text-inherit">{this.cutLink(link)}</a>
|
||||
{this.cutLink(link)}
|
||||
</td>
|
||||
<td>
|
||||
{(isPro && permissions) && (
|
||||
@@ -98,9 +119,8 @@ class LinkItem extends React.Component {
|
||||
{expire_date ? moment(expire_date).format('YYYY-MM-DD HH:mm') : '--'}
|
||||
</td>
|
||||
<td>
|
||||
<a href="#" role="button" onClick={this.copyLink} className={`sf2-icon-copy action-icon ${isItemOpVisible ? '' : 'invisible'}`} title={gettext('Copy')} aria-label={gettext('Copy')}></a>
|
||||
<a href="#" role="button" onClick={this.viewDetails} className={`fa fa-pencil-alt attr-action-icon ${isItemOpVisible ? '' : 'invisible'}`} title={gettext('Edit')} aria-label={gettext('Edit')}></a>
|
||||
<a href="#" role="button" onClick={this.toggleDeleteShareLinkDialog} className={`sf2-icon-delete action-icon ${isItemOpVisible ? '' : 'invisible'}`} title={gettext('Delete')} aria-label={gettext('Delete')}></a>
|
||||
<a href="#" role="button" onClick={this.onCopyIconClicked} className={`sf2-icon-copy action-icon ${isItemOpVisible ? '' : 'invisible'}`} title={gettext('Copy')} aria-label={gettext('Copy')}></a>
|
||||
<a href="#" role="button" onClick={this.onDeleteIconClicked} className={`sf2-icon-delete action-icon ${isItemOpVisible ? '' : 'invisible'}`} title={gettext('Delete')} aria-label={gettext('Delete')}></a>
|
||||
</td>
|
||||
</tr>
|
||||
{this.state.isDeleteShareLinkDialogOpen && (
|
||||
|
@@ -80,10 +80,10 @@ class LinkList extends React.Component {
|
||||
<th width="5%" className="text-center">
|
||||
<input type="checkbox" checked={isAllLinksSelected} className="vam" onChange={this.toggleSelectAllLinks} />
|
||||
</th>
|
||||
<th width="23%">{gettext('Link')}</th>
|
||||
<th width="26%">{gettext('Link')}</th>
|
||||
<th width="30%">{gettext('Permission')}</th>
|
||||
<th width="24%">{gettext('Expiration')}</th>
|
||||
<th width="18%"></th>
|
||||
<th width="25%">{gettext('Expiration')}</th>
|
||||
<th width="14%"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@@ -1,40 +1,202 @@
|
||||
from datetime import datetime
|
||||
|
||||
from django.db import connection
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from seaserv import seafile_api
|
||||
from seahub.tags.models import FileUUIDMap
|
||||
from seahub.base.models import UserStarredFiles
|
||||
from seahub.revision_tag.models import RevisionTags
|
||||
from seahub.share.models import ExtraGroupsSharePermission, \
|
||||
ExtraSharePermission, UploadLinkShare
|
||||
from seahub.utils.seafile_db import get_seafile_db_name
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Clear invalid data when repo deleted"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('--dry-run', type=str, choices=['true', 'false'], default='false',
|
||||
help='Whether to only print the log instead of actually deleting the data')
|
||||
|
||||
def get_seafile_repo_count(self, table_name):
|
||||
db_name, error_msg = get_seafile_db_name()
|
||||
if error_msg:
|
||||
self.stderr.write('[%s] Failed to get seafile db name, error: %s' % (datetime.now(), error_msg))
|
||||
return
|
||||
|
||||
sql = """SELECT COUNT(1) FROM `%s`.`%s`""" % (db_name, table_name)
|
||||
try:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(sql)
|
||||
repo_count = int(cursor.fetchone()[0])
|
||||
except Exception as e:
|
||||
self.stderr.write('[%s] Failed to count the number of %s, error: %s.' % (datetime.now(), table_name, e))
|
||||
return
|
||||
|
||||
return repo_count
|
||||
|
||||
def query_seafile_repo_ids(self, count, table_name):
|
||||
db_name, error_msg = get_seafile_db_name()
|
||||
if error_msg:
|
||||
self.stderr.write('[%s] Failed to get seafile db name, error: %s' % (datetime.now(), error_msg))
|
||||
return
|
||||
|
||||
repo_ids = set()
|
||||
sql = """SELECT repo_id FROM `%s`.`%s` LIMIT %%s, %%s""" % (db_name, table_name)
|
||||
for i in range(0, count, 1000):
|
||||
try:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(sql, (i, 1000))
|
||||
res = cursor.fetchall()
|
||||
except Exception as e:
|
||||
self.stderr.write('[%s] Failed to query all repo_id from %s, error: %s.' %
|
||||
(datetime.now(), table_name, e))
|
||||
return
|
||||
for repo_id, *_ in res:
|
||||
repo_ids.add(repo_id)
|
||||
|
||||
return repo_ids
|
||||
|
||||
def get_repo_id_count(self, table_name):
|
||||
self.stdout.write('[%s] Count the number of repo_id of %s.' % (datetime.now(), table_name))
|
||||
sql = """SELECT COUNT(DISTINCT(repo_id)) FROM %s""" % table_name
|
||||
try:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(sql)
|
||||
repo_id_count = int(cursor.fetchone()[0])
|
||||
except Exception as e:
|
||||
self.stderr.write('[%s] Failed to count the number repo_id of %s, error: %s.' %
|
||||
(datetime.now(), table_name, e))
|
||||
return
|
||||
|
||||
self.stdout.write('[%s] The number of repo_id of %s: %s.' % (datetime.now(), table_name, repo_id_count))
|
||||
return repo_id_count
|
||||
|
||||
def query_invalid_repo_ids(self, all_repo_ids, repo_id_count, table_name):
|
||||
self.stdout.write('[%s] Start to query all repo_id of %s.' % (datetime.now(), table_name))
|
||||
sql = """SELECT DISTINCT(repo_id) FROM %s LIMIT %%s, %%s""" % table_name
|
||||
repo_ids = list()
|
||||
invalid_repo_ids = list()
|
||||
for i in range(0, repo_id_count, 1000):
|
||||
try:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(sql, (i, 1000))
|
||||
res = cursor.fetchall()
|
||||
except Exception as e:
|
||||
self.stderr.write('[%s] Failed to query repo_id from %s, error: %s.' %
|
||||
(datetime.now(), table_name, e))
|
||||
return
|
||||
|
||||
for repo_id, *_ in res:
|
||||
repo_ids.append(repo_ids)
|
||||
if repo_id not in all_repo_ids:
|
||||
invalid_repo_ids.append(repo_id)
|
||||
|
||||
self.stdout.write('[%s] Successfully queried all repo_id of %s, result length: %s' %
|
||||
(datetime.now(), table_name, len(repo_ids)))
|
||||
|
||||
self.stdout.write('[%s] The number of invalid repo_id of %s: %s' %
|
||||
(datetime.now(), table_name, len(invalid_repo_ids)))
|
||||
|
||||
return invalid_repo_ids
|
||||
|
||||
def clean_up_invalid_records(self, dry_run, invalid_repo_ids, table_name):
|
||||
self.stdout.write('[%s] Start to count invalid records of %s.' % (datetime.now(), table_name))
|
||||
invalid_records_count = 0
|
||||
if invalid_repo_ids:
|
||||
count_sql = """SELECT COUNT(1) FROM %s WHERE repo_id IN %%s""" % table_name
|
||||
try:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(count_sql, (invalid_repo_ids,))
|
||||
invalid_records_count = int(cursor.fetchone()[0])
|
||||
except Exception as e:
|
||||
self.stderr.write('[%s] Failed to count invalid records of %s, error: %s.' %
|
||||
(datetime.now(), table_name, e))
|
||||
return False
|
||||
|
||||
self.stdout.write('[%s] The number of invalid records of %s: %s' %
|
||||
(datetime.now(), table_name, invalid_records_count))
|
||||
|
||||
self.stdout.write('[%s] Start to clean up invalid records of %s...' % (datetime.now(), table_name))
|
||||
if dry_run == 'false':
|
||||
clean_sql = """DELETE FROM %s WHERE repo_id IN %%s LIMIT 10000""" % table_name
|
||||
for i in range(0, invalid_records_count, 10000):
|
||||
try:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(clean_sql, (invalid_repo_ids,))
|
||||
except Exception as e:
|
||||
self.stderr.write('[%s] Failed to clean up expired UploadLinkShare, error: %s.' %
|
||||
(datetime.now(), e))
|
||||
return False
|
||||
|
||||
self.stdout.write('[%s] Successfully cleaned up invalid records of %s.' % (datetime.now(), table_name))
|
||||
return True
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
self.stdout.write('Start to get all existing repo')
|
||||
self.all_repo = [repo.repo_id for repo in seafile_api.get_repo_list(-1, -1, ret_virt_repo=True)]
|
||||
trash_repo = [repo.repo_id for repo in seafile_api.get_trash_repo_list(-1, -1)]
|
||||
self.all_repo.extend(trash_repo)
|
||||
self.stdout.write('Successly get all existing repos')
|
||||
dry_run = kwargs['dry_run']
|
||||
# get all exist repo_id
|
||||
self.stdout.write('[%s] Count the number of exist repo.' % datetime.now())
|
||||
exist_repo_count = self.get_seafile_repo_count('Repo')
|
||||
if exist_repo_count is None:
|
||||
return
|
||||
self.stdout.write('[%s] The number of exist repo: %s.' % (datetime.now(), exist_repo_count))
|
||||
|
||||
# on_delete is CASCADE, so FileTag/FileComment will be deleted
|
||||
self.tables = {'FileUUIDMap': FileUUIDMap, 'RevisionTags': RevisionTags,
|
||||
'UserStarredFiles': UserStarredFiles,
|
||||
'ExtraGroupsSharePermission': ExtraGroupsSharePermission,
|
||||
'ExtraSharePermission': ExtraSharePermission,
|
||||
'UploadLinkShare': UploadLinkShare}
|
||||
self.stdout.write('[%s] Start to query all exist repo_id.' % datetime.now())
|
||||
exist_repo_ids = self.query_seafile_repo_ids(exist_repo_count, 'Repo')
|
||||
if exist_repo_ids is None:
|
||||
return
|
||||
self.stdout.write('[%s] Successfully queried all exist repo_id, result length: %s.' %
|
||||
(datetime.now(), len(exist_repo_ids)))
|
||||
|
||||
for table in list(self.tables.items()):
|
||||
self.clear_table(table[0], table[1])
|
||||
# get all virtual repo_id
|
||||
self.stdout.write('[%s] Count the number of virtual repo.' % datetime.now())
|
||||
virtual_repo_count = self.get_seafile_repo_count('VirtualRepo')
|
||||
if virtual_repo_count is None:
|
||||
return
|
||||
self.stdout.write('[%s] The number of virtual repo: %s.' % (datetime.now(), virtual_repo_count))
|
||||
|
||||
self.stdout.write('All invalid repo data are deleted')
|
||||
self.stdout.write('[%s] Start to query all virtual repo_id.' % datetime.now())
|
||||
virtual_repo_ids = self.query_seafile_repo_ids(virtual_repo_count, 'VirtualRepo')
|
||||
if virtual_repo_ids is None:
|
||||
return
|
||||
self.stdout.write('[%s] Successfully queried all virtual repo_id, result length: %s.' %
|
||||
(datetime.now(), len(virtual_repo_ids)))
|
||||
|
||||
def clear_table(self, table_name, table_model):
|
||||
""" clear invalid data on table
|
||||
table must has `repo_id` column and without foreign relationship
|
||||
"""
|
||||
self.stdout.write('Start to clear %s table' % table_name)
|
||||
tb_repo_ids = table_model.objects.all().values_list('repo_id', flat=True)
|
||||
table_model.objects.filter(repo_id__in=list(set(tb_repo_ids) - set(self.all_repo))).delete()
|
||||
self.stdout.write('%s table has been clear' % table_name)
|
||||
# get all trash repo_id
|
||||
self.stdout.write('[%s] Count the number of trash repo.' % datetime.now())
|
||||
trash_repo_count = self.get_seafile_repo_count('RepoTrash')
|
||||
if trash_repo_count is None:
|
||||
return
|
||||
self.stdout.write('[%s] The number of trash repo: %s.' % (datetime.now(), trash_repo_count))
|
||||
|
||||
self.stdout.write('[%s] Start to query all trash repo_id.' % datetime.now())
|
||||
trash_repo_ids = self.query_seafile_repo_ids(trash_repo_count, 'RepoTrash')
|
||||
if trash_repo_ids is None:
|
||||
return
|
||||
self.stdout.write('[%s] Successfully queried all trash repo_id, result length: %s.' %
|
||||
(datetime.now(), len(trash_repo_ids)))
|
||||
|
||||
all_repo_ids = exist_repo_ids | virtual_repo_ids | trash_repo_ids
|
||||
self.stdout.write('[%s] The number of valid repo: %s.' % (datetime.now(), len(all_repo_ids)))
|
||||
|
||||
# clean up expired upload_link
|
||||
self.stdout.write('[%s] Start to clean up expired upload_link...' % datetime.now())
|
||||
if dry_run == 'false':
|
||||
sql1 = """DELETE FROM share_uploadlinkshare WHERE expire_date < DATE_SUB(CURDATE(), INTERVAL 3 DAY)"""
|
||||
try:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(sql1)
|
||||
except Exception as e:
|
||||
self.stderr.write('[%s] Failed to clean up expired upload_link, error: %s.' % (datetime.now(), e))
|
||||
return
|
||||
self.stdout.write('[%s] Successfully cleaned up expired upload_link.' % datetime.now())
|
||||
|
||||
# clean up invalid upload_link
|
||||
repo_id_count = self.get_repo_id_count('share_uploadlinkshare')
|
||||
if repo_id_count is None:
|
||||
return
|
||||
|
||||
invalid_repo_ids = self.query_invalid_repo_ids(all_repo_ids, repo_id_count, 'share_uploadlinkshare')
|
||||
if invalid_repo_ids is None:
|
||||
return
|
||||
|
||||
clean_up_res = self.clean_up_invalid_records(dry_run, invalid_repo_ids, 'share_uploadlinkshare')
|
||||
if clean_up_res is None:
|
||||
return
|
||||
|
||||
# TODO: tags_fileuuidmap, revision_tag_revisiontags, base_userstarredfiles, share_extragroupssharepermission, share_extrasharepermission
|
||||
|
27
seahub/utils/seafile_db.py
Normal file
27
seahub/utils/seafile_db.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
import configparser
|
||||
|
||||
|
||||
def get_seafile_db_name():
|
||||
seafile_conf_dir = os.environ.get('SEAFILE_CENTRAL_CONF_DIR') or os.environ.get('SEAFILE_CONF_DIR')
|
||||
if not seafile_conf_dir:
|
||||
error_msg = 'Environment variable seafile_conf_dir is not define.'
|
||||
return None, error_msg
|
||||
|
||||
seafile_conf_path = os.path.join(seafile_conf_dir, 'seafile.conf')
|
||||
config = configparser.ConfigParser()
|
||||
config.read(seafile_conf_path)
|
||||
|
||||
if not config.has_section('database'):
|
||||
error_msg = 'Do not found database configuration items.'
|
||||
return None, error_msg
|
||||
|
||||
db_type = config.get('database', 'type')
|
||||
if db_type != 'mysql':
|
||||
error_msg = 'Unknown database backend: %s' % db_type
|
||||
return None, error_msg
|
||||
|
||||
db_name = config.get('database', 'db_name', fallback='seafile')
|
||||
|
||||
return db_name, None
|
Reference in New Issue
Block a user