mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-16 15:19:06 +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 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 MobileDevices from './devices/mobile-devices';
|
||||
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 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 Notifications from './notifications/notifications';
|
||||
import FileScanRecords from './file-scan-records';
|
||||
import VirusScanRecords from './virus-scan-records';
|
||||
import WorkWeixinDepartments from './work-weixin-departments';
|
||||
import DingtalkDepartments from './dingtalk-departments';
|
||||
import Invitations from './invitations/invitations';
|
||||
import TermsAndConditions from './terms-and-conditions/terms-and-conditions';
|
||||
|
||||
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 AllVirusFiles from './virus-scan/all-virus-files';
|
||||
import UnhandledVirusFiles from './virus-scan/unhandled-virus-files';
|
||||
|
||||
import AdminOperationLogs from './admin-logs/operation-logs';
|
||||
import AdminLoginLogs from './admin-logs/login-logs';
|
||||
|
||||
import '../../assets/css/fa-solid.css';
|
||||
import '../../assets/css/fa-regular.css';
|
||||
@@ -141,6 +142,10 @@ class SysAdmin extends React.Component {
|
||||
tab: 'logs',
|
||||
urlPartList: ['logs/']
|
||||
},
|
||||
{
|
||||
tab: 'virus-files',
|
||||
urlPartList: ['virus-files/']
|
||||
},
|
||||
{
|
||||
tab: 'adminLogs',
|
||||
urlPartList: ['admin-logs/']
|
||||
@@ -244,16 +249,15 @@ class SysAdmin extends React.Component {
|
||||
<Invitations path={siteRoot + 'sys/invitations'} />
|
||||
<TermsAndConditions path={siteRoot + 'sys/terms-and-conditions/'} />
|
||||
|
||||
<AllVirusFiles path={siteRoot + 'sys/virus-files/all'} />
|
||||
<UnhandledVirusFiles path={siteRoot + 'sys/virus-files/unhandled'} />
|
||||
|
||||
<FileScanRecords
|
||||
path={siteRoot + 'sys/file-scan-records'}
|
||||
currentTab={currentTab}
|
||||
tabItemClick={this.tabItemClick}
|
||||
/>
|
||||
<VirusScanRecords
|
||||
path={siteRoot + 'sys/virus-scan-records'}
|
||||
currentTab={currentTab}
|
||||
tabItemClick={this.tabItemClick}
|
||||
/>
|
||||
|
||||
<WorkWeixinDepartments
|
||||
path={siteRoot + 'sys/work-weixin'}
|
||||
currentTab={currentTab}
|
||||
|
@@ -202,9 +202,9 @@ class SidePanel extends React.Component {
|
||||
{isPro && otherPermission &&
|
||||
<li className="nav-item">
|
||||
<Link
|
||||
className={`nav-link ellipsis ${this.getActiveClass('virus-scan-records')}`}
|
||||
to={siteRoot + 'sys/virus-scan-records/'}
|
||||
onClick={() => this.props.tabItemClick('virus-scan-records')}
|
||||
className={`nav-link ellipsis ${this.getActiveClass('virus-files')}`}
|
||||
to={siteRoot + 'sys/virus-files/all/'}
|
||||
onClick={() => this.props.tabItemClick('virus-files')}
|
||||
>
|
||||
<span className="sf2-icon-security" aria-hidden="true"></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.views import APIView
|
||||
from rest_framework import status
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
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
|
||||
from seahub.api2.utils import api_error, to_python_boolean
|
||||
from seahub.utils import get_virus_files, get_virus_file_by_vid, delete_virus_file, operate_virus_file
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AdminVirusScanRecords(APIView):
|
||||
class AdminVirusFilesView(APIView):
|
||||
|
||||
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
||||
permission_classes = (IsAdminUser, IsProVersion)
|
||||
throttle_classes = (UserRateThrottle,)
|
||||
|
||||
def get(self, request):
|
||||
"""get virus scan records
|
||||
"""get virus files
|
||||
"""
|
||||
|
||||
if not request.user.admin_permissions.other_permission():
|
||||
@@ -42,21 +43,32 @@ class AdminVirusScanRecords(APIView):
|
||||
except ValueError:
|
||||
per_page = 25
|
||||
|
||||
try:
|
||||
has_handled = to_python_boolean(request.GET.get('has_handled', ''))
|
||||
except ValueError:
|
||||
has_handled = None
|
||||
|
||||
start = (page - 1) * per_page
|
||||
count = per_page
|
||||
count = per_page + 1
|
||||
|
||||
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:
|
||||
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:
|
||||
if len(virus_files) > per_page:
|
||||
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:
|
||||
repo = seafile_api.get_repo(virus_record.repo_id)
|
||||
repo_owner = seafile_api.get_repo_owner(virus_record.repo_id)
|
||||
repo = seafile_api.get_repo(virus_file.repo_id)
|
||||
repo_owner = seafile_api.get_repo_owner(virus_file.repo_id)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
continue
|
||||
@@ -67,40 +79,176 @@ class AdminVirusScanRecords(APIView):
|
||||
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)
|
||||
record["file_path"] = virus_file.file_path
|
||||
record["has_deleted"] = virus_file.has_deleted
|
||||
record["has_ignored"] = virus_file.has_ignored
|
||||
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)
|
||||
permission_classes = (IsAdminUser, IsProVersion)
|
||||
throttle_classes = (UserRateThrottle,)
|
||||
|
||||
def delete(self, request, virus_id):
|
||||
"""delete virus file
|
||||
"""
|
||||
|
||||
if not request.user.admin_permissions.other_permission():
|
||||
return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.')
|
||||
|
||||
virus_record = get_virus_record_by_id(virus_id)
|
||||
if not virus_record:
|
||||
error_msg = 'Virus record %d not found.' % virus_id
|
||||
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)
|
||||
|
||||
parent_dir = os.path.dirname(virus_record.file_path)
|
||||
filename = os.path.basename(virus_record.file_path)
|
||||
parent_dir = os.path.dirname(virus_file.file_path)
|
||||
filename = os.path.basename(virus_file.file_path)
|
||||
try:
|
||||
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:
|
||||
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 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, \
|
||||
AdminDingtalkDepartmentMembers, AdminDingtalkUsersBatch, \
|
||||
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.repo_related_users import RepoRelatedUsersView
|
||||
|
||||
@@ -614,9 +615,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<virus_id>\d+)/$', AdminVirusScanRecord.as_view(), name='api-v2.1-admin-virus-scan-record'),
|
||||
# admin::virus-files
|
||||
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-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
|
||||
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
|
||||
if EVENTS_ENABLED:
|
||||
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:
|
||||
|
@@ -823,18 +823,22 @@ if EVENTS_CONFIG_FILE:
|
||||
|
||||
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:
|
||||
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 []
|
||||
|
||||
def handle_virus_record(vid):
|
||||
def delete_virus_file(vid):
|
||||
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:
|
||||
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):
|
||||
records = seafevents_api.get_content_scan_results(start, limit)
|
||||
@@ -876,11 +880,13 @@ else:
|
||||
pass
|
||||
def get_perm_audit_events():
|
||||
pass
|
||||
def get_virus_record():
|
||||
def get_virus_files():
|
||||
pass
|
||||
def handle_virus_record():
|
||||
def delete_virus_file():
|
||||
pass
|
||||
def get_virus_record_by_id(vid):
|
||||
def operate_virus_file():
|
||||
pass
|
||||
def get_virus_file_by_vid(vid):
|
||||
pass
|
||||
def get_file_scan_record():
|
||||
pass
|
||||
|
@@ -48,8 +48,8 @@ from seahub.role_permissions.models import AdminRole
|
||||
from seahub.two_factor.models import default_device
|
||||
from seahub.utils import IS_EMAIL_CONFIGURED, string2list, is_valid_username, \
|
||||
is_pro_version, send_html_email, \
|
||||
get_server_id, handle_virus_record, get_virus_record_by_id, \
|
||||
get_virus_record, FILE_AUDIT_ENABLED, get_max_upload_file_size, \
|
||||
get_server_id, delete_virus_file, get_virus_file_by_vid, \
|
||||
get_virus_files, FILE_AUDIT_ENABLED, get_max_upload_file_size, \
|
||||
get_site_name, seafevents_api
|
||||
from seahub.utils.ip import get_remote_ip
|
||||
from seahub.utils.file_size import get_file_size_unit
|
||||
|
Reference in New Issue
Block a user