feat: 工具箱病毒扫描支持定时扫描 (#5847)

This commit is contained in:
ssongliu 2024-07-17 16:55:28 +08:00 committed by GitHub
parent ca0c96cb12
commit 3c0dc7459c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 747 additions and 26 deletions

View file

@ -51,16 +51,38 @@ func (b *BaseApi) UpdateClam(c *gin.Context) {
helper.SuccessWithData(c, nil)
}
// @Tags Clam
// @Summary Update clam status
// @Description 修改扫描规则状态
// @Accept json
// @Param request body dto.ClamUpdateStatus true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /toolbox/clam/status/update [post]
// @x-panel-log {"bodyKeys":["id","status"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"clams","output_column":"name","output_value":"name"}],"formatZH":"修改扫描规则 [name] 状态为 [status]","formatEN":"change the status of clam [name] to [status]."}
func (b *BaseApi) UpdateClamStatus(c *gin.Context) {
var req dto.ClamUpdateStatus
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := clamService.UpdateStatus(req.ID, req.Status); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}
// @Tags Clam
// @Summary Page clam
// @Description 获取扫描规则列表分页
// @Accept json
// @Param request body dto.SearchWithPage true "request"
// @Param request body dto.SearchClamWithPage true "request"
// @Success 200 {object} dto.PageResult
// @Security ApiKeyAuth
// @Router /toolbox/clam/search [post]
func (b *BaseApi) SearchClam(c *gin.Context) {
var req dto.SearchWithPage
var req dto.SearchClamWithPage
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}

View file

@ -4,6 +4,13 @@ import (
"time"
)
type SearchClamWithPage struct {
PageInfo
Info string `json:"info"`
OrderBy string `json:"orderBy" validate:"required,oneof=name status created_at"`
Order string `json:"order" validate:"required,oneof=null ascending descending"`
}
type ClamBaseInfo struct {
Version string `json:"version"`
IsActive bool `json:"isActive"`
@ -19,10 +26,12 @@ type ClamInfo struct {
CreatedAt time.Time `json:"createdAt"`
Name string `json:"name"`
Status string `json:"status"`
Path string `json:"path"`
InfectedStrategy string `json:"infectedStrategy"`
InfectedDir string `json:"infectedDir"`
LastHandleDate string `json:"lastHandleDate"`
Spec string `json:"spec"`
Description string `json:"description"`
}
@ -56,9 +65,11 @@ type ClamLog struct {
type ClamCreate struct {
Name string `json:"name"`
Status string `json:"status"`
Path string `json:"path"`
InfectedStrategy string `json:"infectedStrategy"`
InfectedDir string `json:"infectedDir"`
Spec string `json:"spec"`
Description string `json:"description"`
}
@ -69,9 +80,15 @@ type ClamUpdate struct {
Path string `json:"path"`
InfectedStrategy string `json:"infectedStrategy"`
InfectedDir string `json:"infectedDir"`
Spec string `json:"spec"`
Description string `json:"description"`
}
type ClamUpdateStatus struct {
ID uint `json:"id"`
Status string `json:"status"`
}
type ClamDelete struct {
RemoveRecord bool `json:"removeRecord"`
RemoveInfected bool `json:"removeInfected"`

View file

@ -4,8 +4,11 @@ type Clam struct {
BaseModel
Name string `gorm:"type:varchar(64);not null" json:"name"`
Status string `gorm:"type:varchar(64)" json:"status"`
Path string `gorm:"type:varchar(64);not null" json:"path"`
InfectedStrategy string `gorm:"type:varchar(64)" json:"infectedStrategy"`
InfectedDir string `gorm:"type:varchar(64)" json:"infectedDir"`
Spec string `gorm:"type:varchar(64)" json:"spec"`
EntryID int `gorm:"type:varchar(64)" json:"entryID"`
Description string `gorm:"type:varchar(64)" json:"description"`
}

View file

@ -13,6 +13,7 @@ type IClamRepo interface {
Update(id uint, vars map[string]interface{}) error
Delete(opts ...DBOption) error
Get(opts ...DBOption) (model.Clam, error)
List(opts ...DBOption) ([]model.Clam, error)
}
func NewIClamRepo() IClamRepo {
@ -29,6 +30,16 @@ func (u *ClamRepo) Get(opts ...DBOption) (model.Clam, error) {
return clam, err
}
func (u *ClamRepo) List(opts ...DBOption) ([]model.Clam, error) {
var clam []model.Clam
db := global.DB
for _, opt := range opts {
db = opt(db)
}
err := db.Find(&clam).Error
return clam, err
}
func (u *ClamRepo) Page(page, size int, opts ...DBOption) (int64, []model.Clam, error) {
var users []model.Clam
db := global.DB.Model(&model.Clam{})

View file

@ -12,13 +12,16 @@ import (
"time"
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/app/model"
"github.com/1Panel-dev/1Panel/backend/buserr"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
"github.com/1Panel-dev/1Panel/backend/utils/common"
"github.com/1Panel-dev/1Panel/backend/utils/systemctl"
"github.com/1Panel-dev/1Panel/backend/utils/xpack"
"github.com/jinzhu/copier"
"github.com/robfig/cron/v3"
"github.com/pkg/errors"
)
@ -37,9 +40,10 @@ type ClamService struct {
type IClamService interface {
LoadBaseInfo() (dto.ClamBaseInfo, error)
Operate(operate string) error
SearchWithPage(search dto.SearchWithPage) (int64, interface{}, error)
SearchWithPage(search dto.SearchClamWithPage) (int64, interface{}, error)
Create(req dto.ClamCreate) error
Update(req dto.ClamUpdate) error
UpdateStatus(id uint, status string) error
Delete(req dto.ClamDelete) error
HandleOnce(req dto.OperateByID) error
LoadFile(req dto.ClamFileReq) (string, error)
@ -75,8 +79,7 @@ func (c *ClamService) LoadBaseInfo() (dto.ClamBaseInfo, error) {
baseInfo.FreshIsExist = true
baseInfo.FreshIsActive, _ = systemctl.IsActive(freshClamService)
}
stdout, err := cmd.Exec("which clamdscan")
if err != nil || (len(strings.ReplaceAll(stdout, "\n", "")) == 0 && strings.HasPrefix(stdout, "/")) {
if !cmd.Which("clamdscan") {
baseInfo.IsActive = false
}
@ -122,8 +125,8 @@ func (c *ClamService) Operate(operate string) error {
}
}
func (c *ClamService) SearchWithPage(req dto.SearchWithPage) (int64, interface{}, error) {
total, commands, err := clamRepo.Page(req.Page, req.PageSize, commonRepo.WithLikeName(req.Info))
func (c *ClamService) SearchWithPage(req dto.SearchClamWithPage) (int64, interface{}, error) {
total, commands, err := clamRepo.Page(req.Page, req.PageSize, commonRepo.WithLikeName(req.Info), commonRepo.WithOrderRuleBy(req.OrderBy, req.Order))
if err != nil {
return 0, nil, err
}
@ -164,6 +167,14 @@ func (c *ClamService) Create(req dto.ClamCreate) error {
if clam.InfectedStrategy == "none" || clam.InfectedStrategy == "remove" {
clam.InfectedDir = ""
}
if len(req.Spec) != 0 {
entryID, err := xpack.StartClam(clam, false)
if err != nil {
return err
}
clam.EntryID = entryID
clam.Status = constant.StatusEnable
}
if err := clamRepo.Create(&clam); err != nil {
return err
}
@ -178,11 +189,36 @@ func (c *ClamService) Update(req dto.ClamUpdate) error {
if req.InfectedStrategy == "none" || req.InfectedStrategy == "remove" {
req.InfectedDir = ""
}
var clamItem model.Clam
if err := copier.Copy(&clamItem, &req); err != nil {
return errors.WithMessage(constant.ErrStructTransform, err.Error())
}
clamItem.EntryID = clam.EntryID
upMap := map[string]interface{}{}
if len(clam.Spec) != 0 && clam.EntryID != 0 {
global.Cron.Remove(cron.EntryID(clamItem.EntryID))
upMap["entry_id"] = 0
}
if len(req.Spec) == 0 {
upMap["status"] = ""
upMap["entry_id"] = 0
}
if len(req.Spec) != 0 && clam.Status != constant.StatusDisable {
newEntryID, err := xpack.StartClam(clamItem, true)
if err != nil {
return err
}
upMap["entry_id"] = newEntryID
}
if len(clam.Spec) == 0 && len(req.Spec) != 0 {
upMap["status"] = constant.StatusEnable
}
upMap["name"] = req.Name
upMap["path"] = req.Path
upMap["infected_dir"] = req.InfectedDir
upMap["infected_strategy"] = req.InfectedStrategy
upMap["spec"] = req.Spec
upMap["description"] = req.Description
if err := clamRepo.Update(req.ID, upMap); err != nil {
return err
@ -190,6 +226,28 @@ func (c *ClamService) Update(req dto.ClamUpdate) error {
return nil
}
func (c *ClamService) UpdateStatus(id uint, status string) error {
clam, _ := clamRepo.Get(commonRepo.WithByID(id))
if clam.ID == 0 {
return constant.ErrRecordNotFound
}
var (
entryID int
err error
)
if status == constant.StatusEnable {
entryID, err = xpack.StartClam(clam, true)
if err != nil {
return err
}
} else {
global.Cron.Remove(cron.EntryID(clam.EntryID))
global.LOG.Infof("stop cronjob entryID: %v", clam.EntryID)
}
return clamRepo.Update(clam.ID, map[string]interface{}{"status": status, "entry_id": entryID})
}
func (c *ClamService) Delete(req dto.ClamDelete) error {
for _, id := range req.Ids {
clam, _ := clamRepo.Get(commonRepo.WithByID(id))

View file

@ -92,6 +92,7 @@ func Init() {
migrations.AddForward,
migrations.AddShellColumn,
migrations.AddClam,
migrations.AddClamStatus,
})
if err := m.Migrate(); err != nil {
global.LOG.Error(err)

View file

@ -278,3 +278,13 @@ var AddClam = &gormigrate.Migration{
return nil
},
}
var AddClamStatus = &gormigrate.Migration{
ID: "20240716-add-clam-status",
Migrate: func(tx *gorm.DB) error {
if err := tx.AutoMigrate(&model.Clam{}); err != nil {
return err
}
return nil
},
}

View file

@ -56,6 +56,7 @@ func (s *ToolboxRouter) InitRouter(Router *gin.RouterGroup) {
toolboxRouter.POST("/clam/base", baseApi.LoadClamBaseInfo)
toolboxRouter.POST("/clam/operate", baseApi.OperateClam)
toolboxRouter.POST("/clam/update", baseApi.UpdateClam)
toolboxRouter.POST("/clam/status/update", baseApi.UpdateClamStatus)
toolboxRouter.POST("/clam/del", baseApi.DeleteClam)
toolboxRouter.POST("/clam/handle", baseApi.HandleClamScan)
}

View file

@ -203,8 +203,11 @@ func SudoHandleCmd() string {
}
func Which(name string) bool {
_, err := exec.LookPath(name)
return err == nil
stdout, err := Execf("which %s", name)
if err != nil || (len(strings.ReplaceAll(stdout, "\n", "")) == 0 && strings.HasPrefix(stdout, "/")) {
return false
}
return true
}
func ExecShellWithTimeOut(cmdStr, workdir string, logger *log.Logger, timeout time.Duration) error {

View file

@ -7,6 +7,10 @@ import (
"net"
"net/http"
"time"
"github.com/1Panel-dev/1Panel/backend/app/model"
"github.com/1Panel-dev/1Panel/backend/buserr"
"github.com/1Panel-dev/1Panel/backend/constant"
)
func RemoveTamper(website string) {}
@ -27,3 +31,7 @@ func LoadRequestTransport() *http.Transport {
func LoadGpuInfo() []interface{} {
return nil
}
func StartClam(startClam model.Clam, isUpdate bool) (int, error) {
return 0, buserr.New(constant.ErrXpackNotFound)
}

View file

@ -11500,6 +11500,58 @@ const docTemplate = `{
}
}
},
"/toolbox/clam/status/update": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "修改扫描规则状态",
"consumes": [
"application/json"
],
"tags": [
"Clam"
],
"summary": "Update clam status",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.ClamUpdateStatus"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [
{
"db": "clams",
"input_column": "id",
"input_value": "id",
"isList": false,
"output_column": "name",
"output_value": "name"
}
],
"bodyKeys": [
"id",
"status"
],
"formatEN": "change the status of clam [name] to [status].",
"formatZH": "修改扫描规则 [name] 状态为 [status]",
"paramKeys": []
}
}
},
"/toolbox/clam/update": {
"post": {
"security": [
@ -15570,6 +15622,12 @@ const docTemplate = `{
},
"path": {
"type": "string"
},
"spec": {
"type": "string"
},
"status": {
"type": "string"
}
}
},
@ -15665,6 +15723,20 @@ const docTemplate = `{
},
"path": {
"type": "string"
},
"spec": {
"type": "string"
}
}
},
"dto.ClamUpdateStatus": {
"type": "object",
"properties": {
"id": {
"type": "integer"
},
"status": {
"type": "string"
}
}
},
@ -18468,7 +18540,7 @@ const docTemplate = `{
"type": "string",
"enum": [
"name",
"status",
"state",
"created_at"
]
},
@ -22601,7 +22673,8 @@ const docTemplate = `{
"primary_domain",
"type",
"status",
"created_at"
"created_at",
"expire_date"
]
},
"page": {
@ -22619,8 +22692,7 @@ const docTemplate = `{
"type": "object",
"required": [
"id",
"primaryDomain",
"webSiteGroupID"
"primaryDomain"
],
"properties": {
"IPV6": {

View file

@ -11493,6 +11493,58 @@
}
}
},
"/toolbox/clam/status/update": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "修改扫描规则状态",
"consumes": [
"application/json"
],
"tags": [
"Clam"
],
"summary": "Update clam status",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.ClamUpdateStatus"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [
{
"db": "clams",
"input_column": "id",
"input_value": "id",
"isList": false,
"output_column": "name",
"output_value": "name"
}
],
"bodyKeys": [
"id",
"status"
],
"formatEN": "change the status of clam [name] to [status].",
"formatZH": "修改扫描规则 [name] 状态为 [status]",
"paramKeys": []
}
}
},
"/toolbox/clam/update": {
"post": {
"security": [
@ -15563,6 +15615,12 @@
},
"path": {
"type": "string"
},
"spec": {
"type": "string"
},
"status": {
"type": "string"
}
}
},
@ -15658,6 +15716,20 @@
},
"path": {
"type": "string"
},
"spec": {
"type": "string"
}
}
},
"dto.ClamUpdateStatus": {
"type": "object",
"properties": {
"id": {
"type": "integer"
},
"status": {
"type": "string"
}
}
},
@ -18461,7 +18533,7 @@
"type": "string",
"enum": [
"name",
"status",
"state",
"created_at"
]
},
@ -22594,7 +22666,8 @@
"primary_domain",
"type",
"status",
"created_at"
"created_at",
"expire_date"
]
},
"page": {
@ -22612,8 +22685,7 @@
"type": "object",
"required": [
"id",
"primaryDomain",
"webSiteGroupID"
"primaryDomain"
],
"properties": {
"IPV6": {

View file

@ -243,6 +243,10 @@ definitions:
type: string
path:
type: string
spec:
type: string
status:
type: string
type: object
dto.ClamDelete:
properties:
@ -305,6 +309,15 @@ definitions:
type: string
path:
type: string
spec:
type: string
type: object
dto.ClamUpdateStatus:
properties:
id:
type: integer
status:
type: string
type: object
dto.Clean:
properties:
@ -2198,7 +2211,7 @@ definitions:
orderBy:
enum:
- name
- status
- state
- created_at
type: string
page:
@ -4974,6 +4987,7 @@ definitions:
- type
- status
- created_at
- expire_date
type: string
page:
type: integer
@ -5004,7 +5018,6 @@ definitions:
required:
- id
- primaryDomain
- webSiteGroupID
type: object
request.WebsiteUpdateDir:
properties:
@ -12767,6 +12780,40 @@ paths:
summary: Page clam
tags:
- Clam
/toolbox/clam/status/update:
post:
consumes:
- application/json
description: 修改扫描规则状态
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.ClamUpdateStatus'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
summary: Update clam status
tags:
- Clam
x-panel-log:
BeforeFunctions:
- db: clams
input_column: id
input_value: id
isList: false
output_column: name
output_value: name
bodyKeys:
- id
- status
formatEN: change the status of clam [name] to [status].
formatZH: 修改扫描规则 [name] 状态为 [status]
paramKeys: []
/toolbox/clam/update:
post:
consumes:

View file

@ -1,4 +1,5 @@
import { ReqPage } from '.';
import { Cronjob } from './cronjob';
export namespace Toolbox {
export interface DeviceBaseInfo {
@ -129,10 +130,14 @@ export namespace Toolbox {
export interface ClamInfo {
id: number;
name: string;
status: string;
path: string;
infectedStrategy: string;
infectedDir: string;
lastHandleDate: string;
hasSpec: boolean;
spec: string;
specObj: Cronjob.SpecObj;
description: string;
}
export interface ClamCreate {
@ -140,6 +145,8 @@ export namespace Toolbox {
path: string;
infectedStrategy: string;
infectedDir: string;
spec: string;
specObj: Cronjob.SpecObj;
description: string;
}
export interface ClamUpdate {
@ -148,6 +155,8 @@ export namespace Toolbox {
path: string;
infectedStrategy: string;
infectedDir: string;
spec: string;
specObj: Cronjob.SpecObj;
description: string;
}
export interface ClamSearchLog extends ReqPage {

View file

@ -138,6 +138,9 @@ export const createClam = (params: Toolbox.ClamCreate) => {
export const updateClam = (params: Toolbox.ClamUpdate) => {
return http.post(`/toolbox/clam/update`, params);
};
export const updateClamStatus = (id: number, status: string) => {
return http.post(`/toolbox/clam/status/update`, { id: id, status: status });
};
export const deleteClam = (params: { ids: number[]; removeRecord: boolean; removeInfected: boolean }) => {
return http.post(`/toolbox/clam/del`, params);
};

View file

@ -1082,6 +1082,13 @@ const message = {
},
clam: {
clam: 'Virus Scan',
cron: 'Scheduled scan',
cronHelper: 'Professional version supports scheduled scan feature',
specErr: 'Execution schedule format error, please check and retry!',
disableMsg:
'Stopping scheduled execution will prevent this scan task from running automatically. Do you want to continue?',
enableMsg:
'Enabling scheduled execution will allow this scan task to run automatically at regular intervals. Do you want to continue?',
showFresh: 'Show Virus Database Service',
hideFresh: 'Hide Virus Database Service',
clamHelper:
@ -1577,6 +1584,7 @@ const message = {
recoverDetail: 'Recover detail',
createSnapshot: 'Create Snapshot',
importSnapshot: 'Sync Snapshot',
importHelper: 'Snapshot directory:',
recover: 'Recover',
lastRecoverAt: 'Last recovery time',
lastRollbackAt: 'Last rollback time',

View file

@ -1023,6 +1023,11 @@ const message = {
},
clam: {
clam: '病毒掃描',
cron: '定時掃描',
cronHelper: '專業版支持定時掃描功能',
specErr: '執行周期格式錯誤請檢查後重試',
disableMsg: '停止定時執行會導致該掃描任務不再自動執行是否繼續',
enableMsg: '啟用定時執行會讓該掃描任務定期自動執行是否繼續',
showFresh: '顯示病毒庫服務',
hideFresh: '隱藏病毒庫服務',
clamHelper:
@ -1395,6 +1400,7 @@ const message = {
recoverDetail: '恢復詳情',
createSnapshot: '創建快照',
importSnapshot: '同步快照',
importHelper: '快照文件目錄',
recover: '恢復',
lastRecoverAt: '上次恢復時間',
lastRollbackAt: '上次回滾時間',

View file

@ -1024,6 +1024,11 @@ const message = {
},
clam: {
clam: '病毒扫描',
cron: '定时扫描',
cronHelper: '专业版支持定时扫描功能 ',
specErr: '执行周期格式错误请检查后重试',
disableMsg: '停止定时执行会导致该扫描任务不再自动执行是否继续',
enableMsg: '启用定时执行会让该扫描任务定期自动执行是否继续',
showFresh: '显示病毒库服务',
hideFresh: '隐藏病毒库服务',
clamHelper:
@ -1397,6 +1402,7 @@ const message = {
recoverDetail: '恢复详情',
createSnapshot: '创建快照',
importSnapshot: '同步快照',
importHelper: '快照文件目录',
recover: '恢复',
lastRecoverAt: '上次恢复时间',
lastRollbackAt: '上次回滚时间',

View file

@ -16,6 +16,10 @@
:label="item.label"
/>
</el-select>
<div v-if="form.from === 'LOCAL'">
<span class="import-help">{{ $t('setting.importHelper') }}</span>
<span @click="toFolder()" class="import-link-help">{{ backupPath }}</span>
</div>
</el-form-item>
<el-form-item :label="$t('commons.table.name')" prop="names">
<el-select style="width: 100%" v-model="form.names" multiple clearable>
@ -57,6 +61,7 @@ import { snapshotImport } from '@/api/modules/setting';
import { getBackupList, getFilesFromBackup } from '@/api/modules/setting';
import { Rules } from '@/global/form-rules';
import { MsgSuccess } from '@/utils/message';
import router from '@/routers';
const drawerVisible = ref(false);
const loading = ref();
@ -65,6 +70,7 @@ const formRef = ref();
const backupOptions = ref();
const fileNames = ref();
const existNames = ref();
const backupPath = ref('');
const form = reactive({
from: '',
@ -102,6 +108,9 @@ const checkDisable = (val: string) => {
}
return false;
};
const toFolder = async () => {
router.push({ path: '/hosts/files', query: { path: backupPath.value } });
};
const submitImport = async (formEl: FormInstance | undefined) => {
loading.value = true;
@ -131,6 +140,10 @@ const loadBackups = async () => {
if (item.id !== 0) {
backupOptions.value.push({ label: i18n.global.t('setting.' + item.type), value: item.type });
}
if (item.type === 'LOCAL') {
item.varsJson = JSON.parse(item.vars);
backupPath.value = item.varsJson['dir'] + '/system_snapshot';
}
}
})
.catch(() => {
@ -148,3 +161,18 @@ defineExpose({
acceptParams,
});
</script>
<style lang="scss" scoped>
.import-help {
font-size: 12px;
color: #8f959e;
}
.import-link-help {
color: $primary-color;
cursor: pointer;
}
.import-link-help:hover {
opacity: 0.6;
}
</style>

View file

@ -405,10 +405,17 @@ const search = async () => {
page: paginationConfig.currentPage,
pageSize: paginationConfig.pageSize,
};
const res = await searchSnapshotPage(params);
loading.value = true;
await searchSnapshotPage(params)
.then((res) => {
loading.value = false;
cleanData.value = false;
data.value = res.data.items || [];
paginationConfig.total = res.data.total;
})
.catch(() => {
loading.value = false;
});
};
onMounted(() => {

View file

@ -56,6 +56,7 @@
:label="$t('commons.table.name')"
:min-width="60"
prop="name"
sortable
show-overflow-tooltip
>
<template #default="{ row }">
@ -74,6 +75,47 @@
<el-button link type="primary" @click="toFolder(row.path)">{{ row.path }}</el-button>
</template>
</el-table-column>
<el-table-column
v-if="isProductPro"
:label="$t('commons.table.status')"
:min-width="70"
prop="status"
sortable
>
<template #default="{ row }">
<el-button
v-if="row.status === 'Enable'"
@click="onChangeStatus(row.id, 'disable')"
link
icon="VideoPlay"
type="success"
>
{{ $t('commons.status.enabled') }}
</el-button>
<el-button
v-if="row.status === 'Disable'"
icon="VideoPause"
link
type="danger"
@click="onChangeStatus(row.id, 'enable')"
>
{{ $t('commons.status.disabled') }}
</el-button>
<span v-if="row.status === ''">-</span>
</template>
</el-table-column>
<el-table-column
v-if="isProductPro"
:label="$t('cronjob.cronSpec')"
show-overflow-tooltip
:min-width="120"
>
<template #default="{ row }">
<span>
{{ row.spec !== '' ? transSpecToStr(row.spec) : '-' }}
</span>
</template>
</el-table-column>
<el-table-column
:label="$t('toolbox.clam.infectedDir')"
:min-width="120"
@ -138,17 +180,22 @@
import { onMounted, reactive, ref } from 'vue';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
import { deleteClam, handleClamScan, searchClam, updateClam } from '@/api/modules/toolbox';
import { deleteClam, handleClamScan, searchClam, updateClam, updateClamStatus } from '@/api/modules/toolbox';
import OperateDialog from '@/views/toolbox/clam/operate/index.vue';
import LogDialog from '@/views/toolbox/clam/record/index.vue';
import ClamStatus from '@/views/toolbox/clam/status/index.vue';
import SettingDialog from '@/views/toolbox/clam/setting/index.vue';
import { Toolbox } from '@/api/interface/toolbox';
import router from '@/routers';
import { transSpecToStr } from '../../cronjob/helper';
import { GlobalStore } from '@/store';
import { storeToRefs } from 'pinia';
const loading = ref();
const selects = ref<any>([]);
const globalStore = GlobalStore();
const { isProductPro } = storeToRefs(globalStore);
const data = ref();
const paginationConfig = reactive({
cacheSizeKey: 'clam-page-size',
@ -176,12 +223,16 @@ const clamStatus = ref({
isRunning: true,
});
const search = async () => {
const search = async (column?: any) => {
paginationConfig.orderBy = column?.order ? column.prop : paginationConfig.orderBy;
paginationConfig.order = column?.order ? column.order : paginationConfig.order;
loading.value = true;
let params = {
info: searchName.value,
page: paginationConfig.currentPage,
pageSize: paginationConfig.pageSize,
orderBy: paginationConfig.orderBy,
order: paginationConfig.order,
};
await searchClam(params)
.then((res) => {
@ -218,6 +269,14 @@ const onOpenDialog = async (
title: string,
rowData: Partial<Toolbox.ClamInfo> = {
infectedStrategy: 'none',
specObj: {
specType: 'perDay',
week: 1,
day: 3,
hour: 1,
minute: 30,
second: 30,
},
},
) => {
let params = {
@ -272,6 +331,18 @@ const onSubmitDelete = async () => {
});
};
const onChangeStatus = async (id: number, status: string) => {
ElMessageBox.confirm(i18n.global.t('toolbox.clam.' + status + 'Msg'), i18n.global.t('cronjob.changeStatus'), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
}).then(async () => {
let itemStatus = status === 'enable' ? 'Enable' : 'Disable';
await updateClamStatus(id, itemStatus);
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
search();
});
};
const buttons = [
{
label: i18n.global.t('commons.button.handle'),

View file

@ -50,6 +50,77 @@
</template>
</el-input>
</el-form-item>
<el-form-item prop="hasSpec">
<el-checkbox v-model="dialogData.rowData!.hasSpec" :label="$t('toolbox.clam.cron')" />
</el-form-item>
<el-form-item v-if="dialogData.rowData!.hasSpec && !isProductPro">
<span>{{ $t('toolbox.clam.cronHelper') }}</span>
<el-button link type="primary" @click="toUpload">
{{ $t('license.levelUpPro') }}
</el-button>
</el-form-item>
<el-form-item prop="spec" v-if="dialogData.rowData!.hasSpec && isProductPro">
<el-select
class="specTypeClass"
v-model="dialogData.rowData!.specObj.specType"
@change="changeSpecType()"
>
<el-option
v-for="item in specOptions"
:key="item.label"
:value="item.value"
:label="item.label"
/>
</el-select>
<el-select
v-if="dialogData.rowData!.specObj.specType === 'perWeek'"
class="specClass"
v-model="dialogData.rowData!.specObj.week"
>
<el-option
v-for="item in weekOptions"
:key="item.label"
:value="item.value"
:label="item.label"
/>
</el-select>
<el-input
v-if="hasDay(dialogData.rowData!.specObj)"
class="specClass"
v-model.number="dialogData.rowData!.specObj.day"
>
<template #append>
<div class="append">{{ $t('cronjob.day') }}</div>
</template>
</el-input>
<el-input
v-if="hasHour(dialogData.rowData!.specObj)"
class="specClass"
v-model.number="dialogData.rowData!.specObj.hour"
>
<template #append>
<div class="append">{{ $t('commons.units.hour') }}</div>
</template>
</el-input>
<el-input
v-if="dialogData.rowData!.specObj.specType !== 'perNSecond'"
class="specClass"
v-model.number="dialogData.rowData!.specObj.minute"
>
<template #append>
<div class="append">{{ $t('commons.units.minute') }}</div>
</template>
</el-input>
<el-input
v-if="dialogData.rowData!.specObj.specType === 'perNSecond'"
class="specClass"
v-model.number="dialogData.rowData!.specObj.second"
>
<template #append>
<div class="append">{{ $t('commons.units.second') }}</div>
</template>
</el-input>
</el-form-item>
<el-form-item :label="$t('commons.table.description')" prop="description">
<el-input type="textarea" :rows="3" clearable v-model="dialogData.rowData!.description" />
</el-form-item>
@ -64,6 +135,7 @@
</el-button>
</span>
</template>
<LicenseImport ref="licenseRef" />
</el-drawer>
</template>
@ -73,11 +145,18 @@ import { Rules } from '@/global/form-rules';
import FileList from '@/components/file-list/index.vue';
import i18n from '@/lang';
import { ElForm } from 'element-plus';
import LicenseImport from '@/components/license-import/index.vue';
import DrawerHeader from '@/components/drawer-header/index.vue';
import { MsgSuccess } from '@/utils/message';
import { MsgError, MsgSuccess } from '@/utils/message';
import { Toolbox } from '@/api/interface/toolbox';
import { createClam, updateClam } from '@/api/modules/toolbox';
import { specOptions, transObjToSpec, transSpecToObj, weekOptions } from '../../../cronjob/helper';
import { storeToRefs } from 'pinia';
import { GlobalStore } from '@/store';
const globalStore = GlobalStore();
const licenseRef = ref();
const { isProductPro } = storeToRefs(globalStore);
interface DialogProps {
title: string;
rowData?: Toolbox.ClamInfo;
@ -92,6 +171,19 @@ const dialogData = ref<DialogProps>({
const acceptParams = (params: DialogProps): void => {
dialogData.value = params;
if (dialogData.value.rowData?.spec) {
dialogData.value.rowData.hasSpec = true;
dialogData.value.rowData.specObj = transSpecToObj(dialogData.value.rowData.spec);
} else {
dialogData.value.rowData.specObj = {
specType: 'perDay',
week: 1,
day: 3,
hour: 1,
minute: 30,
second: 30,
};
}
title.value = i18n.global.t('commons.button.' + dialogData.value.title);
drawerVisible.value = true;
};
@ -101,9 +193,97 @@ const handleClose = () => {
drawerVisible.value = false;
};
const verifySpec = (rule: any, value: any, callback: any) => {
let item = dialogData.value.rowData!.specObj;
if (
!Number.isInteger(item.day) ||
!Number.isInteger(item.hour) ||
!Number.isInteger(item.minute) ||
!Number.isInteger(item.second) ||
!Number.isInteger(item.week)
) {
callback(new Error(i18n.global.t('cronjob.specErr')));
return;
}
switch (item.specType) {
case 'perMonth':
if (
item.day < 0 ||
item.day > 31 ||
item.hour < 0 ||
item.hour > 23 ||
item.minute < 0 ||
item.minute > 59
) {
callback(new Error(i18n.global.t('cronjob.specErr')));
return;
}
break;
case 'perNDay':
if (
item.day < 0 ||
item.day > 366 ||
item.hour < 0 ||
item.hour > 23 ||
item.minute < 0 ||
item.minute > 59
) {
callback(new Error(i18n.global.t('cronjob.specErr')));
return;
}
break;
case 'perWeek':
if (
item.week < 0 ||
item.week > 6 ||
item.hour < 0 ||
item.hour > 23 ||
item.minute < 0 ||
item.minute > 59
) {
callback(new Error(i18n.global.t('cronjob.specErr')));
return;
}
break;
case 'perDay':
if (item.hour < 0 || item.hour > 23 || item.minute < 0 || item.minute > 59) {
callback(new Error(i18n.global.t('cronjob.specErr')));
return;
}
break;
case 'perNHour':
if (item.hour < 0 || item.hour > 8784 || item.minute < 0 || item.minute > 59) {
callback(new Error(i18n.global.t('cronjob.specErr')));
return;
}
break;
case 'perHour':
if (item.minute < 0 || item.minute > 59) {
callback(new Error(i18n.global.t('cronjob.specErr')));
return;
}
case 'perNMinute':
if (item.minute < 0 || item.minute > 527040) {
callback(new Error(i18n.global.t('cronjob.specErr')));
return;
}
break;
case 'perNSecond':
if (item.second < 0 || item.second > 31622400) {
callback(new Error(i18n.global.t('cronjob.specErr')));
return;
}
break;
}
callback();
};
const rules = reactive({
name: [Rules.simpleName],
path: [Rules.requiredInput, Rules.noSpace],
spec: [
{ validator: verifySpec, trigger: 'blur', required: true },
{ validator: verifySpec, trigger: 'change', required: true },
],
});
type FormInstance = InstanceType<typeof ElForm>;
@ -120,12 +300,62 @@ const loadDir = async (path: string) => {
const loadInfectedDir = async (path: string) => {
dialogData.value.rowData!.infectedDir = path;
};
const hasDay = (item: any) => {
return item.specType === 'perMonth' || item.specType === 'perNDay';
};
const hasHour = (item: any) => {
return item.specType !== 'perHour' && item.specType !== 'perNMinute' && item.specType !== 'perNSecond';
};
const toUpload = () => {
licenseRef.value.acceptParams();
};
const changeSpecType = () => {
let item = dialogData.value.rowData!.specObj;
switch (item.specType) {
case 'perMonth':
case 'perNDay':
item.day = 3;
item.hour = 1;
item.minute = 30;
break;
case 'perWeek':
item.week = 1;
item.hour = 1;
item.minute = 30;
break;
case 'perDay':
case 'perNHour':
item.hour = 2;
item.minute = 30;
break;
case 'perHour':
case 'perNMinute':
item.minute = 30;
break;
case 'perNSecond':
item.second = 30;
break;
}
};
const onSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate(async (valid) => {
if (!valid) return;
loading.value = true;
let spec = '';
let item = dialogData.value.rowData.specObj;
if (dialogData.value.rowData!.hasSpec) {
spec = transObjToSpec(item.specType, item.week, item.day, item.hour, item.minute, item.second);
if (spec === '') {
MsgError(i18n.global.t('cronjob.cronSpecHelper'));
return;
}
}
dialogData.value.rowData.spec = spec;
if (dialogData.value.title === 'edit') {
await updateClam(dialogData.value.rowData)
.then(() => {
@ -158,3 +388,31 @@ defineExpose({
acceptParams,
});
</script>
<style scoped lang="scss">
.specClass {
width: 20% !important;
margin-left: 20px;
.append {
width: 20px;
}
}
@media only screen and (max-width: 1000px) {
.specClass {
width: 100% !important;
margin-top: 20px;
margin-left: 0;
.append {
width: 43px;
}
}
}
.specTypeClass {
width: 22% !important;
}
@media only screen and (max-width: 1000px) {
.specTypeClass {
width: 100% !important;
}
}
</style>