mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-15 06:44:16 +00:00
Virus scan frontend (#4574)
* ignore virus scan record * get virus record by has_handle * add has_next_page * change has_handle to has_deleted * change url and view name * delete or ignore virus files in batch * rebase master * add cancel-ignore * optimize code * add translate * [virus scan] added 'All' and 'Unhandled' tabs * rewrote 'all' page (previous 'virus-scan-records') * added 'unhandled' page * [virus scan] 'unhandled' tab: added 'delete & ignore selected items' * [virus scan] 'unhandled' tab: added 'handle selected items' Co-authored-by: 王健辉 <40563566+jianhw@users.noreply.github.com>
This commit is contained in:
@@ -7,6 +7,12 @@ import MainPanel from './main-panel';
|
|||||||
|
|
||||||
import Info from './info';
|
import Info from './info';
|
||||||
|
|
||||||
|
import StatisticFile from './statistic/statistic-file';
|
||||||
|
import StatisticStorage from './statistic/statistic-storage';
|
||||||
|
import StatisticTraffic from './statistic/statistic-traffic';
|
||||||
|
import StatisticUsers from './statistic/statistic-users';
|
||||||
|
import StatisticReport from './statistic/statistic-reports';
|
||||||
|
|
||||||
import DesktopDevices from './devices/desktop-devices';
|
import DesktopDevices from './devices/desktop-devices';
|
||||||
import MobileDevices from './devices/mobile-devices';
|
import MobileDevices from './devices/mobile-devices';
|
||||||
import DeviceErrors from './devices/devices-errors';
|
import DeviceErrors from './devices/devices-errors';
|
||||||
@@ -58,24 +64,19 @@ import FileAccessLogs from './logs-page/file-access-logs';
|
|||||||
import FileUpdateLogs from './logs-page/file-update-logs';
|
import FileUpdateLogs from './logs-page/file-update-logs';
|
||||||
import SharePermissionLogs from './logs-page/share-permission-logs';
|
import SharePermissionLogs from './logs-page/share-permission-logs';
|
||||||
|
|
||||||
import AdminOperationLogs from './admin-logs/operation-logs';
|
|
||||||
import AdminLoginLogs from './admin-logs/login-logs';
|
|
||||||
|
|
||||||
import TermsAndConditions from './terms-and-conditions/terms-and-conditions';
|
|
||||||
|
|
||||||
import WebSettings from './web-settings/web-settings';
|
import WebSettings from './web-settings/web-settings';
|
||||||
import Notifications from './notifications/notifications';
|
import Notifications from './notifications/notifications';
|
||||||
import FileScanRecords from './file-scan-records';
|
import FileScanRecords from './file-scan-records';
|
||||||
import VirusScanRecords from './virus-scan-records';
|
|
||||||
import WorkWeixinDepartments from './work-weixin-departments';
|
import WorkWeixinDepartments from './work-weixin-departments';
|
||||||
import DingtalkDepartments from './dingtalk-departments';
|
import DingtalkDepartments from './dingtalk-departments';
|
||||||
import Invitations from './invitations/invitations';
|
import Invitations from './invitations/invitations';
|
||||||
|
import TermsAndConditions from './terms-and-conditions/terms-and-conditions';
|
||||||
|
|
||||||
import StatisticFile from './statistic/statistic-file';
|
import AllVirusFiles from './virus-scan/all-virus-files';
|
||||||
import StatisticStorage from './statistic/statistic-storage';
|
import UnhandledVirusFiles from './virus-scan/unhandled-virus-files';
|
||||||
import StatisticTraffic from './statistic/statistic-traffic';
|
|
||||||
import StatisticUsers from './statistic/statistic-users';
|
import AdminOperationLogs from './admin-logs/operation-logs';
|
||||||
import StatisticReport from './statistic/statistic-reports';
|
import AdminLoginLogs from './admin-logs/login-logs';
|
||||||
|
|
||||||
import '../../assets/css/fa-solid.css';
|
import '../../assets/css/fa-solid.css';
|
||||||
import '../../assets/css/fa-regular.css';
|
import '../../assets/css/fa-regular.css';
|
||||||
@@ -141,6 +142,10 @@ class SysAdmin extends React.Component {
|
|||||||
tab: 'logs',
|
tab: 'logs',
|
||||||
urlPartList: ['logs/']
|
urlPartList: ['logs/']
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
tab: 'virus-files',
|
||||||
|
urlPartList: ['virus-files/']
|
||||||
|
},
|
||||||
{
|
{
|
||||||
tab: 'adminLogs',
|
tab: 'adminLogs',
|
||||||
urlPartList: ['admin-logs/']
|
urlPartList: ['admin-logs/']
|
||||||
@@ -244,16 +249,15 @@ class SysAdmin extends React.Component {
|
|||||||
<Invitations path={siteRoot + 'sys/invitations'} />
|
<Invitations path={siteRoot + 'sys/invitations'} />
|
||||||
<TermsAndConditions path={siteRoot + 'sys/terms-and-conditions/'} />
|
<TermsAndConditions path={siteRoot + 'sys/terms-and-conditions/'} />
|
||||||
|
|
||||||
|
<AllVirusFiles path={siteRoot + 'sys/virus-files/all'} />
|
||||||
|
<UnhandledVirusFiles path={siteRoot + 'sys/virus-files/unhandled'} />
|
||||||
|
|
||||||
<FileScanRecords
|
<FileScanRecords
|
||||||
path={siteRoot + 'sys/file-scan-records'}
|
path={siteRoot + 'sys/file-scan-records'}
|
||||||
currentTab={currentTab}
|
currentTab={currentTab}
|
||||||
tabItemClick={this.tabItemClick}
|
tabItemClick={this.tabItemClick}
|
||||||
/>
|
/>
|
||||||
<VirusScanRecords
|
|
||||||
path={siteRoot + 'sys/virus-scan-records'}
|
|
||||||
currentTab={currentTab}
|
|
||||||
tabItemClick={this.tabItemClick}
|
|
||||||
/>
|
|
||||||
<WorkWeixinDepartments
|
<WorkWeixinDepartments
|
||||||
path={siteRoot + 'sys/work-weixin'}
|
path={siteRoot + 'sys/work-weixin'}
|
||||||
currentTab={currentTab}
|
currentTab={currentTab}
|
||||||
|
@@ -202,9 +202,9 @@ class SidePanel extends React.Component {
|
|||||||
{isPro && otherPermission &&
|
{isPro && otherPermission &&
|
||||||
<li className="nav-item">
|
<li className="nav-item">
|
||||||
<Link
|
<Link
|
||||||
className={`nav-link ellipsis ${this.getActiveClass('virus-scan-records')}`}
|
className={`nav-link ellipsis ${this.getActiveClass('virus-files')}`}
|
||||||
to={siteRoot + 'sys/virus-scan-records/'}
|
to={siteRoot + 'sys/virus-files/all/'}
|
||||||
onClick={() => this.props.tabItemClick('virus-scan-records')}
|
onClick={() => this.props.tabItemClick('virus-files')}
|
||||||
>
|
>
|
||||||
<span className="sf2-icon-security" aria-hidden="true"></span>
|
<span className="sf2-icon-security" aria-hidden="true"></span>
|
||||||
<span className="nav-text">{gettext('Virus Scan')}</span>
|
<span className="nav-text">{gettext('Virus Scan')}</span>
|
||||||
|
@@ -1,206 +0,0 @@
|
|||||||
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 (
|
|
||||||
<tr>
|
|
||||||
<td>{record.repo_name}</td>
|
|
||||||
<td>{record.repo_owner}</td>
|
|
||||||
<td>{record.file_path}</td>
|
|
||||||
<td>
|
|
||||||
{
|
|
||||||
this.state.handleStatus ?
|
|
||||||
<span style={{color: 'green'}}>{gettext('Handled')}</span> :
|
|
||||||
<a style={{color: 'red', cursor: 'pointer'}} onClick={this.deleteVirusScanRecord}>{gettext('Delete')}</a>
|
|
||||||
}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 <span className="loading-icon loading-tip"></span>;
|
|
||||||
} else if (errorMsg) {
|
|
||||||
return <p className="error text-center">{errorMsg}</p>;
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
<table width="100%" className="table table-hover table-vcenter">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th width="28%">{gettext('Library')}</th>
|
|
||||||
<th width="28%">{gettext('Owner')}</th>
|
|
||||||
<th width="29%">{gettext('Virus File')}</th>
|
|
||||||
<th width="15%">{gettext('Operations')}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{records.map((record, index) => {
|
|
||||||
return (
|
|
||||||
<RecordItem key={index} record={record} />
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{isLoadingMore ? <span className="loading-icon loading-tip"></span> : ''}
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<Fragment>
|
|
||||||
<div className="main-panel-north border-left-show">
|
|
||||||
<div className="cur-view-toolbar">
|
|
||||||
<span className="sf2-icon-menu side-nav-toggle hidden-md-up d-md-none" title="Side Nav Menu"></span>
|
|
||||||
</div>
|
|
||||||
<div className="common-toolbar">
|
|
||||||
<Account isAdminPanel={true} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="main-panel-center">
|
|
||||||
<div className="cur-view-container" id="content-scan-records">
|
|
||||||
<div className="cur-view-path">
|
|
||||||
<h3 className="sf-heading">{gettext('Virus Scan Records')}</h3>
|
|
||||||
</div>
|
|
||||||
<div className="cur-view-content" onScroll={this.handleScroll}>
|
|
||||||
<RecordList
|
|
||||||
loading={this.state.loading}
|
|
||||||
isLoadingMore={this.state.isLoadingMore}
|
|
||||||
errorMsg={this.state.errorMsg}
|
|
||||||
records={this.state.records}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default VirusScanRecords;
|
|
313
frontend/src/pages/sys-admin/virus-scan/all-virus-files.js
Normal file
313
frontend/src/pages/sys-admin/virus-scan/all-virus-files.js
Normal file
@@ -0,0 +1,313 @@
|
|||||||
|
import React, { Component, Fragment } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { seafileAPI } from '../../../utils/seafile-api';
|
||||||
|
import { gettext } from '../../../utils/constants';
|
||||||
|
import { Utils } from '../../../utils/utils';
|
||||||
|
import toaster from '../../../components/toast';
|
||||||
|
import OpMenu from '../../../components/dialog/op-menu';
|
||||||
|
import Loading from '../../../components/loading';
|
||||||
|
import Paginator from '../../../components/paginator';
|
||||||
|
import MainPanelTopbar from '../main-panel-topbar';
|
||||||
|
import Nav from './nav';
|
||||||
|
|
||||||
|
const virusFileItemPropTypes = {
|
||||||
|
virusFile: PropTypes.object.isRequired,
|
||||||
|
isItemFreezed: PropTypes.bool.isRequired,
|
||||||
|
onFreezedItem: PropTypes.func.isRequired,
|
||||||
|
onUnfreezedItem: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
class VirusFileItem extends Component {
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
highlight: false,
|
||||||
|
isOpIconShown: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseEnter = () => {
|
||||||
|
if (!this.props.isItemFreezed) {
|
||||||
|
this.setState({
|
||||||
|
isOpIconShown: true,
|
||||||
|
highlight: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseLeave = () => {
|
||||||
|
if (!this.props.isItemFreezed) {
|
||||||
|
this.setState({
|
||||||
|
isOpIconShown: false,
|
||||||
|
highlight: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnfreezedItem = () => {
|
||||||
|
this.setState({
|
||||||
|
highlight: false,
|
||||||
|
isOpIconShow: false
|
||||||
|
});
|
||||||
|
this.props.onUnfreezedItem();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMenuItemClick = (operation) => {
|
||||||
|
this.props.handleFile(this.props.virusFile.virus_id, operation);
|
||||||
|
}
|
||||||
|
|
||||||
|
translateOperations = (item) => {
|
||||||
|
let translateResult = '';
|
||||||
|
switch(item) {
|
||||||
|
case 'delete':
|
||||||
|
translateResult = gettext('Delete');
|
||||||
|
break;
|
||||||
|
case 'ignore':
|
||||||
|
translateResult = gettext('Ignore');
|
||||||
|
break;
|
||||||
|
case 'do-not-ignore':
|
||||||
|
translateResult = gettext('Don\'t ignore');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return translateResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const virusFile = this.props.virusFile;
|
||||||
|
let fileStatus = '',
|
||||||
|
fileOpList = [];
|
||||||
|
if (virusFile.has_deleted) {
|
||||||
|
fileStatus = <span className="text-green">{gettext('Deleted')}</span>;
|
||||||
|
} else if (virusFile.has_ignored) {
|
||||||
|
fileStatus = <span className="text-orange">{gettext('Ignored')}</span>;
|
||||||
|
fileOpList = ['do-not-ignore'];
|
||||||
|
} else {
|
||||||
|
fileStatus = <span className="text-red">{gettext('Unhandled')}</span>;
|
||||||
|
fileOpList = ['delete', 'ignore'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr className={this.state.highlight ? 'tr-highlight' : ''} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
|
||||||
|
<td>{virusFile.repo_name}</td>
|
||||||
|
<td>{virusFile.repo_owner}</td>
|
||||||
|
<td>{virusFile.file_path}</td>
|
||||||
|
<td>{fileStatus}</td>
|
||||||
|
<td>
|
||||||
|
{fileOpList.length > 0 && this.state.isOpIconShown &&
|
||||||
|
<OpMenu
|
||||||
|
operations={fileOpList}
|
||||||
|
translateOperations={this.translateOperations}
|
||||||
|
onMenuItemClick={this.onMenuItemClick}
|
||||||
|
onFreezedItem={this.props.onFreezedItem}
|
||||||
|
onUnfreezedItem={this.onUnfreezedItem}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VirusFileItem.propTypes = virusFileItemPropTypes;
|
||||||
|
|
||||||
|
|
||||||
|
const virusFileListPropTypes = {
|
||||||
|
loading: PropTypes.bool.isRequired,
|
||||||
|
errorMsg: PropTypes.string.isRequired,
|
||||||
|
virusFiles: PropTypes.array.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
class Content extends Component {
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
isItemFreezed: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
onFreezedItem = () => {
|
||||||
|
this.setState({isItemFreezed: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnfreezedItem = () => {
|
||||||
|
this.setState({isItemFreezed: false});
|
||||||
|
}
|
||||||
|
|
||||||
|
getPreviousPage = () => {
|
||||||
|
this.props.getListByPage(this.props.currentPage - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
getNextPage = () => {
|
||||||
|
this.props.getListByPage(this.props.currentPage + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
loading, errorMsg, virusFiles,
|
||||||
|
curPerPage, hasNextPage, currentPage
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <Loading />;
|
||||||
|
} else if (errorMsg) {
|
||||||
|
return <p className="error text-center mt-4">{errorMsg}</p>;
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th width="27%">{gettext('Library')}</th>
|
||||||
|
<th width="25%">{gettext('Owner')}</th>
|
||||||
|
<th width="28%">{gettext('Virus File')}</th>
|
||||||
|
<th width="15%">{gettext('Status')}</th>
|
||||||
|
<th width="5%">{/* Operations */}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{virusFiles.map((virusFile, index) => {
|
||||||
|
return (
|
||||||
|
<VirusFileItem
|
||||||
|
key={index}
|
||||||
|
virusFile={virusFile}
|
||||||
|
isItemFreezed={this.state.isItemFreezed}
|
||||||
|
onFreezedItem={this.onFreezedItem}
|
||||||
|
onUnfreezedItem={this.onUnfreezedItem}
|
||||||
|
handleFile={this.props.handleFile}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{virusFiles.length > 0 &&
|
||||||
|
<Paginator
|
||||||
|
gotoPreviousPage={this.getPreviousPage}
|
||||||
|
gotoNextPage={this.getNextPage}
|
||||||
|
currentPage={currentPage}
|
||||||
|
hasNextPage={hasNextPage}
|
||||||
|
curPerPage={curPerPage}
|
||||||
|
resetPerPage={this.props.resetPerPage}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Content.propTypes = virusFileListPropTypes;
|
||||||
|
|
||||||
|
|
||||||
|
class AllVirusFiles extends Component {
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
loading: true,
|
||||||
|
errorMsg: '',
|
||||||
|
virusFiles: [],
|
||||||
|
currentPage: 1,
|
||||||
|
perPage: 25,
|
||||||
|
hasNextPage: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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.getListByPage(this.state.currentPage);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getListByPage = (page) => {
|
||||||
|
const { perPage } = this.state;
|
||||||
|
seafileAPI.listVirusFiles(page, perPage).then((res) => {
|
||||||
|
const data = res.data;
|
||||||
|
this.setState({
|
||||||
|
loading: false,
|
||||||
|
virusFiles: data.virus_file_list,
|
||||||
|
hasNextPage: data.has_next_page
|
||||||
|
});
|
||||||
|
}).catch((error) => {
|
||||||
|
this.setState({
|
||||||
|
loading: false,
|
||||||
|
errorMsg: Utils.getErrorMsg(error, true) // true: show login tip if 403
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
resetPerPage = (perPage) => {
|
||||||
|
this.setState({
|
||||||
|
perPage: perPage
|
||||||
|
}, () => {
|
||||||
|
this.getListByPage(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFile = (virusID, op) => {
|
||||||
|
let request;
|
||||||
|
switch(op) {
|
||||||
|
case 'delete':
|
||||||
|
request = seafileAPI.deleteVirusFile(virusID);
|
||||||
|
break;
|
||||||
|
case 'ignore':
|
||||||
|
request = seafileAPI.toggleIgnoreVirusFile(virusID, true);
|
||||||
|
break;
|
||||||
|
case 'do-not-ignore':
|
||||||
|
request = seafileAPI.toggleIgnoreVirusFile(virusID, false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
request.then((res) => {
|
||||||
|
this.setState({
|
||||||
|
virusFiles: this.state.virusFiles.map((item) => {
|
||||||
|
if (item.virus_id == virusID) {
|
||||||
|
if (op == 'delete') {
|
||||||
|
item.has_deleted = true;
|
||||||
|
} else {
|
||||||
|
item = res.data.virus_file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}).catch((error) => {
|
||||||
|
toaster.danger(Utils.getErrorMsg(error));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<MainPanelTopbar />
|
||||||
|
<div className="main-panel-center">
|
||||||
|
<div className="cur-view-container">
|
||||||
|
<Nav currentItem="all" />
|
||||||
|
<div className="cur-view-content">
|
||||||
|
<Content
|
||||||
|
loading={this.state.loading}
|
||||||
|
errorMsg={this.state.errorMsg}
|
||||||
|
virusFiles={this.state.virusFiles}
|
||||||
|
currentPage={this.state.currentPage}
|
||||||
|
hasNextPage={this.state.hasNextPage}
|
||||||
|
curPerPage={this.state.perPage}
|
||||||
|
resetPerPage={this.resetPerPage}
|
||||||
|
getListByPage={this.getListByPage}
|
||||||
|
handleFile={this.handleFile}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AllVirusFiles;
|
40
frontend/src/pages/sys-admin/virus-scan/nav.js
Normal file
40
frontend/src/pages/sys-admin/virus-scan/nav.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Link } from '@reach/router';
|
||||||
|
import { siteRoot, gettext } from '../../../utils/constants';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
currentItem: PropTypes.string.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
class Nav extends React.Component {
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.navItems = [
|
||||||
|
{name: 'all', urlPart: 'all', text: gettext('All')},
|
||||||
|
{name: 'unhandled', urlPart: 'unhandled', text: gettext('Unhandled')}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { currentItem } = this.props;
|
||||||
|
return (
|
||||||
|
<div className="cur-view-path tab-nav-container">
|
||||||
|
<ul className="nav">
|
||||||
|
{this.navItems.map((item, index) => {
|
||||||
|
return (
|
||||||
|
<li className="nav-item" key={index}>
|
||||||
|
<Link to={`${siteRoot}sys/virus-files/${item.urlPart}/`} className={`nav-link${currentItem == item.name ? ' active' : ''}`}>{item.text}</Link>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Nav.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default Nav;
|
421
frontend/src/pages/sys-admin/virus-scan/unhandled-virus-files.js
Normal file
421
frontend/src/pages/sys-admin/virus-scan/unhandled-virus-files.js
Normal file
@@ -0,0 +1,421 @@
|
|||||||
|
import React, { Component, Fragment } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Button } from 'reactstrap';
|
||||||
|
import { seafileAPI } from '../../../utils/seafile-api';
|
||||||
|
import { gettext } from '../../../utils/constants';
|
||||||
|
import { Utils } from '../../../utils/utils';
|
||||||
|
import toaster from '../../../components/toast';
|
||||||
|
import OpMenu from '../../../components/dialog/op-menu';
|
||||||
|
import Loading from '../../../components/loading';
|
||||||
|
import Paginator from '../../../components/paginator';
|
||||||
|
import MainPanelTopbar from '../main-panel-topbar';
|
||||||
|
import Nav from './nav';
|
||||||
|
|
||||||
|
const virusFileItemPropTypes = {
|
||||||
|
virusFile: PropTypes.object.isRequired,
|
||||||
|
isItemFreezed: PropTypes.bool.isRequired,
|
||||||
|
onFreezedItem: PropTypes.func.isRequired,
|
||||||
|
onUnfreezedItem: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
class VirusFileItem extends Component {
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
highlight: false,
|
||||||
|
isOpIconShown: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseEnter = () => {
|
||||||
|
if (!this.props.isItemFreezed) {
|
||||||
|
this.setState({
|
||||||
|
isOpIconShown: true,
|
||||||
|
highlight: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseLeave = () => {
|
||||||
|
if (!this.props.isItemFreezed) {
|
||||||
|
this.setState({
|
||||||
|
isOpIconShown: false,
|
||||||
|
highlight: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnfreezedItem = () => {
|
||||||
|
this.setState({
|
||||||
|
highlight: false,
|
||||||
|
isOpIconShow: false
|
||||||
|
});
|
||||||
|
this.props.onUnfreezedItem();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMenuItemClick = (operation) => {
|
||||||
|
this.props.handleFile(this.props.virusFile.virus_id, operation);
|
||||||
|
}
|
||||||
|
|
||||||
|
translateOperations = (item) => {
|
||||||
|
let translateResult = '';
|
||||||
|
switch(item) {
|
||||||
|
case 'delete':
|
||||||
|
translateResult = gettext('Delete');
|
||||||
|
break;
|
||||||
|
case 'ignore':
|
||||||
|
translateResult = gettext('Ignore');
|
||||||
|
break;
|
||||||
|
case 'do-not-ignore':
|
||||||
|
translateResult = gettext('Don\'t ignore');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return translateResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleItemSelected = (e) => {
|
||||||
|
this.props.toggleItemSelected(this.props.virusFile, e.target.checked);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const virusFile = this.props.virusFile;
|
||||||
|
let fileStatus = '',
|
||||||
|
fileOpList = [];
|
||||||
|
if (virusFile.has_deleted) {
|
||||||
|
fileStatus = <span className="text-green">{gettext('Deleted')}</span>;
|
||||||
|
} else if (virusFile.has_ignored) {
|
||||||
|
fileStatus = <span className="text-orange">{gettext('Ignored')}</span>;
|
||||||
|
fileOpList = ['do-not-ignore'];
|
||||||
|
} else {
|
||||||
|
fileStatus = <span className="text-red">{gettext('Unhandled')}</span>;
|
||||||
|
fileOpList = ['delete', 'ignore'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr className={this.state.highlight ? 'tr-highlight' : ''} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
|
||||||
|
<td className="text-center">
|
||||||
|
<input type="checkbox" checked={virusFile.isSelected} onChange={this.toggleItemSelected} />
|
||||||
|
</td>
|
||||||
|
<td>{virusFile.repo_name}</td>
|
||||||
|
<td>{virusFile.repo_owner}</td>
|
||||||
|
<td>{virusFile.file_path}</td>
|
||||||
|
<td>{fileStatus}</td>
|
||||||
|
<td>
|
||||||
|
{fileOpList.length > 0 && this.state.isOpIconShown &&
|
||||||
|
<OpMenu
|
||||||
|
operations={fileOpList}
|
||||||
|
translateOperations={this.translateOperations}
|
||||||
|
onMenuItemClick={this.onMenuItemClick}
|
||||||
|
onFreezedItem={this.props.onFreezedItem}
|
||||||
|
onUnfreezedItem={this.onUnfreezedItem}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VirusFileItem.propTypes = virusFileItemPropTypes;
|
||||||
|
|
||||||
|
|
||||||
|
const virusFileListPropTypes = {
|
||||||
|
loading: PropTypes.bool.isRequired,
|
||||||
|
errorMsg: PropTypes.string.isRequired,
|
||||||
|
virusFiles: PropTypes.array.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
class Content extends Component {
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
isItemFreezed: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
onFreezedItem = () => {
|
||||||
|
this.setState({isItemFreezed: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnfreezedItem = () => {
|
||||||
|
this.setState({isItemFreezed: false});
|
||||||
|
}
|
||||||
|
|
||||||
|
getPreviousPage = () => {
|
||||||
|
this.props.getListByPage(this.props.currentPage - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
getNextPage = () => {
|
||||||
|
this.props.getListByPage(this.props.currentPage + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
loading, errorMsg, virusFiles,
|
||||||
|
curPerPage, hasNextPage, currentPage,
|
||||||
|
isAllItemsSelected
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <Loading />;
|
||||||
|
} else if (errorMsg) {
|
||||||
|
return <p className="error text-center mt-4">{errorMsg}</p>;
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th width="3%" className="text-center">
|
||||||
|
<input type="checkbox" checked={isAllItemsSelected} onChange={this.props.toggleAllSelected} />
|
||||||
|
</th>
|
||||||
|
<th width="24%">{gettext('Library')}</th>
|
||||||
|
<th width="25%">{gettext('Owner')}</th>
|
||||||
|
<th width="28%">{gettext('Virus File')}</th>
|
||||||
|
<th width="15%">{gettext('Status')}</th>
|
||||||
|
<th width="5%">{/* Operations */}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{virusFiles.map((virusFile, index) => {
|
||||||
|
return (
|
||||||
|
<VirusFileItem
|
||||||
|
key={index}
|
||||||
|
virusFile={virusFile}
|
||||||
|
isItemFreezed={this.state.isItemFreezed}
|
||||||
|
onFreezedItem={this.onFreezedItem}
|
||||||
|
onUnfreezedItem={this.onUnfreezedItem}
|
||||||
|
handleFile={this.props.handleFile}
|
||||||
|
toggleItemSelected={this.props.toggleItemSelected}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{virusFiles.length > 0 &&
|
||||||
|
<Paginator
|
||||||
|
gotoPreviousPage={this.getPreviousPage}
|
||||||
|
gotoNextPage={this.getNextPage}
|
||||||
|
currentPage={currentPage}
|
||||||
|
hasNextPage={hasNextPage}
|
||||||
|
curPerPage={curPerPage}
|
||||||
|
resetPerPage={this.props.resetPerPage}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Content.propTypes = virusFileListPropTypes;
|
||||||
|
|
||||||
|
|
||||||
|
class UnhandledVirusFiles extends Component {
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
loading: true,
|
||||||
|
errorMsg: '',
|
||||||
|
virusFiles: [],
|
||||||
|
|
||||||
|
isAllItemsSelected: false,
|
||||||
|
selectedItems: [],
|
||||||
|
|
||||||
|
currentPage: 1,
|
||||||
|
perPage: 25,
|
||||||
|
hasNextPage: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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.getListByPage(this.state.currentPage);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getListByPage = (page) => {
|
||||||
|
const { perPage } = this.state;
|
||||||
|
const hasHandled = false;
|
||||||
|
seafileAPI.listVirusFiles(page, perPage, hasHandled).then((res) => {
|
||||||
|
const data = res.data;
|
||||||
|
const items = data.virus_file_list.map(item => {
|
||||||
|
item.isSelected = false;
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
this.setState({
|
||||||
|
loading: false,
|
||||||
|
virusFiles: items,
|
||||||
|
hasNextPage: data.has_next_page
|
||||||
|
});
|
||||||
|
}).catch((error) => {
|
||||||
|
this.setState({
|
||||||
|
loading: false,
|
||||||
|
errorMsg: Utils.getErrorMsg(error, true) // true: show login tip if 403
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
resetPerPage = (perPage) => {
|
||||||
|
this.setState({
|
||||||
|
perPage: perPage
|
||||||
|
}, () => {
|
||||||
|
this.getListByPage(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFile = (virusID, op) => {
|
||||||
|
let request;
|
||||||
|
switch(op) {
|
||||||
|
case 'delete':
|
||||||
|
request = seafileAPI.deleteVirusFile(virusID);
|
||||||
|
break;
|
||||||
|
case 'ignore':
|
||||||
|
request = seafileAPI.toggleIgnoreVirusFile(virusID, true);
|
||||||
|
break;
|
||||||
|
case 'do-not-ignore':
|
||||||
|
request = seafileAPI.toggleIgnoreVirusFile(virusID, false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
request.then((res) => {
|
||||||
|
this.setState({
|
||||||
|
virusFiles: this.state.virusFiles.map((item) => {
|
||||||
|
if (item.virus_id == virusID) {
|
||||||
|
if (op == 'delete') {
|
||||||
|
item.has_deleted = true;
|
||||||
|
} else {
|
||||||
|
item = res.data.virus_file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}).catch((error) => {
|
||||||
|
toaster.danger(Utils.getErrorMsg(error));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleAllSelected = () => {
|
||||||
|
this.setState((prevState) => ({
|
||||||
|
isAllItemsSelected: !prevState.isAllItemsSelected,
|
||||||
|
virusFiles: this.state.virusFiles.map((item) => {
|
||||||
|
item.isSelected = !prevState.isAllItemsSelected;
|
||||||
|
return item;
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleItemSelected = (targetItem, isSelected) => {
|
||||||
|
this.setState({
|
||||||
|
virusFiles: this.state.virusFiles.map((item) => {
|
||||||
|
if (item === targetItem) {
|
||||||
|
item.isSelected = isSelected;
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
})
|
||||||
|
}, () => {
|
||||||
|
this.setState({
|
||||||
|
isAllItemsSelected: !this.state.virusFiles.some(item => !item.isSelected)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSelectedItems = (op) => {
|
||||||
|
// op: 'delete-virus', 'ignore-virus'
|
||||||
|
const virusIDs = this.state.virusFiles
|
||||||
|
.filter(item => {
|
||||||
|
if (op == 'delete-virus') {
|
||||||
|
return item.isSelected && !item.has_deleted;
|
||||||
|
} else {
|
||||||
|
return item.isSelected && !item.has_ignored;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map(item => item.virus_id);
|
||||||
|
seafileAPI.batchProcessVirusFiles(virusIDs, op).then((res) => {
|
||||||
|
let fileList = this.state.virusFiles;
|
||||||
|
res.data.success.forEach(item => {
|
||||||
|
let file = fileList.find(file => file.virus_id == item.virus_id);
|
||||||
|
if (op == 'delete-virus') {
|
||||||
|
file.has_deleted = true;
|
||||||
|
} else {
|
||||||
|
file.has_ignored = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.setState({
|
||||||
|
virusFiles: fileList
|
||||||
|
});
|
||||||
|
|
||||||
|
res.data.failed.forEach(item => {
|
||||||
|
const file = fileList.find(file => file.virus_id == item.virus_id);
|
||||||
|
let errMsg = op == 'delete-virus' ?
|
||||||
|
gettext('Failed to delete %(virus_file) from library %(library): %(error_msg)') :
|
||||||
|
gettext('Failed to ignore %(virus_file) from library %(library): %(error_msg)');
|
||||||
|
errMsg = errMsg.replace('%(virus_file)', file.file_path)
|
||||||
|
.replace('%(library)', file.repo_name)
|
||||||
|
.replace('%(error_msg)', item.error_msg);
|
||||||
|
toaster.danger(errMsg);
|
||||||
|
});
|
||||||
|
}).catch((error) => {
|
||||||
|
toaster.danger(Utils.getErrorMsg(error));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteSelectedItems = () => {
|
||||||
|
const op = 'delete-virus';
|
||||||
|
this.handleSelectedItems(op);
|
||||||
|
}
|
||||||
|
|
||||||
|
ignoreSelectedItems = () => {
|
||||||
|
const op = 'ignore-virus';
|
||||||
|
this.handleSelectedItems(op);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
{this.state.virusFiles.some(item => item.isSelected) ? (
|
||||||
|
<MainPanelTopbar>
|
||||||
|
<Fragment>
|
||||||
|
<Button onClick={this.deleteSelectedItems} className="operation-item">{gettext('Delete')}</Button>
|
||||||
|
<Button onClick={this.ignoreSelectedItems} className="operation-item">{gettext('Ignore')}</Button>
|
||||||
|
</Fragment>
|
||||||
|
</MainPanelTopbar>
|
||||||
|
) : <MainPanelTopbar />
|
||||||
|
}
|
||||||
|
<div className="main-panel-center">
|
||||||
|
<div className="cur-view-container">
|
||||||
|
<Nav currentItem="unhandled" />
|
||||||
|
<div className="cur-view-content">
|
||||||
|
<Content
|
||||||
|
loading={this.state.loading}
|
||||||
|
errorMsg={this.state.errorMsg}
|
||||||
|
virusFiles={this.state.virusFiles}
|
||||||
|
currentPage={this.state.currentPage}
|
||||||
|
hasNextPage={this.state.hasNextPage}
|
||||||
|
curPerPage={this.state.perPage}
|
||||||
|
resetPerPage={this.resetPerPage}
|
||||||
|
getListByPage={this.getListByPage}
|
||||||
|
handleFile={this.handleFile}
|
||||||
|
isAllItemsSelected={this.state.isAllItemsSelected}
|
||||||
|
toggleAllSelected={this.toggleAllSelected}
|
||||||
|
toggleItemSelected={this.toggleItemSelected}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UnhandledVirusFiles;
|
@@ -7,26 +7,27 @@ from rest_framework.permissions import IsAdminUser
|
|||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
from seaserv import seafile_api
|
from seaserv import seafile_api
|
||||||
|
|
||||||
from seahub.api2.authentication import TokenAuthentication
|
from seahub.api2.authentication import TokenAuthentication
|
||||||
from seahub.api2.throttling import UserRateThrottle
|
from seahub.api2.throttling import UserRateThrottle
|
||||||
from seahub.api2.permissions import IsProVersion
|
from seahub.api2.permissions import IsProVersion
|
||||||
from seahub.api2.utils import api_error
|
from seahub.api2.utils import api_error, to_python_boolean
|
||||||
from seahub.utils import get_virus_record, get_virus_record_by_id, handle_virus_record
|
from seahub.utils import get_virus_files, get_virus_file_by_vid, delete_virus_file, operate_virus_file
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class AdminVirusScanRecords(APIView):
|
class AdminVirusFilesView(APIView):
|
||||||
|
|
||||||
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
||||||
permission_classes = (IsAdminUser, IsProVersion)
|
permission_classes = (IsAdminUser, IsProVersion)
|
||||||
throttle_classes = (UserRateThrottle,)
|
throttle_classes = (UserRateThrottle,)
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""get virus scan records
|
"""get virus files
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not request.user.admin_permissions.other_permission():
|
if not request.user.admin_permissions.other_permission():
|
||||||
@@ -42,21 +43,32 @@ class AdminVirusScanRecords(APIView):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
per_page = 25
|
per_page = 25
|
||||||
|
|
||||||
|
try:
|
||||||
|
has_handled = to_python_boolean(request.GET.get('has_handled', ''))
|
||||||
|
except ValueError:
|
||||||
|
has_handled = None
|
||||||
|
|
||||||
start = (page - 1) * per_page
|
start = (page - 1) * per_page
|
||||||
count = per_page
|
count = per_page + 1
|
||||||
|
|
||||||
try:
|
try:
|
||||||
virus_records = get_virus_record(start=start, limit=count)
|
virus_files = get_virus_files(has_handled=has_handled, start=start, limit=count)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(e)
|
logger.error(e)
|
||||||
error_msg = 'Internal Server Error'
|
error_msg = 'Internal Server Error'
|
||||||
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||||
|
|
||||||
record_list = list()
|
if len(virus_files) > per_page:
|
||||||
for virus_record in virus_records:
|
virus_files = virus_files[:per_page]
|
||||||
|
has_next_page = True
|
||||||
|
else:
|
||||||
|
has_next_page = False
|
||||||
|
|
||||||
|
virus_file_list = list()
|
||||||
|
for virus_file in virus_files:
|
||||||
try:
|
try:
|
||||||
repo = seafile_api.get_repo(virus_record.repo_id)
|
repo = seafile_api.get_repo(virus_file.repo_id)
|
||||||
repo_owner = seafile_api.get_repo_owner(virus_record.repo_id)
|
repo_owner = seafile_api.get_repo_owner(virus_file.repo_id)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(e)
|
logger.error(e)
|
||||||
continue
|
continue
|
||||||
@@ -67,40 +79,176 @@ class AdminVirusScanRecords(APIView):
|
|||||||
record = dict()
|
record = dict()
|
||||||
record["repo_name"] = repo.name
|
record["repo_name"] = repo.name
|
||||||
record["repo_owner"] = repo_owner
|
record["repo_owner"] = repo_owner
|
||||||
record["file_path"] = virus_record.file_path
|
record["file_path"] = virus_file.file_path
|
||||||
record["has_handle"] = virus_record.has_handle
|
record["has_deleted"] = virus_file.has_deleted
|
||||||
record["virus_id"] = virus_record.vid
|
record["has_ignored"] = virus_file.has_ignored
|
||||||
record_list.append(record)
|
record["virus_id"] = virus_file.vid
|
||||||
|
virus_file_list.append(record)
|
||||||
|
|
||||||
return Response({"record_list": record_list}, status=status.HTTP_200_OK)
|
return Response({"virus_file_list": virus_file_list, "has_next_page": has_next_page}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
class AdminVirusScanRecord(APIView):
|
class AdminVirusFileView(APIView):
|
||||||
|
|
||||||
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
||||||
permission_classes = (IsAdminUser, IsProVersion)
|
permission_classes = (IsAdminUser, IsProVersion)
|
||||||
throttle_classes = (UserRateThrottle,)
|
throttle_classes = (UserRateThrottle,)
|
||||||
|
|
||||||
def delete(self, request, virus_id):
|
def delete(self, request, virus_id):
|
||||||
|
"""delete virus file
|
||||||
|
"""
|
||||||
|
|
||||||
if not request.user.admin_permissions.other_permission():
|
if not request.user.admin_permissions.other_permission():
|
||||||
return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.')
|
return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.')
|
||||||
|
|
||||||
virus_record = get_virus_record_by_id(virus_id)
|
virus_file = get_virus_file_by_vid(virus_id)
|
||||||
if not virus_record:
|
if not virus_file:
|
||||||
error_msg = 'Virus record %d not found.' % virus_id
|
error_msg = 'Virus file %s not found.' % virus_id
|
||||||
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||||
|
|
||||||
parent_dir = os.path.dirname(virus_record.file_path)
|
parent_dir = os.path.dirname(virus_file.file_path)
|
||||||
filename = os.path.basename(virus_record.file_path)
|
filename = os.path.basename(virus_file.file_path)
|
||||||
try:
|
try:
|
||||||
seafile_api.del_file(
|
seafile_api.del_file(
|
||||||
virus_record.repo_id, parent_dir, filename, request.user.username
|
virus_file.repo_id, parent_dir, filename, request.user.username
|
||||||
)
|
)
|
||||||
handle_virus_record(virus_id)
|
delete_virus_file(virus_id)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(e)
|
logger.error(e)
|
||||||
error_msg = 'Failed to delete, please try again later.'
|
error_msg = 'Internal Server Error'
|
||||||
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||||
|
|
||||||
return Response({"success": True}, status=status.HTTP_200_OK)
|
return Response({"success": True}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
def put(self, request, virus_id):
|
||||||
|
"""ignore or un-ignore virus file
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not request.user.admin_permissions.other_permission():
|
||||||
|
return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.')
|
||||||
|
|
||||||
|
ignore = request.data.get('ignore')
|
||||||
|
if ignore not in ('true', 'false'):
|
||||||
|
error_msg = 'ignore invalid.'
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||||
|
ignore = to_python_boolean(ignore)
|
||||||
|
|
||||||
|
virus_file = get_virus_file_by_vid(virus_id)
|
||||||
|
if not virus_file:
|
||||||
|
error_msg = 'Virus file %s not found.' % virus_id
|
||||||
|
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||||
|
|
||||||
|
try:
|
||||||
|
operate_virus_file(virus_id, ignore)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
error_msg = 'Internal Server Error'
|
||||||
|
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||||
|
|
||||||
|
virus_file = get_virus_file_by_vid(virus_id)
|
||||||
|
|
||||||
|
repo = seafile_api.get_repo(virus_file.repo_id)
|
||||||
|
repo_owner = seafile_api.get_repo_owner(virus_file.repo_id)
|
||||||
|
res = dict(repo_name=repo.name)
|
||||||
|
res["repo_owner"] = repo_owner
|
||||||
|
res["file_path"] = virus_file.file_path
|
||||||
|
res["has_deleted"] = virus_file.has_deleted
|
||||||
|
res["has_ignored"] = virus_file.has_ignored
|
||||||
|
res["virus_id"] = virus_file.vid
|
||||||
|
|
||||||
|
return Response({"virus_file": res}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
class AdminVirusFilesBatchView(APIView):
|
||||||
|
|
||||||
|
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
||||||
|
permission_classes = (IsAdminUser, IsProVersion)
|
||||||
|
throttle_classes = (UserRateThrottle,)
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
""" Delete virus files, ignore or cancel ignore virus files, in batch.
|
||||||
|
|
||||||
|
Permission checking:
|
||||||
|
1. admin user.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not request.user.admin_permissions.other_permission():
|
||||||
|
return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.')
|
||||||
|
|
||||||
|
# argument check
|
||||||
|
virus_ids = request.POST.getlist('virus_ids', None)
|
||||||
|
if not virus_ids:
|
||||||
|
error_msg = 'virus_ids invalid.'
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||||
|
|
||||||
|
operation = request.POST.get('operation', None)
|
||||||
|
if operation not in ('delete-virus', 'ignore-virus', 'cancel-ignore-virus'):
|
||||||
|
error_msg = "operation can only be 'delete-virus', 'ignore-virus' or 'cancel-ignore-virus'."
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||||
|
|
||||||
|
result = dict(failed=[])
|
||||||
|
result['success'] = []
|
||||||
|
|
||||||
|
virus_files = []
|
||||||
|
for virus_id in virus_ids:
|
||||||
|
virus_file = get_virus_file_by_vid(int(virus_id))
|
||||||
|
if virus_file:
|
||||||
|
virus_files.append(virus_file)
|
||||||
|
else:
|
||||||
|
result['failed'].append({
|
||||||
|
'virus_id': virus_id,
|
||||||
|
'error_msg': _('Virus file not found.')
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
|
||||||
|
if operation == 'delete-virus':
|
||||||
|
for virus_file in virus_files:
|
||||||
|
parent_dir = os.path.dirname(virus_file.file_path)
|
||||||
|
filename = os.path.basename(virus_file.file_path)
|
||||||
|
virus_id = int(virus_file.vid)
|
||||||
|
try:
|
||||||
|
seafile_api.del_file(
|
||||||
|
virus_file.repo_id, parent_dir, filename, request.user.username
|
||||||
|
)
|
||||||
|
delete_virus_file(virus_id)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
result['failed'].append({
|
||||||
|
'virus_id': virus_id,
|
||||||
|
'error_msg': _('Internal Server Error')
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
|
||||||
|
result['success'].append({'virus_id': virus_id})
|
||||||
|
|
||||||
|
if operation == 'ignore-virus':
|
||||||
|
for virus_file in virus_files:
|
||||||
|
virus_id = int(virus_file.vid)
|
||||||
|
try:
|
||||||
|
operate_virus_file(virus_id, True)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
result['failed'].append({
|
||||||
|
'virus_id': virus_id,
|
||||||
|
'error_msg': _('Internal Server Error')
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
|
||||||
|
result['success'].append({'virus_id': virus_id})
|
||||||
|
|
||||||
|
if operation == 'cancel-ignore-virus':
|
||||||
|
for virus_file in virus_files:
|
||||||
|
virus_id = int(virus_file.vid)
|
||||||
|
try:
|
||||||
|
operate_virus_file(virus_id, False)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
result['failed'].append({
|
||||||
|
'virus_id': virus_id,
|
||||||
|
'error_msg': _('Internal Server Error')
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
|
||||||
|
result['success'].append({'virus_id': virus_id})
|
||||||
|
|
||||||
|
return Response(result)
|
||||||
|
@@ -171,7 +171,8 @@ from seahub.api2.endpoints.admin.work_weixin import AdminWorkWeixinDepartments,
|
|||||||
from seahub.api2.endpoints.admin.dingtalk import AdminDingtalkDepartments, \
|
from seahub.api2.endpoints.admin.dingtalk import AdminDingtalkDepartments, \
|
||||||
AdminDingtalkDepartmentMembers, AdminDingtalkUsersBatch, \
|
AdminDingtalkDepartmentMembers, AdminDingtalkUsersBatch, \
|
||||||
AdminDingtalkDepartmentsImport
|
AdminDingtalkDepartmentsImport
|
||||||
from seahub.api2.endpoints.admin.virus_scan_records import AdminVirusScanRecords, AdminVirusScanRecord
|
from seahub.api2.endpoints.admin.virus_scan_records import AdminVirusFilesView, AdminVirusFileView, \
|
||||||
|
AdminVirusFilesBatchView
|
||||||
from seahub.api2.endpoints.file_participants import FileParticipantsView, FileParticipantView
|
from seahub.api2.endpoints.file_participants import FileParticipantsView, FileParticipantView
|
||||||
from seahub.api2.endpoints.repo_related_users import RepoRelatedUsersView
|
from seahub.api2.endpoints.repo_related_users import RepoRelatedUsersView
|
||||||
|
|
||||||
@@ -614,9 +615,10 @@ urlpatterns = [
|
|||||||
## admin::file-scan-records
|
## admin::file-scan-records
|
||||||
url(r'^api/v2.1/admin/file-scan-records/$', AdminFileScanRecords.as_view(), name='api-v2.1-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
|
# admin::virus-files
|
||||||
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-files/$', AdminVirusFilesView.as_view(), name='api-v2.1-admin-virus-files'),
|
||||||
url(r'^api/v2.1/admin/virus-scan-records/(?P<virus_id>\d+)/$', AdminVirusScanRecord.as_view(), name='api-v2.1-admin-virus-scan-record'),
|
url(r'^api/v2.1/admin/virus-files/(?P<virus_id>\d+)/$', AdminVirusFileView.as_view(), name='api-v2.1-admin-virus-file'),
|
||||||
|
url(r'^api/v2.1/admin/virus-files/batch/$', AdminVirusFilesBatchView.as_view(), name='api-v2.1-admin-virus-files-batch'),
|
||||||
|
|
||||||
## admin::notifications
|
## admin::notifications
|
||||||
url(r'^api/v2.1/admin/notifications/$', AdminNotificationsView.as_view(), name='api-2.1-admin-notifications'),
|
url(r'^api/v2.1/admin/notifications/$', AdminNotificationsView.as_view(), name='api-2.1-admin-notifications'),
|
||||||
@@ -731,7 +733,8 @@ if ENABLE_FILE_SCAN:
|
|||||||
from seahub.utils import EVENTS_ENABLED
|
from seahub.utils import EVENTS_ENABLED
|
||||||
if EVENTS_ENABLED:
|
if EVENTS_ENABLED:
|
||||||
urlpatterns += [
|
urlpatterns += [
|
||||||
url(r'^sys/virus-scan-records/$', sysadmin_react_fake_view, name='sys_virus_scan_records'),
|
url(r'^sys/virus-files/all/$', sysadmin_react_fake_view, name='sys_virus_scan_records'),
|
||||||
|
url(r'^sys/virus-files/unhandled/$', sysadmin_react_fake_view, name='sys_virus_scan_records'),
|
||||||
]
|
]
|
||||||
|
|
||||||
if settings.SERVE_STATIC:
|
if settings.SERVE_STATIC:
|
||||||
|
@@ -823,18 +823,22 @@ if EVENTS_CONFIG_FILE:
|
|||||||
|
|
||||||
return events if events else None
|
return events if events else None
|
||||||
|
|
||||||
def get_virus_record(repo_id=None, start=-1, limit=-1):
|
def get_virus_files(repo_id=None, has_handled=None, start=-1, limit=-1):
|
||||||
with _get_seafevents_session() as session:
|
with _get_seafevents_session() as session:
|
||||||
r = seafevents.get_virus_record(session, repo_id, start, limit)
|
r = seafevents.get_virus_files(session, repo_id, has_handled, start, limit)
|
||||||
return r if r else []
|
return r if r else []
|
||||||
|
|
||||||
def handle_virus_record(vid):
|
def delete_virus_file(vid):
|
||||||
with _get_seafevents_session() as session:
|
with _get_seafevents_session() as session:
|
||||||
return True if seafevents.handle_virus_record(session, vid) == 0 else False
|
return True if seafevents.delete_virus_file(session, vid) == 0 else False
|
||||||
|
|
||||||
def get_virus_record_by_id(vid):
|
def operate_virus_file(vid, ignore):
|
||||||
with _get_seafevents_session() as session:
|
with _get_seafevents_session() as session:
|
||||||
return seafevents.get_virus_record_by_id(session, vid)
|
return True if seafevents.operate_virus_file(session, vid, ignore) == 0 else False
|
||||||
|
|
||||||
|
def get_virus_file_by_vid(vid):
|
||||||
|
with _get_seafevents_session() as session:
|
||||||
|
return seafevents.get_virus_file_by_vid(session, vid)
|
||||||
|
|
||||||
def get_file_scan_record(start=-1, limit=-1):
|
def get_file_scan_record(start=-1, limit=-1):
|
||||||
records = seafevents_api.get_content_scan_results(start, limit)
|
records = seafevents_api.get_content_scan_results(start, limit)
|
||||||
@@ -876,11 +880,13 @@ else:
|
|||||||
pass
|
pass
|
||||||
def get_perm_audit_events():
|
def get_perm_audit_events():
|
||||||
pass
|
pass
|
||||||
def get_virus_record():
|
def get_virus_files():
|
||||||
pass
|
pass
|
||||||
def handle_virus_record():
|
def delete_virus_file():
|
||||||
pass
|
pass
|
||||||
def get_virus_record_by_id(vid):
|
def operate_virus_file():
|
||||||
|
pass
|
||||||
|
def get_virus_file_by_vid(vid):
|
||||||
pass
|
pass
|
||||||
def get_file_scan_record():
|
def get_file_scan_record():
|
||||||
pass
|
pass
|
||||||
|
@@ -48,8 +48,8 @@ from seahub.role_permissions.models import AdminRole
|
|||||||
from seahub.two_factor.models import default_device
|
from seahub.two_factor.models import default_device
|
||||||
from seahub.utils import IS_EMAIL_CONFIGURED, string2list, is_valid_username, \
|
from seahub.utils import IS_EMAIL_CONFIGURED, string2list, is_valid_username, \
|
||||||
is_pro_version, send_html_email, \
|
is_pro_version, send_html_email, \
|
||||||
get_server_id, handle_virus_record, get_virus_record_by_id, \
|
get_server_id, delete_virus_file, get_virus_file_by_vid, \
|
||||||
get_virus_record, FILE_AUDIT_ENABLED, get_max_upload_file_size, \
|
get_virus_files, FILE_AUDIT_ENABLED, get_max_upload_file_size, \
|
||||||
get_site_name, seafevents_api
|
get_site_name, seafevents_api
|
||||||
from seahub.utils.ip import get_remote_ip
|
from seahub.utils.ip import get_remote_ip
|
||||||
from seahub.utils.file_size import get_file_size_unit
|
from seahub.utils.file_size import get_file_size_unit
|
||||||
|
Reference in New Issue
Block a user