mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-20 02:48:51 +00:00
Merge branch '10.0'
This commit is contained in:
@@ -36,7 +36,6 @@ class SelectEditor extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
document.addEventListener('click', this.onHideSelect);
|
|
||||||
this.setOptions();
|
this.setOptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,10 +77,6 @@ class SelectEditor extends React.Component {
|
|||||||
this.setOptions();
|
this.setOptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
document.removeEventListener('click', this.onHideSelect);
|
|
||||||
}
|
|
||||||
|
|
||||||
onEditPermission = (e) => {
|
onEditPermission = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.nativeEvent.stopImmediatePropagation();
|
e.nativeEvent.stopImmediatePropagation();
|
||||||
@@ -102,7 +97,7 @@ class SelectEditor extends React.Component {
|
|||||||
e.nativeEvent.stopImmediatePropagation();
|
e.nativeEvent.stopImmediatePropagation();
|
||||||
}
|
}
|
||||||
|
|
||||||
onHideSelect = () => {
|
onMenuClose = () => {
|
||||||
this.setState({isEditing: false});
|
this.setState({isEditing: false});
|
||||||
this.props.toggleItemFreezed && this.props.toggleItemFreezed(false);
|
this.props.toggleItemFreezed && this.props.toggleItemFreezed(false);
|
||||||
}
|
}
|
||||||
@@ -149,6 +144,7 @@ class SelectEditor extends React.Component {
|
|||||||
menuPosition={'fixed'}
|
menuPosition={'fixed'}
|
||||||
menuPortalTarget={document.querySelector('#wrapper')}
|
menuPortalTarget={document.querySelector('#wrapper')}
|
||||||
styles={MenuSelectStyle}
|
styles={MenuSelectStyle}
|
||||||
|
onMenuClose={this.onMenuClose}
|
||||||
menuShouldScrollIntoView
|
menuShouldScrollIntoView
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
@@ -43,22 +43,32 @@ class LinkItem extends React.Component {
|
|||||||
return link.slice(0, 9) + '...' + link.slice(length-5);
|
return link.slice(0, 9) + '...' + link.slice(length-5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onDeleteIconClicked = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this.toggleDeleteShareLinkDialog();
|
||||||
|
}
|
||||||
|
|
||||||
toggleDeleteShareLinkDialog = () => {
|
toggleDeleteShareLinkDialog = () => {
|
||||||
this.setState({isDeleteShareLinkDialogOpen: !this.state.isDeleteShareLinkDialogOpen});
|
this.setState({isDeleteShareLinkDialogOpen: !this.state.isDeleteShareLinkDialogOpen});
|
||||||
}
|
}
|
||||||
|
|
||||||
copyLink = (e) => {
|
onCopyIconClicked = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
const { item } = this.props;
|
const { item } = this.props;
|
||||||
copy(item.link);
|
copy(item.link);
|
||||||
toaster.success(gettext('Share link is copied to the clipboard.'));
|
toaster.success(gettext('Share link is copied to the clipboard.'));
|
||||||
}
|
}
|
||||||
|
|
||||||
viewDetails = (e) => {
|
clickItem = (e) => {
|
||||||
e.preventDefault();
|
|
||||||
this.props.showLinkDetails(this.props.item);
|
this.props.showLinkDetails(this.props.item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onCheckboxClicked = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
toggleSelectLink = (e) => {
|
toggleSelectLink = (e) => {
|
||||||
const { item } = this.props;
|
const { item } = this.props;
|
||||||
this.props.toggleSelectLink(item, e.target.checked);
|
this.props.toggleSelectLink(item, e.target.checked);
|
||||||
@@ -76,12 +86,23 @@ class LinkItem extends React.Component {
|
|||||||
const currentPermission = Utils.getShareLinkPermissionStr(permissions);
|
const currentPermission = Utils.getShareLinkPermissionStr(permissions);
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<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">
|
<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>
|
||||||
<td>
|
<td>
|
||||||
<a href="#" onClick={this.viewDetails} className="text-inherit">{this.cutLink(link)}</a>
|
{this.cutLink(link)}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{(isPro && permissions) && (
|
{(isPro && permissions) && (
|
||||||
@@ -98,9 +119,8 @@ class LinkItem extends React.Component {
|
|||||||
{expire_date ? moment(expire_date).format('YYYY-MM-DD HH:mm') : '--'}
|
{expire_date ? moment(expire_date).format('YYYY-MM-DD HH:mm') : '--'}
|
||||||
</td>
|
</td>
|
||||||
<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.onCopyIconClicked} 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.onDeleteIconClicked} className={`sf2-icon-delete action-icon ${isItemOpVisible ? '' : 'invisible'}`} title={gettext('Delete')} aria-label={gettext('Delete')}></a>
|
||||||
<a href="#" role="button" onClick={this.toggleDeleteShareLinkDialog} className={`sf2-icon-delete action-icon ${isItemOpVisible ? '' : 'invisible'}`} title={gettext('Delete')} aria-label={gettext('Delete')}></a>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{this.state.isDeleteShareLinkDialogOpen && (
|
{this.state.isDeleteShareLinkDialogOpen && (
|
||||||
|
@@ -80,10 +80,10 @@ class LinkList extends React.Component {
|
|||||||
<th width="5%" className="text-center">
|
<th width="5%" className="text-center">
|
||||||
<input type="checkbox" checked={isAllLinksSelected} className="vam" onChange={this.toggleSelectAllLinks} />
|
<input type="checkbox" checked={isAllLinksSelected} className="vam" onChange={this.toggleSelectAllLinks} />
|
||||||
</th>
|
</th>
|
||||||
<th width="23%">{gettext('Link')}</th>
|
<th width="26%">{gettext('Link')}</th>
|
||||||
<th width="30%">{gettext('Permission')}</th>
|
<th width="30%">{gettext('Permission')}</th>
|
||||||
<th width="24%">{gettext('Expiration')}</th>
|
<th width="25%">{gettext('Expiration')}</th>
|
||||||
<th width="18%"></th>
|
<th width="14%"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
@@ -1,40 +1,202 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from django.db import connection
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
from seaserv import seafile_api
|
from seahub.utils.seafile_db import get_seafile_db_name
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
help = "Clear invalid data when repo deleted"
|
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):
|
def handle(self, *args, **kwargs):
|
||||||
self.stdout.write('Start to get all existing repo')
|
dry_run = kwargs['dry_run']
|
||||||
self.all_repo = [repo.repo_id for repo in seafile_api.get_repo_list(-1, -1, ret_virt_repo=True)]
|
# get all exist repo_id
|
||||||
trash_repo = [repo.repo_id for repo in seafile_api.get_trash_repo_list(-1, -1)]
|
self.stdout.write('[%s] Count the number of exist repo.' % datetime.now())
|
||||||
self.all_repo.extend(trash_repo)
|
exist_repo_count = self.get_seafile_repo_count('Repo')
|
||||||
self.stdout.write('Successly get all existing repos')
|
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.stdout.write('[%s] Start to query all exist repo_id.' % datetime.now())
|
||||||
self.tables = {'FileUUIDMap': FileUUIDMap, 'RevisionTags': RevisionTags,
|
exist_repo_ids = self.query_seafile_repo_ids(exist_repo_count, 'Repo')
|
||||||
'UserStarredFiles': UserStarredFiles,
|
if exist_repo_ids is None:
|
||||||
'ExtraGroupsSharePermission': ExtraGroupsSharePermission,
|
return
|
||||||
'ExtraSharePermission': ExtraSharePermission,
|
self.stdout.write('[%s] Successfully queried all exist repo_id, result length: %s.' %
|
||||||
'UploadLinkShare': UploadLinkShare}
|
(datetime.now(), len(exist_repo_ids)))
|
||||||
|
|
||||||
for table in list(self.tables.items()):
|
# get all virtual repo_id
|
||||||
self.clear_table(table[0], table[1])
|
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):
|
# get all trash repo_id
|
||||||
""" clear invalid data on table
|
self.stdout.write('[%s] Count the number of trash repo.' % datetime.now())
|
||||||
table must has `repo_id` column and without foreign relationship
|
trash_repo_count = self.get_seafile_repo_count('RepoTrash')
|
||||||
"""
|
if trash_repo_count is None:
|
||||||
self.stdout.write('Start to clear %s table' % table_name)
|
return
|
||||||
tb_repo_ids = table_model.objects.all().values_list('repo_id', flat=True)
|
self.stdout.write('[%s] The number of trash repo: %s.' % (datetime.now(), trash_repo_count))
|
||||||
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)
|
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