diff --git a/backend/app/api/v1/clam.go b/backend/app/api/v1/clam.go index 261a6308a..0e4e7cf47 100644 --- a/backend/app/api/v1/clam.go +++ b/backend/app/api/v1/clam.go @@ -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 } diff --git a/backend/app/dto/clam.go b/backend/app/dto/clam.go index 4c93b9d0c..964db15ee 100644 --- a/backend/app/dto/clam.go +++ b/backend/app/dto/clam.go @@ -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"` diff --git a/backend/app/model/clam.go b/backend/app/model/clam.go index 044c7bb31..ff1a73e67 100644 --- a/backend/app/model/clam.go +++ b/backend/app/model/clam.go @@ -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"` } diff --git a/backend/app/repo/clam.go b/backend/app/repo/clam.go index 215915556..4fd212c8c 100644 --- a/backend/app/repo/clam.go +++ b/backend/app/repo/clam.go @@ -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{}) diff --git a/backend/app/service/clam.go b/backend/app/service/clam.go index 9989e7545..e54c4f3d5 100644 --- a/backend/app/service/clam.go +++ b/backend/app/service/clam.go @@ -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)) diff --git a/backend/init/migration/migrate.go b/backend/init/migration/migrate.go index d374d451b..8711fb0fe 100644 --- a/backend/init/migration/migrate.go +++ b/backend/init/migration/migrate.go @@ -92,6 +92,7 @@ func Init() { migrations.AddForward, migrations.AddShellColumn, migrations.AddClam, + migrations.AddClamStatus, }) if err := m.Migrate(); err != nil { global.LOG.Error(err) diff --git a/backend/init/migration/migrations/v_1_10.go b/backend/init/migration/migrations/v_1_10.go index 61de816ef..c87545da6 100644 --- a/backend/init/migration/migrations/v_1_10.go +++ b/backend/init/migration/migrations/v_1_10.go @@ -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 + }, +} diff --git a/backend/router/ro_toolbox.go b/backend/router/ro_toolbox.go index 52b79d9a4..6a331bbb2 100644 --- a/backend/router/ro_toolbox.go +++ b/backend/router/ro_toolbox.go @@ -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) } diff --git a/backend/utils/cmd/cmd.go b/backend/utils/cmd/cmd.go index ff81054d6..46b20fc1f 100644 --- a/backend/utils/cmd/cmd.go +++ b/backend/utils/cmd/cmd.go @@ -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 { diff --git a/backend/utils/xpack/xpack.go b/backend/utils/xpack/xpack.go index 717efbb7e..14cf0b913 100644 --- a/backend/utils/xpack/xpack.go +++ b/backend/utils/xpack/xpack.go @@ -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) +} diff --git a/cmd/server/docs/docs.go b/cmd/server/docs/docs.go index e423e7260..b004a49f5 100644 --- a/cmd/server/docs/docs.go +++ b/cmd/server/docs/docs.go @@ -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": { diff --git a/cmd/server/docs/swagger.json b/cmd/server/docs/swagger.json index 082ca50fe..de2b02277 100644 --- a/cmd/server/docs/swagger.json +++ b/cmd/server/docs/swagger.json @@ -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": { diff --git a/cmd/server/docs/swagger.yaml b/cmd/server/docs/swagger.yaml index 7b975a8ac..9dabd1965 100644 --- a/cmd/server/docs/swagger.yaml +++ b/cmd/server/docs/swagger.yaml @@ -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: diff --git a/frontend/src/api/interface/toolbox.ts b/frontend/src/api/interface/toolbox.ts index 7930e066d..dd94a17ba 100644 --- a/frontend/src/api/interface/toolbox.ts +++ b/frontend/src/api/interface/toolbox.ts @@ -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 { diff --git a/frontend/src/api/modules/toolbox.ts b/frontend/src/api/modules/toolbox.ts index fbcfa3a2e..0002b37a0 100644 --- a/frontend/src/api/modules/toolbox.ts +++ b/frontend/src/api/modules/toolbox.ts @@ -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); }; diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index 02dc6a62b..20e24af6d 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -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', diff --git a/frontend/src/lang/modules/tw.ts b/frontend/src/lang/modules/tw.ts index e73bc7d80..837246ff0 100644 --- a/frontend/src/lang/modules/tw.ts +++ b/frontend/src/lang/modules/tw.ts @@ -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: '上次回滾時間', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index da790c3f5..0b6f4548c 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -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: '上次回滚时间', diff --git a/frontend/src/views/setting/snapshot/import/index.vue b/frontend/src/views/setting/snapshot/import/index.vue index f50a694d2..788f34a35 100644 --- a/frontend/src/views/setting/snapshot/import/index.vue +++ b/frontend/src/views/setting/snapshot/import/index.vue @@ -16,6 +16,10 @@ :label="item.label" /> +
+ {{ $t('setting.importHelper') }} + {{ backupPath }} +
@@ -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, }); + + diff --git a/frontend/src/views/setting/snapshot/index.vue b/frontend/src/views/setting/snapshot/index.vue index d7bd08b75..5bcb88613 100644 --- a/frontend/src/views/setting/snapshot/index.vue +++ b/frontend/src/views/setting/snapshot/index.vue @@ -405,10 +405,17 @@ const search = async () => { page: paginationConfig.currentPage, pageSize: paginationConfig.pageSize, }; - const res = await searchSnapshotPage(params); - cleanData.value = false; - data.value = res.data.items || []; - paginationConfig.total = res.data.total; + 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(() => { diff --git a/frontend/src/views/toolbox/clam/index.vue b/frontend/src/views/toolbox/clam/index.vue index 1fe2ce26b..58076034c 100644 --- a/frontend/src/views/toolbox/clam/index.vue +++ b/frontend/src/views/toolbox/clam/index.vue @@ -56,6 +56,7 @@ :label="$t('commons.table.name')" :min-width="60" prop="name" + sortable show-overflow-tooltip > + + + + + + ([]); +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 = { 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'), diff --git a/frontend/src/views/toolbox/clam/operate/index.vue b/frontend/src/views/toolbox/clam/operate/index.vue index 82687670f..67a613077 100644 --- a/frontend/src/views/toolbox/clam/operate/index.vue +++ b/frontend/src/views/toolbox/clam/operate/index.vue @@ -50,6 +50,77 @@ + + + + + {{ $t('toolbox.clam.cronHelper') }} + + {{ $t('license.levelUpPro') }} + + + + + + + + + + + + + + + + + + + + + + @@ -64,6 +135,7 @@ + @@ -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({ 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; @@ -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, }); + +