From 47c189ebcf1d32eb9df47a62a9507be435a24e7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=81=A5=E8=BE=89?= Date: Wed, 6 Nov 2019 11:51:38 +0800 Subject: [PATCH] virus scan react (#4222) * virus scan react * repair code * repair code * update seafile-js --- frontend/package-lock.json | 18 +- frontend/package.json | 2 +- frontend/src/pages/sys-admin/index.js | 6 + frontend/src/pages/sys-admin/side-panel.js | 8 +- .../src/pages/sys-admin/virus-scan-records.js | 206 ++++++++++++++++++ .../endpoints/admin/virus_scan_records.py | 98 +++++++++ seahub/templates/sysadmin/base.html | 2 +- seahub/urls.py | 8 +- 8 files changed, 333 insertions(+), 15 deletions(-) create mode 100644 frontend/src/pages/sys-admin/virus-scan-records.js create mode 100644 seahub/api2/endpoints/admin/virus_scan_records.py diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d8ac71144f..baf24a0117 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -5470,7 +5470,7 @@ }, "git-up": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/git-up/-/git-up-1.2.1.tgz", + "resolved": "http://registry.npmjs.org/git-up/-/git-up-1.2.1.tgz", "integrity": "sha1-JkSAoAax2EJhrB/gmjpRacV+oZ0=", "requires": { "is-ssh": "^1.0.0", @@ -5479,7 +5479,7 @@ }, "git-url-parse": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-5.0.1.tgz", + "resolved": "http://registry.npmjs.org/git-url-parse/-/git-url-parse-5.0.1.tgz", "integrity": "sha1-/j15xnRq4FBIz6UIyB553du6OEM=", "requires": { "git-up": "^1.0.0" @@ -8217,7 +8217,7 @@ }, "node-status-codes": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-status-codes/-/node-status-codes-1.0.0.tgz", + "resolved": "http://registry.npmjs.org/node-status-codes/-/node-status-codes-1.0.0.tgz", "integrity": "sha1-WuVUHQJGRdMqWPzdyc7s6nrjrC8=" }, "noop6": { @@ -8524,7 +8524,7 @@ }, "package.json": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/package.json/-/package.json-2.0.1.tgz", + "resolved": "http://registry.npmjs.org/package.json/-/package.json-2.0.1.tgz", "integrity": "sha1-+IYFnSpJ7QduZIg2ldc7K0bSHW0=", "requires": { "git-package-json": "^1.4.0", @@ -8534,7 +8534,7 @@ "dependencies": { "got": { "version": "5.7.1", - "resolved": "https://registry.npmjs.org/got/-/got-5.7.1.tgz", + "resolved": "http://registry.npmjs.org/got/-/got-5.7.1.tgz", "integrity": "sha1-X4FjWmHkplifGAVp6k44FoClHzU=", "requires": { "create-error-class": "^3.0.1", @@ -8556,7 +8556,7 @@ }, "package-json": { "version": "2.4.0", - "resolved": "https://registry.npmjs.org/package-json/-/package-json-2.4.0.tgz", + "resolved": "http://registry.npmjs.org/package-json/-/package-json-2.4.0.tgz", "integrity": "sha1-DRW9Z9HLvduyyiIv8u24a8sxqLs=", "requires": { "got": "^5.0.0", @@ -11386,9 +11386,9 @@ } }, "seafile-js": { - "version": "0.2.136", - "resolved": "https://registry.npmjs.org/seafile-js/-/seafile-js-0.2.136.tgz", - "integrity": "sha512-0/MYJrLRHwU7tKawbtNVfVXG+tk1rfUiYC8fiWMfsyDDhXwXEmqGaKMJuYqDU2rpAyqgpyTZLUX43UXmZF9Kiw==", + "version": "0.2.138", + "resolved": "https://registry.npmjs.org/seafile-js/-/seafile-js-0.2.138.tgz", + "integrity": "sha512-glPINgDUw2a5q1qBX3hJPKlT2iNltvt3hQXkC6f2pQqLRdh3s8faNHP0ftD+7QnmEmXZpr1LX/6RjtdwP6U57g==", "requires": { "axios": "^0.18.0", "form-data": "^2.3.2", diff --git a/frontend/package.json b/frontend/package.json index 809381c042..ef18957bfc 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -41,7 +41,7 @@ "react-responsive": "^6.1.2", "react-select": "^2.4.1", "reactstrap": "^6.4.0", - "seafile-js": "^0.2.136", + "seafile-js": "^0.2.138", "socket.io-client": "^2.2.0", "sw-precache-webpack-plugin": "0.11.4", "unified": "^7.0.0", diff --git a/frontend/src/pages/sys-admin/index.js b/frontend/src/pages/sys-admin/index.js index 549b2ca90f..9169baea7c 100644 --- a/frontend/src/pages/sys-admin/index.js +++ b/frontend/src/pages/sys-admin/index.js @@ -60,6 +60,7 @@ import AdminLoginLogs from './admin-logs/login-logs'; import WebSettings from './web-settings/web-settings'; import Notifications from './notifications/notifications'; import FileScanRecords from './file-scan-records'; +import VirusScanRecords from './virus-scan-records'; import WorkWeixinDepartments from './work-weixin-departments'; import Invitations from './invitations/invitations'; @@ -198,6 +199,11 @@ class SysAdmin extends React.Component { currentTab={currentTab} tabItemClick={this.tabItemClick} /> + - + this.props.tabItemClick('virus-scan-records')} + > {gettext('Virus Scan')} - + } {enableGuestInvitation && isDefaultAdmin && diff --git a/frontend/src/pages/sys-admin/virus-scan-records.js b/frontend/src/pages/sys-admin/virus-scan-records.js new file mode 100644 index 0000000000..7f96991f1b --- /dev/null +++ b/frontend/src/pages/sys-admin/virus-scan-records.js @@ -0,0 +1,206 @@ +import React, { Component, Fragment } from 'react'; +import PropTypes from 'prop-types'; +import { seafileAPI } from '../../utils/seafile-api'; +import { gettext } from '../../utils/constants'; +import toaster from '../../components/toast' +import Account from '../../components/common/account'; + + +const recordItemPropTypes = { + record: PropTypes.object.isRequired, +}; + +class RecordItem extends Component { + + constructor(props) { + super(props); + this.state = { + handleStatus: this.props.record.has_handle, + errorMsg: '', + }; + } + + deleteVirusScanRecord = () => { + seafileAPI.deleteVirusScanRecord(this.props.record.virus_id).then(() => { + this.setState({ + handleStatus: !this.state.handleStatus, + }); + }).catch((error) => { + if (error.response) { + this.setState({ + errorMsg: error.response.data.error_msg, + }); + toaster.danger(this.state.errorMsg); + } + }); + } + + render() { + let record = this.props.record; + + return ( + + {record.repo_name} + {record.repo_owner} + {record.file_path} + + { + this.state.handleStatus ? + {gettext('Handled')} : + {gettext('Delete')} + } + + + ); + } +} + +RecordItem.propTypes = recordItemPropTypes; + + +const recordListPropTypes = { + loading: PropTypes.bool.isRequired, + isLoadingMore: PropTypes.bool.isRequired, + errorMsg: PropTypes.string.isRequired, + records: PropTypes.array.isRequired, +}; + +class RecordList extends Component { + + render() { + let { loading, isLoadingMore, errorMsg, records } = this.props; + + if (loading) { + return ; + } else if (errorMsg) { + return

{errorMsg}

; + } else { + return ( + + + + + + + + + + + + {records.map((record, index) => { + return ( + + ); + })} + +
{gettext('Library')}{gettext('Owner')}{gettext('Virus File')}{gettext('Operations')}
+ {isLoadingMore ? : ''} +
+ ); + } + } +} + +RecordList.propTypes = recordListPropTypes; + + +class VirusScanRecords extends Component { + + constructor(props) { + super(props); + this.state = { + loading: true, + isLoadingMore: false, + currentPage: 1, + hasMore: true, + errorMsg: '', + records: [], + }; + } + + getMore() { + let currentPage = this.state.currentPage; + seafileAPI.listVirusScanRecords(currentPage).then((res) => { + this.setState({ + isLoadingMore: false, + records: [...this.state.records, ...res.data.record_list], + currentPage: currentPage + 1, + hasMore: res.data.record_list.length === 0 ? false : true, + }); + }).catch((error) => { + if (error.response) { + this.setState({ + isLoadingMore: false, + errorMsg: error.response.data.error_msg, + }); + toaster.danger(this.state.errorMsg); + } + }); + } + + handleScroll = (event) => { + if (!this.state.isLoadingMore && this.state.hasMore) { + const clientHeight = event.target.clientHeight; + const scrollHeight = event.target.scrollHeight; + const scrollTop = event.target.scrollTop; + const isBottom = (clientHeight + scrollTop + 1 >= scrollHeight); + if (isBottom) { + this.setState({isLoadingMore: true}, () => { + this.getMore(); + }); + } + } + } + + componentDidMount() { + let currentPage = this.state.currentPage; + seafileAPI.listVirusScanRecords(currentPage).then((res) => { + this.setState({ + loading: false, + records: res.data.record_list, + currentPage: currentPage + 1, + hasMore: true, + }); + }).catch((error) => { + if (error.response) { + this.setState({ + loading: false, + errorMsg: error.response.data.error_msg, + }); + toaster.danger(this.state.errorMsg); + } + }); + } + + render() { + return ( + +
+
+ +
+
+ +
+
+
+
+
+

{gettext('Virus Scan Records')}

+
+
+ +
+
+
+
+ ); + } +} + +export default VirusScanRecords; diff --git a/seahub/api2/endpoints/admin/virus_scan_records.py b/seahub/api2/endpoints/admin/virus_scan_records.py new file mode 100644 index 0000000000..8ade1a276c --- /dev/null +++ b/seahub/api2/endpoints/admin/virus_scan_records.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- +import os +import logging + +from rest_framework.authentication import SessionAuthentication +from rest_framework.permissions import IsAdminUser +from rest_framework.response import Response +from rest_framework.views import APIView +from rest_framework import status + +from seaserv import seafile_api + +from seahub.api2.authentication import TokenAuthentication +from seahub.api2.throttling import UserRateThrottle +from seahub.api2.permissions import IsProVersion +from seahub.api2.utils import api_error +from seahub.utils import get_virus_record, get_virus_record_by_id, handle_virus_record + +logger = logging.getLogger(__name__) + + +class AdminVirusScanRecords(APIView): + + authentication_classes = (TokenAuthentication, SessionAuthentication) + permission_classes = (IsAdminUser, IsProVersion) + throttle_classes = (UserRateThrottle,) + + def get(self, request): + """get virus scan records + """ + try: + page = int(request.GET.get('page', '')) + except ValueError: + page = 1 + + try: + per_page = int(request.GET.get('per_page', '')) + except ValueError: + per_page = 25 + + start = (page - 1) * per_page + count = per_page + + try: + virus_records = get_virus_record(start=start, limit=count) + except Exception as e: + logger.error(e) + error_msg = 'Internal Server Error' + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + + record_list = list() + for virus_record in virus_records: + try: + repo = seafile_api.get_repo(virus_record.repo_id) + repo_owner = seafile_api.get_repo_owner(virus_record.repo_id) + except Exception as e: + logger.error(e) + continue + + if not repo: + continue + else: + record = dict() + record["repo_name"] = repo.name + record["repo_owner"] = repo_owner + record["file_path"] = virus_record.file_path + record["has_handle"] = virus_record.has_handle + record["virus_id"] = virus_record.vid + record_list.append(record) + + return Response({"record_list": record_list}, status=status.HTTP_200_OK) + + +class AdminVirusScanRecord(APIView): + + authentication_classes = (TokenAuthentication, SessionAuthentication) + permission_classes = (IsAdminUser, IsProVersion) + throttle_classes = (UserRateThrottle,) + + def delete(self, request, virus_id): + virus_record = get_virus_record_by_id(virus_id) + if not virus_record: + error_msg = 'Virus file %d not found.' % virus_record.file_path + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + parent_dir = os.path.dirname(virus_record.file_path) + filename = os.path.basename(virus_record.file_path) + try: + seafile_api.del_file( + virus_record.repo_id, parent_dir, filename, request.user.username + ) + handle_virus_record(virus_id) + except Exception as e: + logger.error(e) + error_msg = 'Failed to delete, please try again later.' + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + + return Response({"success": True}, status=status.HTTP_200_OK) diff --git a/seahub/templates/sysadmin/base.html b/seahub/templates/sysadmin/base.html index 5369952447..aa877a92df 100644 --- a/seahub/templates/sysadmin/base.html +++ b/seahub/templates/sysadmin/base.html @@ -107,7 +107,7 @@ {% if is_pro and is_default_admin %}
  • - {% trans "Virus Scan" %} + {% trans "Virus Scan" %}
  • {% endif %} diff --git a/seahub/urls.py b/seahub/urls.py index ffda018b4c..0b96a99770 100644 --- a/seahub/urls.py +++ b/seahub/urls.py @@ -157,6 +157,7 @@ from seahub.api2.endpoints.admin.logs import AdminLogsLoginLogs, AdminLogsFileAc AdminLogsSharePermissionLogs from seahub.api2.endpoints.admin.work_weixin import AdminWorkWeixinDepartments, \ AdminWorkWeixinDepartmentMembers, AdminWorkWeixinUsersBatch, AdminWorkWeixinDepartmentsImport +from seahub.api2.endpoints.admin.virus_scan_records import AdminVirusScanRecords, AdminVirusScanRecord from seahub.api2.endpoints.file_participants import FileParticipantsView, FileParticipantView from seahub.api2.endpoints.repo_related_users import RepoRelatedUsersView @@ -610,6 +611,10 @@ urlpatterns = [ ## admin::file-scan-records url(r'^api/v2.1/admin/file-scan-records/$', AdminFileScanRecords.as_view(), name='api-v2.1-admin-file-scan-records'), + # admin::virus-scan-records + url(r'^api/v2.1/admin/virus-scan-records/$', AdminVirusScanRecords.as_view(), name='api-v2.1-admin-virus-scan-records'), + url(r'^api/v2.1/admin/virus-scan-records/(?P\d+)/$', AdminVirusScanRecord.as_view(), name='api-v2.1-admin-virus-scan-record'), + ## admin::notifications url(r'^api/v2.1/admin/notifications/$', AdminNotificationsView.as_view(), name='api-2.1-admin-notifications'), url(r'^api/v2.1/admin/sys-notifications/$', AdminSysNotificationsView.as_view(), name='api-2.1-admin-sys-notifications'), @@ -751,8 +756,7 @@ if ENABLE_FILE_SCAN: from seahub.utils import EVENTS_ENABLED if EVENTS_ENABLED: urlpatterns += [ - url(r'^sys/virus_scan_records/$', sys_virus_scan_records, name='sys_virus_scan_records'), - url(r'^sys/virus_scan_records/delete/(?P\d+)/$', sys_delete_virus_scan_records, name='sys_delete_virus_scan_records'), + url(r'^sys/virus-scan-records/$', sysadmin_react_fake_view, name='sys_virus_scan_records'), ] if settings.SERVE_STATIC: