@@ -65,6 +69,12 @@ class OrgLogs extends Component {
to={siteRoot + 'org/logadmin/perm-audit/'} title={gettext('Permission')}>{gettext('Permission')}
+
this.tabItemClick('repo-transfer')}>
+ {gettext('Repo Transfer')}
+
+
diff --git a/frontend/src/pages/sys-admin/index.js b/frontend/src/pages/sys-admin/index.js
index eda20d8b29..91cd6d2b84 100644
--- a/frontend/src/pages/sys-admin/index.js
+++ b/frontend/src/pages/sys-admin/index.js
@@ -67,6 +67,7 @@ import LoginLogs from './logs-page/login-logs';
import FileAccessLogs from './logs-page/file-access-logs';
import FileUpdateLogs from './logs-page/file-update-logs';
import SharePermissionLogs from './logs-page/share-permission-logs';
+import FIleTransferLogs from './logs-page/file-transfer-log';
import WebSettings from './web-settings/web-settings';
import Notifications from './notifications/notifications';
@@ -251,6 +252,7 @@ class SysAdmin extends React.Component {
+
diff --git a/frontend/src/pages/sys-admin/logs-page/file-transfer-log.js b/frontend/src/pages/sys-admin/logs-page/file-transfer-log.js
new file mode 100644
index 0000000000..2ecc60394c
--- /dev/null
+++ b/frontend/src/pages/sys-admin/logs-page/file-transfer-log.js
@@ -0,0 +1,230 @@
+import React, { Component, Fragment } from 'react';
+import PropTypes from 'prop-types';
+import { Link } from '@gatsbyjs/reach-router';
+import dayjs from 'dayjs';
+import relativeTime from 'dayjs/plugin/relativeTime';
+import { seafileAPI } from '../../../utils/seafile-api';
+import { gettext, siteRoot } from '../../../utils/constants';
+import { Utils } from '../../../utils/utils';
+import EmptyTip from '../../../components/empty-tip';
+import Loading from '../../../components/loading';
+import Paginator from '../../../components/paginator';
+import MainPanelTopbar from '../main-panel-topbar';
+import UserLink from '../user-link';
+import LogsNav from './logs-nav';
+
+dayjs.extend(relativeTime);
+
+class Content extends Component {
+
+ getPreviousPage = () => {
+ this.props.getLogsByPage(this.props.currentPage - 1);
+ };
+
+ getNextPage = () => {
+ this.props.getLogsByPage(this.props.currentPage + 1);
+ };
+
+ render() {
+ const { loading, errorMsg, items, perPage, currentPage, hasNextPage } = this.props;
+ if (loading) {
+ return
;
+ } else if (errorMsg) {
+ return
{errorMsg}
;
+ } else {
+ const emptyTip = (
+
+
+ );
+ const table = (
+
+
+
+
+ {gettext('Transfer From')} |
+ {gettext('Transfer To')} |
+ {gettext('Operator')} |
+ {gettext('Library')} |
+ {gettext('Date')} |
+
+
+ {items &&
+
+ {items.map((item, index) => {
+ return ( );
+ })}
+
+ }
+
+
+
+ );
+ return items.length ? table : emptyTip;
+ }
+ }
+}
+
+Content.propTypes = {
+ loading: PropTypes.bool.isRequired,
+ errorMsg: PropTypes.string.isRequired,
+ items: PropTypes.array.isRequired,
+ getLogsByPage: PropTypes.func,
+ resetPerPage: PropTypes.func,
+ currentPage: PropTypes.number,
+ perPage: PropTypes.number,
+ pageInfo: PropTypes.object,
+ hasNextPage: PropTypes.bool,
+};
+
+class Item extends Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ isOpIconShown: false,
+ };
+ }
+
+ handleMouseOver = () => {
+ this.setState({
+ isOpIconShown: true
+ });
+ };
+
+ handleMouseOut = () => {
+ this.setState({
+ isOpIconShown: false
+ });
+ };
+
+ getTransferTo = (item) => {
+ switch (item.to_type) {
+ case 'user':
+ return
;
+ case 'group':
+ return
{item.to_group_name};
+ default:
+ return gettext('Deleted');
+ }
+ };
+
+ getTransferFrom = (item) => {
+ switch (item.from_type) {
+ case 'user':
+ return
;
+ case 'group':
+ return
{item.from_group_name};
+ default:
+ return gettext('Deleted');
+ }
+ };
+
+ getOperator = (item) => {
+ return
;
+ };
+
+ render() {
+ let { item } = this.props;
+ return (
+
+ {this.getTransferFrom(item)} |
+ {this.getTransferTo(item)} |
+ {this.getOperator(item)} |
+ {item.repo_name ? item.repo_name : gettext('Deleted')} |
+ {dayjs(item.date).fromNow()} |
+
+ );
+ }
+}
+
+Item.propTypes = {
+ item: PropTypes.object.isRequired,
+};
+
+class FIleTransferLogs extends Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ loading: true,
+ errorMsg: '',
+ logList: [],
+ perPage: 100,
+ currentPage: 1,
+ hasNextPage: false,
+ };
+ this.initPage = 1;
+ }
+
+ componentDidMount() {
+ let urlParams = (new URL(window.location)).searchParams;
+ const { currentPage, perPage } = this.state;
+ this.setState({
+ perPage: parseInt(urlParams.get('per_page') || perPage),
+ currentPage: parseInt(urlParams.get('page') || currentPage)
+ }, () => {
+ this.getLogsByPage(this.state.currentPage);
+ });
+ }
+
+ getLogsByPage = (page) => {
+ let { perPage } = this.state;
+ seafileAPI.sysAdminListFileTransferLogs(page, perPage).then((res) => {
+ this.setState({
+ logList: res.data.repo_transfer_log_list,
+ loading: false,
+ currentPage: page,
+ hasNextPage: res.data.has_next_page,
+ });
+ }).catch((error) => {
+ this.setState({
+ loading: false,
+ errorMsg: Utils.getErrorMsg(error, true) // true: show login tip if 403
+ });
+ });
+ };
+
+ resetPerPage = (newPerPage) => {
+ this.setState({
+ perPage: newPerPage,
+ }, () => this.getLogsByPage(this.initPage));
+ };
+
+ render() {
+ let { logList, currentPage, perPage, hasNextPage } = this.state;
+ return (
+
+
+
+
+ );
+ }
+}
+
+export default FIleTransferLogs;
diff --git a/frontend/src/pages/sys-admin/logs-page/logs-nav.js b/frontend/src/pages/sys-admin/logs-page/logs-nav.js
index c85a25c411..370228d67c 100644
--- a/frontend/src/pages/sys-admin/logs-page/logs-nav.js
+++ b/frontend/src/pages/sys-admin/logs-page/logs-nav.js
@@ -16,6 +16,7 @@ class Nav extends React.Component {
{ name: 'fileAccessLogs', urlPart: 'logs/file-access', text: gettext('File Access') },
{ name: 'fileUpdateLogs', urlPart: 'logs/file-update', text: gettext('File Update') },
{ name: 'sharePermissionLogs', urlPart: 'logs/share-permission', text: gettext('Permission') },
+ { name: 'fileTransfer', urlPart: 'logs/repo-transfer', text: gettext('Repo Transfer') },
];
}
diff --git a/frontend/src/tests/utils/utils.test.js b/frontend/src/tests/utils/utils.test.js
index 8602d61b8a..da5996a450 100644
--- a/frontend/src/tests/utils/utils.test.js
+++ b/frontend/src/tests/utils/utils.test.js
@@ -1,4 +1,4 @@
-import { Utils } from "../../utils/utils";
+import { Utils } from '../../utils/utils';
describe('getFileExtension', () => {
it('should return the file extension with dot', () => {
diff --git a/frontend/src/utils/org-admin-api.js b/frontend/src/utils/org-admin-api.js
index 170b934917..bafe855c08 100644
--- a/frontend/src/utils/org-admin-api.js
+++ b/frontend/src/utils/org-admin-api.js
@@ -481,6 +481,15 @@ class OrgAdminAPI {
}
// org admin logs
+ orgAdminListFileTransfer(page, perPage) {
+ let url = this.server + '/api/v2.1/org/admin/logs/repo-transfer/';
+ let params = {
+ page: page,
+ per_page: perPage
+ };
+ return this.req.get(url, { params: params });
+ }
+
orgAdminListFileAudit(email, repoID, page) {
let url = this.server + '/api/v2.1/org/admin/logs/file-access/?page=' + page;
if (email) {
diff --git a/frontend/src/utils/seafile-api.js b/frontend/src/utils/seafile-api.js
index 050fb73229..1b165bb672 100644
--- a/frontend/src/utils/seafile-api.js
+++ b/frontend/src/utils/seafile-api.js
@@ -2169,6 +2169,15 @@ class SeafileAPI {
return this._sendPostRequest(url, formData);
}
+ sysAdminListFileTransferLogs(page, perPage) {
+ const url = this.server + '/api/v2.1/admin/logs/repo-transfer-logs/';
+ let params = {
+ page: page,
+ per_page: perPage
+ };
+ return this.req.get(url, { params: params });
+ }
+
}
let seafileAPI = new SeafileAPI();
diff --git a/seahub/api2/endpoints/admin/libraries.py b/seahub/api2/endpoints/admin/libraries.py
index a1df543756..4a988d9071 100644
--- a/seahub/api2/endpoints/admin/libraries.py
+++ b/seahub/api2/endpoints/admin/libraries.py
@@ -28,6 +28,7 @@ from seahub.utils.timeutils import timestamp_to_isoformat_timestr
from seahub.api2.endpoints.group_owned_libraries import get_group_id_by_repo_owner
from seahub.constants import PERMISSION_READ_WRITE
+from seahub.base.models import RepoTransfer
try:
from seahub.settings import MULTI_TENANCY
@@ -423,6 +424,11 @@ class AdminLibrary(APIView):
# transfer repo
try:
transfer_repo(repo_id, new_owner, is_share)
+ RepoTransfer.objects.create(from_user=repo_owner,
+ to=new_owner,
+ repo_id=repo_id,
+ org_id=-1,
+ operator=request.user.username)
except Exception as e:
logger.error(e)
error_msg = 'Internal Server Error'
diff --git a/seahub/api2/endpoints/admin/logs.py b/seahub/api2/endpoints/admin/logs.py
index 8b20d4e417..c6976ec47c 100644
--- a/seahub/api2/endpoints/admin/logs.py
+++ b/seahub/api2/endpoints/admin/logs.py
@@ -20,6 +20,7 @@ from seahub.utils import get_file_audit_events, generate_file_audit_event_type,
get_file_update_events, get_perm_audit_events, is_valid_email
from seahub.utils.timeutils import datetime_to_isoformat_timestr, utc_datetime_to_isoformat_timestr
from seahub.utils.repo import is_valid_repo_id_format
+from seahub.base.models import RepoTransfer
logger = logging.getLogger(__name__)
@@ -420,3 +421,146 @@ class AdminLogsSharePermissionLogs(APIView):
}
return Response(resp)
+
+
+class AdminLogsFileTransferLogs(APIView):
+ authentication_classes = (TokenAuthentication, SessionAuthentication)
+ permission_classes = (IsAdminUser, IsProVersion)
+ throttle_classes = (UserRateThrottle,)
+
+ def get(self, request):
+ """ Get all transfer repo logs.
+
+ Permission checking:
+ 1. only admin can perform this action.
+ """
+
+ if not request.user.admin_permissions.can_view_user_log():
+ return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.')
+
+ 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 = per_page * (current_page - 1)
+ limit = per_page
+
+ if start < 0:
+ error_msg = 'start invalid'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ if limit < 0:
+ error_msg = 'limit invalid'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ events = RepoTransfer.objects.all().order_by('-timestamp')[start:start+limit+1]
+ if len(events) > limit:
+ has_next_page = True
+ events = events[:limit]
+ else:
+ has_next_page = False
+
+ # Use dict to reduce memcache fetch cost in large for-loop.
+ nickname_dict = {}
+ contact_email_dict = {}
+ repo_dict = {}
+ group_name_dict = {}
+
+ user_email_set = set()
+ repo_id_set = set()
+ group_id_set = set()
+
+ for event in events:
+ repo_id_set.add(event.repo_id)
+ if is_valid_email(event.from_user):
+ user_email_set.add(event.from_user)
+ if is_valid_email(event.to):
+ user_email_set.add(event.to)
+ if is_valid_email(event.operator):
+ user_email_set.add(event.operator)
+ if '@seafile_group' in event.to:
+ group_id = int(event.to.split('@')[0])
+ group_id_set.add(group_id)
+ if '@seafile_group' in event.from_user:
+ group_id = int(event.from_user.split('@')[0])
+ group_id_set.add(group_id)
+
+ for e in user_email_set:
+ if e not in nickname_dict:
+ nickname_dict[e] = email2nickname(e)
+ if e not in contact_email_dict:
+ contact_email_dict[e] = email2contact_email(e)
+ for e in repo_id_set:
+ if e not in repo_dict:
+ repo_dict[e] = seafile_api.get_repo(e)
+
+ for group_id in group_id_set:
+ if group_id not in group_name_dict:
+ group = ccnet_api.get_group(int(group_id))
+ if group:
+ group_name_dict[group_id] = group.group_name
+
+ events_info = []
+ for ev in events:
+ data = {
+ 'from_user_email': '',
+ 'from_user_name': '',
+ 'from_user_contact_email': '',
+ 'from_group_id': '',
+ 'from_group_name': '',
+ 'to_user_email': '',
+ 'to_user_name': '',
+ 'to_user_contact_email': '',
+ 'to_group_id': '',
+ 'to_group_name': '',
+ 'operator_email': '',
+ 'operator_name': '',
+ 'operator_contact_email': '',
+ }
+ from_user_email = ev.from_user
+ data['from_user_email'] = from_user_email
+ data['from_user_name'] = nickname_dict.get(from_user_email, '')
+ data['from_user_contact_email'] = contact_email_dict.get(from_user_email, '')
+
+ operator_email = ev.operator
+ data['operator_email'] = operator_email
+ data['operator_name'] = nickname_dict.get(operator_email, '')
+ data['operator_contact_email'] = contact_email_dict.get(operator_email, '')
+
+ if is_valid_email(from_user_email):
+ data['from_type'] = 'user'
+ if '@seafile_group' in from_user_email:
+ from_group_id = int(from_user_email.split('@')[0])
+ data['from_group_id'] = from_group_id
+ data['from_group_name'] = group_name_dict.get(from_group_id, '')
+ data['from_type'] = 'group'
+
+ repo_id = ev.repo_id
+ data['repo_id'] = repo_id
+ repo = repo_dict.get(repo_id, None)
+ data['repo_name'] = repo.name if repo else ''
+ data['date'] = datetime_to_isoformat_timestr(ev.timestamp)
+
+ if is_valid_email(ev.to):
+ to_user_email = ev.to
+ data['to_user_email'] = to_user_email
+ data['to_user_name'] = nickname_dict.get(to_user_email, '')
+ data['to_user_contact_email'] = contact_email_dict.get(to_user_email, '')
+ data['to_type'] = 'user'
+ if '@seafile_group' in ev.to:
+ to_group_id = int(ev.to.split('@')[0])
+ data['to_group_id'] = to_group_id
+ data['to_group_name'] = group_name_dict.get(to_group_id, '')
+ data['to_type'] = 'group'
+
+ events_info.append(data)
+
+ resp = {
+ 'repo_transfer_log_list': events_info,
+ 'has_next_page': has_next_page,
+ }
+
+ return Response(resp)
diff --git a/seahub/api2/endpoints/group_owned_libraries.py b/seahub/api2/endpoints/group_owned_libraries.py
index d1d138e370..268d830bb3 100644
--- a/seahub/api2/endpoints/group_owned_libraries.py
+++ b/seahub/api2/endpoints/group_owned_libraries.py
@@ -50,6 +50,7 @@ from seahub.views import check_folder_permission
from seahub.settings import ENABLE_STORAGE_CLASSES, STORAGE_CLASS_MAPPING_POLICY, \
ENCRYPTED_LIBRARY_VERSION, ENCRYPTED_LIBRARY_PWD_HASH_ALGO, \
ENCRYPTED_LIBRARY_PWD_HASH_PARAMS
+from seahub.base.models import RepoTransfer
logger = logging.getLogger(__name__)
@@ -1497,6 +1498,12 @@ class GroupOwnedLibraryTransferView(APIView):
# transfer repo
try:
transfer_repo(repo_id, new_owner, is_share, org_id)
+ org_id = seafile_api.get_org_id_by_repo_id(repo_id)
+ RepoTransfer.objects.create(from_user=repo_owner,
+ to=new_owner,
+ repo_id=repo_id,
+ org_id=org_id,
+ operator=request.user.username)
except Exception as e:
logger.error(e)
error_msg = 'Internal Server Error'
diff --git a/seahub/api2/endpoints/internal_api.py b/seahub/api2/endpoints/internal_api.py
index 2e49777c40..7e87db0b13 100644
--- a/seahub/api2/endpoints/internal_api.py
+++ b/seahub/api2/endpoints/internal_api.py
@@ -14,6 +14,7 @@ from seahub.share.models import UploadLinkShare, FileShare, check_share_link_acc
from seaserv import seafile_api
from seahub.utils.repo import parse_repo_perm
from seahub.views.file import send_file_access_msg
+from seahub.utils import normalize_file_path
logger = logging.getLogger(__name__)
@@ -146,6 +147,7 @@ class InternalCheckFileOperationAccess(APIView):
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
file_path = request.data.get('path', '/')
+ file_path = normalize_file_path(file_path)
repo = seafile_api.get_repo(repo_id)
if not repo:
return api_error(status.HTTP_404_NOT_FOUND, 'Library %s not found.' % repo_id)
diff --git a/seahub/api2/views.py b/seahub/api2/views.py
index eae8d215c6..c1c380197c 100644
--- a/seahub/api2/views.py
+++ b/seahub/api2/views.py
@@ -46,7 +46,7 @@ from seahub.avatar.templatetags.avatar_tags import api_avatar_url, avatar
from seahub.avatar.templatetags.group_avatar_tags import api_grp_avatar_url, \
grp_avatar
from seahub.base.accounts import User
-from seahub.base.models import UserStarredFiles, DeviceToken, RepoSecretKey, FileComment
+from seahub.base.models import UserStarredFiles, DeviceToken, RepoSecretKey, FileComment, RepoTransfer
from seahub.share.models import ExtraSharePermission, ExtraGroupsSharePermission
from seahub.share.utils import is_repo_admin, check_group_share_in_permission, normalize_custom_permission_name
from seahub.base.templatetags.seahub_tags import email2nickname, \
@@ -1871,6 +1871,12 @@ class RepoOwner(APIView):
# transfer repo
try:
transfer_repo(repo_id, new_owner, is_share, org_id)
+ org_id = seafile_api.get_org_id_by_repo_id(repo_id)
+ RepoTransfer.objects.create(from_user=repo_owner,
+ to=new_owner,
+ repo_id=repo_id,
+ operator=username,
+ org_id=org_id)
except SearpcError as e:
logger.error(e)
error_msg = 'Internal Server Error'
diff --git a/seahub/base/models.py b/seahub/base/models.py
index c8acc9890f..052c5e8819 100644
--- a/seahub/base/models.py
+++ b/seahub/base/models.py
@@ -451,3 +451,15 @@ class ClientSSOToken(models.Model):
if not self.token:
self.token = self.gen_token()
return super(ClientSSOToken, self).save(*args, **kwargs)
+
+
+class RepoTransfer(models.Model):
+ repo_id = models.CharField(max_length=36)
+ org_id = models.IntegerField(db_index=True)
+ from_user = models.CharField(max_length=255)
+ to = models.CharField(max_length=255)
+ operator = models.CharField(max_length=255)
+ timestamp = models.DateTimeField(default=timezone.now, db_index=True)
+
+ class Meta:
+ db_table = 'RepoTransfer'
diff --git a/seahub/organizations/api/admin/logs.py b/seahub/organizations/api/admin/logs.py
index 7251b6d31c..0ec0a23c62 100644
--- a/seahub/organizations/api/admin/logs.py
+++ b/seahub/organizations/api/admin/logs.py
@@ -17,7 +17,8 @@ from seahub.api2.endpoints.utils import get_user_name_dict, \
get_user_contact_email_dict, get_repo_dict, get_group_dict
from seahub.base.templatetags.seahub_tags import email2nickname, email2contact_email
-from seahub.utils import EVENTS_ENABLED, get_file_audit_events, get_file_update_events, get_perm_audit_events
+from seahub.base.models import RepoTransfer
+from seahub.utils import EVENTS_ENABLED, get_file_audit_events, get_file_update_events, get_perm_audit_events, is_valid_email
from seahub.utils.timeutils import timestamp_to_isoformat_timestr, datetime_to_isoformat_timestr
from seahub.organizations.api.permissions import IsOrgAdmin
@@ -263,3 +264,140 @@ class OrgAdminLogsPermAudit(APIView):
'page': page,
'page_next': page_next,
})
+
+
+class OrgAdminLogsFileTransfer(APIView):
+
+ authentication_classes = (TokenAuthentication, SessionAuthentication)
+ throttle_classes = (UserRateThrottle,)
+ permission_classes = (IsProVersion, IsOrgAdmin)
+
+ def get(self, request):
+ """List organization file transfer in logs
+ """
+ try:
+ page = int(request.GET.get('page', '1'))
+ per_page = int(request.GET.get('per_page', '25'))
+ except ValueError:
+ page = 1
+ per_page = 25
+
+ start = per_page * (page - 1)
+ limit = per_page
+
+ org_id = request.user.org.org_id
+ events = RepoTransfer.objects.filter(org_id=org_id).all().order_by('-timestamp')[start:start+limit+1]
+ if len(events) > limit:
+ page_next = True
+ events = events[:limit]
+ else:
+ page_next = False
+
+ event_list = []
+ if not events:
+ return Response({
+ 'log_list': event_list,
+ 'page': page,
+ 'page_next': False
+ })
+
+ # Use dict to reduce memcache fetch cost in large for-loop.
+ nickname_dict = {}
+ contact_email_dict = {}
+ repo_dict = {}
+ group_name_dict = {}
+
+ user_email_set = set()
+ repo_id_set = set()
+ group_id_set = set()
+
+ for event in events:
+ repo_id_set.add(event.repo_id)
+ if is_valid_email(event.from_user):
+ user_email_set.add(event.from_user)
+ if is_valid_email(event.to):
+ user_email_set.add(event.to)
+ if is_valid_email(event.operator):
+ user_email_set.add(event.operator)
+ if '@seafile_group' in event.to:
+ group_id = int(event.to.split('@')[0])
+ group_id_set.add(group_id)
+ if '@seafile_group' in event.from_user:
+ group_id = int(event.from_user.split('@')[0])
+ group_id_set.add(group_id)
+
+ for e in user_email_set:
+ if e not in nickname_dict:
+ nickname_dict[e] = email2nickname(e)
+ if e not in contact_email_dict:
+ contact_email_dict[e] = email2contact_email(e)
+ for e in repo_id_set:
+ if e not in repo_dict:
+ repo_dict[e] = seafile_api.get_repo(e)
+
+ for group_id in group_id_set:
+ if group_id not in group_name_dict:
+ group = ccnet_api.get_group(int(group_id))
+ if group:
+ group_name_dict[group_id] = group.group_name
+
+ event_list = []
+ for ev in events:
+ data = {
+ 'from_user_email': '',
+ 'from_user_name': '',
+ 'from_user_contact_email': '',
+ 'from_group_id': '',
+ 'from_group_name': '',
+ 'to_user_email': '',
+ 'to_user_name': '',
+ 'to_user_contact_email': '',
+ 'to_group_id': '',
+ 'to_group_name': '',
+ 'operator_email': '',
+ 'operator_name': '',
+ 'operator_contact_email': '',
+ }
+ from_user_email = ev.from_user
+ data['from_user_email'] = from_user_email
+ data['from_user_name'] = nickname_dict.get(from_user_email, '')
+ data['from_user_contact_email'] = contact_email_dict.get(from_user_email, '')
+
+ operator_email = ev.operator
+ data['operator_email'] = operator_email
+ data['operator_name'] = nickname_dict.get(operator_email, '')
+ data['operator_contact_email'] = contact_email_dict.get(operator_email, '')
+
+ if is_valid_email(from_user_email):
+ data['from_type'] = 'user'
+ if '@seafile_group' in from_user_email:
+ from_group_id = int(from_user_email.split('@')[0])
+ data['from_group_id'] = from_group_id
+ data['from_group_name'] = group_name_dict.get(from_group_id, '')
+ data['from_type'] = 'group'
+
+ repo_id = ev.repo_id
+ data['repo_id'] = repo_id
+ repo = repo_dict.get(repo_id, None)
+ data['repo_name'] = repo.name if repo else ''
+ data['date'] = datetime_to_isoformat_timestr(ev.timestamp)
+
+ if is_valid_email(ev.to):
+ to_user_email = ev.to
+ data['to_user_email'] = to_user_email
+ data['to_user_name'] = nickname_dict.get(to_user_email, '')
+ data['to_user_contact_email'] = contact_email_dict.get(to_user_email, '')
+ data['to_type'] = 'user'
+ if '@seafile_group' in ev.to:
+ to_group_id = int(ev.to.split('@')[0])
+ data['to_group_id'] = to_group_id
+ data['to_group_name'] = group_name_dict.get(to_group_id, '')
+ data['to_type'] = 'group'
+
+ event_list.append(data)
+
+ return Response({
+ 'log_list': event_list,
+ 'page': page,
+ 'page_next': page_next,
+ })
diff --git a/seahub/organizations/api/admin/repos.py b/seahub/organizations/api/admin/repos.py
index 4873665952..7e0274fa67 100644
--- a/seahub/organizations/api/admin/repos.py
+++ b/seahub/organizations/api/admin/repos.py
@@ -15,6 +15,7 @@ from seahub.api2.throttling import UserRateThrottle
from seahub.api2.authentication import TokenAuthentication
from seahub.api2.utils import api_error
from seahub.base.templatetags.seahub_tags import email2nickname, email2contact_email
+from seahub.base.models import RepoTransfer
from seahub.group.utils import group_id_to_name
from seahub.utils.timeutils import timestamp_to_isoformat_timestr
from seahub.utils import is_valid_email, transfer_repo
@@ -166,6 +167,11 @@ class OrgAdminRepo(APIView):
# transfer repo
try:
transfer_repo(repo_id, new_owner, is_share, org_id)
+ RepoTransfer.objects.create(from_user=repo_owner,
+ to=new_owner,
+ repo_id=repo_id,
+ org_id=org_id,
+ operator=request.user.username)
except Exception as e:
logger.error(e)
error_msg = 'Internal Server Error'
@@ -182,9 +188,15 @@ class OrgAdminRepo(APIView):
break
repo_info = {}
+
repo_info['owner_email'] = new_owner
- repo_info['owner_name'] = email2nickname(new_owner)
- repo_info['encrypted'] = repo.encrypted
+ if '@seafile_group' in new_owner:
+ group_id = get_group_id_by_repo_owner(new_owner)
+ repo_info['group_name'] = group_id_to_name(group_id)
+ repo_info['owner_name'] = group_id_to_name(group_id)
+ else:
+ repo_info['owner_name'] = email2nickname(new_owner)
+ repo_info['encrypted'] = repo.encrypted
repo_info['repo_id'] = repo.repo_id
repo_info['repo_name'] = repo.name
repo_info['is_department_repo'] = False
diff --git a/seahub/organizations/api_urls.py b/seahub/organizations/api_urls.py
index 55eaff63bf..a636af921a 100644
--- a/seahub/organizations/api_urls.py
+++ b/seahub/organizations/api_urls.py
@@ -20,7 +20,7 @@ from .api.admin.trash_libraries import OrgAdminTrashLibraries, OrgAdminTrashLibr
from .api.admin.info import OrgAdminInfo
from .api.admin.links import OrgAdminLinks, OrgAdminLink
from .api.admin.web_settings import OrgAdminWebSettings
-from .api.admin.logs import OrgAdminLogsFileAccess, OrgAdminLogsFileUpdate, OrgAdminLogsPermAudit
+from .api.admin.logs import OrgAdminLogsFileAccess, OrgAdminLogsFileUpdate, OrgAdminLogsPermAudit, OrgAdminLogsFileTransfer
from .api.admin.user_repos import OrgAdminUserRepos, OrgAdminUserBesharedRepos
from .api.admin.devices import OrgAdminDevices, OrgAdminDevicesErrors
@@ -102,6 +102,7 @@ urlpatterns = [
path('admin/logs/file-access/', OrgAdminLogsFileAccess.as_view(), name='api-v2.1-org-admin-logs-file-access'),
path('admin/logs/file-update/', OrgAdminLogsFileUpdate.as_view(), name='api-v2.1-org-admin-logs-file-update'),
path('admin/logs/repo-permission/', OrgAdminLogsPermAudit.as_view(), name='api-v2.1-org-admin-logs-repo-permission'),
+ path('admin/logs/repo-transfer/', OrgAdminLogsFileTransfer.as_view(), name='api-v2.1-org-admin-logs-repo-transfer'),
path('
/admin/departments/', OrgAdminDepartments.as_view(), name='api-v2.1-org-admin-departments'),
path('/admin/logs/export-excel/', OrgLogsExport.as_view(), name='api-v2.1-org-logs-export-excel'),
path('admin/log/export-excel/', org_log_export_excel, name='org_log_export_excel'),
diff --git a/seahub/organizations/urls.py b/seahub/organizations/urls.py
index 4d38ae03fd..a6b71b0cda 100644
--- a/seahub/organizations/urls.py
+++ b/seahub/organizations/urls.py
@@ -35,6 +35,7 @@ urlpatterns = [
path('logadmin/', react_fake_view, name='org_log_file_audit'),
path('logadmin/file-update/', react_fake_view, name='org_log_file_update'),
path('logadmin/perm-audit/', react_fake_view, name='org_log_perm_audit'),
+ path('logadmin/repo-transfer/', react_fake_view, name='org_log_file_transfer'),
path('info/', react_fake_view, name='org_info'),
path('settings/', react_fake_view, name='org_settings'),
diff --git a/seahub/urls.py b/seahub/urls.py
index 111ec8e2a6..cb44117b19 100644
--- a/seahub/urls.py
+++ b/seahub/urls.py
@@ -193,7 +193,7 @@ from seahub.api2.endpoints.admin.file_scan_records import AdminFileScanRecords
from seahub.api2.endpoints.admin.notifications import AdminNotificationsView
from seahub.api2.endpoints.admin.sys_notifications import AdminSysNotificationsView, AdminSysNotificationView
from seahub.api2.endpoints.admin.logs import AdminLogsLoginLogs, AdminLogsFileAccessLogs, AdminLogsFileUpdateLogs, \
- AdminLogsSharePermissionLogs
+ AdminLogsSharePermissionLogs, AdminLogsFileTransferLogs
from seahub.api2.endpoints.admin.terms_and_conditions import AdminTermsAndConditions, AdminTermAndCondition
from seahub.api2.endpoints.admin.work_weixin import AdminWorkWeixinDepartments, \
AdminWorkWeixinDepartmentMembers, AdminWorkWeixinUsersBatch, AdminWorkWeixinDepartmentsImport
@@ -691,6 +691,7 @@ urlpatterns = [
re_path(r'^api/v2.1/admin/logs/file-access-logs/$', AdminLogsFileAccessLogs.as_view(), name='api-v2.1-admin-logs-file-access-logs'),
re_path(r'^api/v2.1/admin/logs/file-update-logs/$', AdminLogsFileUpdateLogs.as_view(), name='api-v2.1-admin-logs-file-update-logs'),
re_path(r'^api/v2.1/admin/logs/share-permission-logs/$', AdminLogsSharePermissionLogs.as_view(), name='api-v2.1-admin-logs-share-permission-logs'),
+ re_path(r'^api/v2.1/admin/logs/repo-transfer-logs/$', AdminLogsFileTransferLogs.as_view(), name='api-v2.1-admin-logs-repo-transfer-logs'),
## admin::admin logs
re_path(r'^api/v2.1/admin/admin-logs/$', AdminOperationLogs.as_view(), name='api-v2.1-admin-admin-operation-logs'),
@@ -865,6 +866,7 @@ urlpatterns = [
path('sys/logs/file-access/', sysadmin_react_fake_view, name="sys_logs_file_access"),
path('sys/logs/file-update/', sysadmin_react_fake_view, name="sys_logs_file_update"),
path('sys/logs/share-permission/', sysadmin_react_fake_view, name="sys_logs_share_permission"),
+ path('sys/logs/repo-transfer/', sysadmin_react_fake_view, name="sys_logs_file_transfer"),
path('sys/admin-logs/operation/', sysadmin_react_fake_view, name="sys_admin_logs_operation"),
path('sys/admin-logs/login/', sysadmin_react_fake_view, name="sys_admin_logs_login"),
path('sys/organizations/', sysadmin_react_fake_view, name="sys_organizations"),
diff --git a/sql/mysql.sql b/sql/mysql.sql
index d9c81a89e1..90c67ab9f5 100644
--- a/sql/mysql.sql
+++ b/sql/mysql.sql
@@ -1575,3 +1575,16 @@ CREATE TABLE `wiki_wiki2_publish` (
UNIQUE KEY `publish_url` (`publish_url`),
KEY `ix_wiki2_publish_repo_id` (`repo_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+
+CREATE TABLE `RepoTransfer` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `repo_id` varchar(36) NOT NULL,
+ `org_id` int(11) NOT NULL,
+ `from_user` varchar(255) NOT NULL,
+ `to` varchar(255) NOT NULL,
+ `timestamp` datetime NOT NULL,
+ `operator` varchar(255) NOT NULL,
+ PRIMARY KEY (`id`),
+ KEY `idx_file_transfer_org_id` (`org_id`),
+ KEY `idx_file_transfer_timestamp` (`timestamp`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;