mirror of https://github.com/1Panel-dev/1Panel
ssongliu
5 months ago
committed by
GitHub
28 changed files with 3455 additions and 2 deletions
@ -0,0 +1,251 @@
|
||||
package v1 |
||||
|
||||
import ( |
||||
"github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" |
||||
"github.com/1Panel-dev/1Panel/backend/app/dto" |
||||
"github.com/1Panel-dev/1Panel/backend/constant" |
||||
"github.com/gin-gonic/gin" |
||||
) |
||||
|
||||
// @Tags Clam
|
||||
// @Summary Create clam
|
||||
// @Description 创建扫描规则
|
||||
// @Accept json
|
||||
// @Param request body dto.ClamCreate true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /toolbox/clam [post]
|
||||
// @x-panel-log {"bodyKeys":["name","path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建扫描规则 [name][path]","formatEN":"create clam [name][path]"}
|
||||
func (b *BaseApi) CreateClam(c *gin.Context) { |
||||
var req dto.ClamCreate |
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil { |
||||
return |
||||
} |
||||
|
||||
if err := clamService.Create(req); err != nil { |
||||
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) |
||||
return |
||||
} |
||||
helper.SuccessWithData(c, nil) |
||||
} |
||||
|
||||
// @Tags Clam
|
||||
// @Summary Update clam
|
||||
// @Description 修改扫描规则
|
||||
// @Accept json
|
||||
// @Param request body dto.ClamUpdate true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /toolbox/clam/update [post]
|
||||
// @x-panel-log {"bodyKeys":["name","path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"修改扫描规则 [name][path]","formatEN":"update clam [name][path]"}
|
||||
func (b *BaseApi) UpdateClam(c *gin.Context) { |
||||
var req dto.ClamUpdate |
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil { |
||||
return |
||||
} |
||||
|
||||
if err := clamService.Update(req); 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"
|
||||
// @Success 200 {object} dto.PageResult
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /toolbox/clam/search [post]
|
||||
func (b *BaseApi) SearchClam(c *gin.Context) { |
||||
var req dto.SearchWithPage |
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil { |
||||
return |
||||
} |
||||
|
||||
total, list, err := clamService.SearchWithPage(req) |
||||
if err != nil { |
||||
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) |
||||
return |
||||
} |
||||
|
||||
helper.SuccessWithData(c, dto.PageResult{ |
||||
Items: list, |
||||
Total: total, |
||||
}) |
||||
} |
||||
|
||||
// @Tags Clam
|
||||
// @Summary Load clam base info
|
||||
// @Description 获取 Clam 基础信息
|
||||
// @Accept json
|
||||
// @Success 200 {object} dto.ClamBaseInfo
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /toolbox/clam/base [get]
|
||||
func (b *BaseApi) LoadClamBaseInfo(c *gin.Context) { |
||||
info, err := clamService.LoadBaseInfo() |
||||
if err != nil { |
||||
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) |
||||
return |
||||
} |
||||
|
||||
helper.SuccessWithData(c, info) |
||||
} |
||||
|
||||
// @Tags Clam
|
||||
// @Summary Operate Clam
|
||||
// @Description 修改 Clam 状态
|
||||
// @Accept json
|
||||
// @Param request body dto.Operate true "request"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /toolbox/clam/operate [post]
|
||||
// @x-panel-log {"bodyKeys":["operation"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operation] Clam","formatEN":"[operation] FTP"}
|
||||
func (b *BaseApi) OperateClam(c *gin.Context) { |
||||
var req dto.Operate |
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil { |
||||
return |
||||
} |
||||
|
||||
if err := clamService.Operate(req.Operation); err != nil { |
||||
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) |
||||
return |
||||
} |
||||
|
||||
helper.SuccessWithData(c, nil) |
||||
} |
||||
|
||||
// @Tags Clam
|
||||
// @Summary Clean clam record
|
||||
// @Description 清空扫描报告
|
||||
// @Accept json
|
||||
// @Param request body dto.OperateByID true "request"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /toolbox/clam/record/clean [post]
|
||||
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":true,"db":"clams","output_column":"name","output_value":"name"}],"formatZH":"清空扫描报告 [name]","formatEN":"clean clam record [name]"}
|
||||
func (b *BaseApi) CleanClamRecord(c *gin.Context) { |
||||
var req dto.OperateByID |
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil { |
||||
return |
||||
} |
||||
if err := clamService.CleanRecord(req); err != nil { |
||||
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) |
||||
return |
||||
} |
||||
|
||||
helper.SuccessWithData(c, nil) |
||||
} |
||||
|
||||
// @Tags Clam
|
||||
// @Summary Page clam record
|
||||
// @Description 获取扫描结果列表分页
|
||||
// @Accept json
|
||||
// @Param request body dto.ClamLogSearch true "request"
|
||||
// @Success 200 {object} dto.PageResult
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /toolbox/clam/record/search [post]
|
||||
func (b *BaseApi) SearchClamRecord(c *gin.Context) { |
||||
var req dto.ClamLogSearch |
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil { |
||||
return |
||||
} |
||||
|
||||
total, list, err := clamService.LoadRecords(req) |
||||
if err != nil { |
||||
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) |
||||
return |
||||
} |
||||
|
||||
helper.SuccessWithData(c, dto.PageResult{ |
||||
Items: list, |
||||
Total: total, |
||||
}) |
||||
} |
||||
|
||||
// @Tags Clam
|
||||
// @Summary Load clam file
|
||||
// @Description 获取扫描文件
|
||||
// @Accept json
|
||||
// @Param request body dto.OperationWithName true "request"
|
||||
// @Success 200 {object} dto.PageResult
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /toolbox/clam/file/search [post]
|
||||
func (b *BaseApi) SearchClamFile(c *gin.Context) { |
||||
var req dto.OperationWithName |
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil { |
||||
return |
||||
} |
||||
|
||||
content, err := clamService.LoadFile(req) |
||||
if err != nil { |
||||
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) |
||||
return |
||||
} |
||||
|
||||
helper.SuccessWithData(c, content) |
||||
} |
||||
|
||||
// @Tags Clam
|
||||
// @Summary Update clam file
|
||||
// @Description 更新病毒扫描配置文件
|
||||
// @Accept json
|
||||
// @Param request body dto.UpdateByNameAndFile true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /toolbox/clam/file/update [post]
|
||||
func (b *BaseApi) UpdateFile(c *gin.Context) { |
||||
var req dto.UpdateByNameAndFile |
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil { |
||||
return |
||||
} |
||||
if err := clamService.UpdateFile(req); err != nil { |
||||
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) |
||||
return |
||||
} |
||||
helper.SuccessWithOutData(c) |
||||
} |
||||
|
||||
// @Tags Clam
|
||||
// @Summary Delete clam
|
||||
// @Description 删除扫描规则
|
||||
// @Accept json
|
||||
// @Param request body dto.BatchDeleteReq true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /toolbox/clam/del [post]
|
||||
// @x-panel-log {"bodyKeys":["ids"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"ids","isList":true,"db":"clams","output_column":"name","output_value":"names"}],"formatZH":"删除扫描规则 [names]","formatEN":"delete clam [names]"}
|
||||
func (b *BaseApi) DeleteClam(c *gin.Context) { |
||||
var req dto.BatchDeleteReq |
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil { |
||||
return |
||||
} |
||||
|
||||
if err := clamService.Delete(req.Ids); err != nil { |
||||
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) |
||||
return |
||||
} |
||||
helper.SuccessWithData(c, nil) |
||||
} |
||||
|
||||
// @Tags Clam
|
||||
// @Summary Handle clam scan
|
||||
// @Description 执行病毒扫描
|
||||
// @Accept json
|
||||
// @Param request body dto.OperateByID true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /toolbox/clam/handle [post]
|
||||
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":true,"db":"clams","output_column":"name","output_value":"name"}],"formatZH":"执行病毒扫描 [name]","formatEN":"handle clam scan [name]"}
|
||||
func (b *BaseApi) HandleClamScan(c *gin.Context) { |
||||
var req dto.OperateByID |
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil { |
||||
return |
||||
} |
||||
|
||||
if err := clamService.HandleOnce(req); err != nil { |
||||
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) |
||||
return |
||||
} |
||||
helper.SuccessWithData(c, nil) |
||||
} |
@ -0,0 +1,52 @@
|
||||
package dto |
||||
|
||||
import ( |
||||
"time" |
||||
) |
||||
|
||||
type ClamBaseInfo struct { |
||||
Version string `json:"version"` |
||||
IsActive bool `json:"isActive"` |
||||
IsExist bool `json:"isExist"` |
||||
} |
||||
|
||||
type ClamInfo struct { |
||||
ID uint `json:"id"` |
||||
CreatedAt time.Time `json:"createdAt"` |
||||
|
||||
Name string `json:"name"` |
||||
Path string `json:"path"` |
||||
LastHandleDate string `json:"lastHandleDate"` |
||||
Description string `json:"description"` |
||||
} |
||||
|
||||
type ClamLogSearch struct { |
||||
PageInfo |
||||
|
||||
ClamID uint `json:"clamID"` |
||||
StartTime time.Time `json:"startTime"` |
||||
EndTime time.Time `json:"endTime"` |
||||
} |
||||
|
||||
type ClamLog struct { |
||||
Name string `json:"name"` |
||||
ScanDate string `json:"scanDate"` |
||||
ScanTime string `json:"scanTime"` |
||||
InfectedFiles string `json:"infectedFiles"` |
||||
Log string `json:"log"` |
||||
Status string `json:"status"` |
||||
} |
||||
|
||||
type ClamCreate struct { |
||||
Name string `json:"name"` |
||||
Path string `json:"path"` |
||||
Description string `json:"description"` |
||||
} |
||||
|
||||
type ClamUpdate struct { |
||||
ID uint `json:"id"` |
||||
|
||||
Name string `json:"name"` |
||||
Path string `json:"path"` |
||||
Description string `json:"description"` |
||||
} |
@ -0,0 +1,9 @@
|
||||
package model |
||||
|
||||
type Clam struct { |
||||
BaseModel |
||||
|
||||
Name string `gorm:"type:varchar(64);not null" json:"name"` |
||||
Path string `gorm:"type:varchar(64);not null" json:"path"` |
||||
Description string `gorm:"type:varchar(64);not null" json:"description"` |
||||
} |
@ -0,0 +1,58 @@
|
||||
package repo |
||||
|
||||
import ( |
||||
"github.com/1Panel-dev/1Panel/backend/app/model" |
||||
"github.com/1Panel-dev/1Panel/backend/global" |
||||
) |
||||
|
||||
type ClamRepo struct{} |
||||
|
||||
type IClamRepo interface { |
||||
Page(limit, offset int, opts ...DBOption) (int64, []model.Clam, error) |
||||
Create(clam *model.Clam) error |
||||
Update(id uint, vars map[string]interface{}) error |
||||
Delete(opts ...DBOption) error |
||||
Get(opts ...DBOption) (model.Clam, error) |
||||
} |
||||
|
||||
func NewIClamRepo() IClamRepo { |
||||
return &ClamRepo{} |
||||
} |
||||
|
||||
func (u *ClamRepo) Get(opts ...DBOption) (model.Clam, error) { |
||||
var clam model.Clam |
||||
db := global.DB |
||||
for _, opt := range opts { |
||||
db = opt(db) |
||||
} |
||||
err := db.First(&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{}) |
||||
for _, opt := range opts { |
||||
db = opt(db) |
||||
} |
||||
count := int64(0) |
||||
db = db.Count(&count) |
||||
err := db.Limit(size).Offset(size * (page - 1)).Find(&users).Error |
||||
return count, users, err |
||||
} |
||||
|
||||
func (u *ClamRepo) Create(clam *model.Clam) error { |
||||
return global.DB.Create(clam).Error |
||||
} |
||||
|
||||
func (u *ClamRepo) Update(id uint, vars map[string]interface{}) error { |
||||
return global.DB.Model(&model.Clam{}).Where("id = ?", id).Updates(vars).Error |
||||
} |
||||
|
||||
func (u *ClamRepo) Delete(opts ...DBOption) error { |
||||
db := global.DB |
||||
for _, opt := range opts { |
||||
db = opt(db) |
||||
} |
||||
return db.Delete(&model.Clam{}).Error |
||||
} |
@ -0,0 +1,366 @@
|
||||
package service |
||||
|
||||
import ( |
||||
"bufio" |
||||
"fmt" |
||||
"os" |
||||
"os/exec" |
||||
"path" |
||||
"path/filepath" |
||||
"sort" |
||||
"strings" |
||||
"time" |
||||
|
||||
"github.com/1Panel-dev/1Panel/backend/app/dto" |
||||
"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/jinzhu/copier" |
||||
|
||||
"github.com/pkg/errors" |
||||
) |
||||
|
||||
const ( |
||||
clamServiceNameCentOs = "clamd@scan.service" |
||||
clamServiceNameUbuntu = "clamav-daemon.service" |
||||
scanDir = "scan-result" |
||||
) |
||||
|
||||
type ClamService struct { |
||||
serviceName string |
||||
} |
||||
|
||||
type IClamService interface { |
||||
LoadBaseInfo() (dto.ClamBaseInfo, error) |
||||
Operate(operate string) error |
||||
SearchWithPage(search dto.SearchWithPage) (int64, interface{}, error) |
||||
Create(req dto.ClamCreate) error |
||||
Update(req dto.ClamUpdate) error |
||||
Delete(ids []uint) error |
||||
HandleOnce(req dto.OperateByID) error |
||||
LoadFile(req dto.OperationWithName) (string, error) |
||||
UpdateFile(req dto.UpdateByNameAndFile) error |
||||
LoadRecords(req dto.ClamLogSearch) (int64, interface{}, error) |
||||
CleanRecord(req dto.OperateByID) error |
||||
} |
||||
|
||||
func NewIClamService() IClamService { |
||||
return &ClamService{} |
||||
} |
||||
|
||||
func (f *ClamService) LoadBaseInfo() (dto.ClamBaseInfo, error) { |
||||
var baseInfo dto.ClamBaseInfo |
||||
baseInfo.Version = "-" |
||||
exist1, _ := systemctl.IsExist(clamServiceNameCentOs) |
||||
if exist1 { |
||||
f.serviceName = clamServiceNameCentOs |
||||
baseInfo.IsExist = true |
||||
baseInfo.IsActive, _ = systemctl.IsActive(clamServiceNameCentOs) |
||||
} |
||||
exist2, _ := systemctl.IsExist(clamServiceNameCentOs) |
||||
if exist2 { |
||||
f.serviceName = clamServiceNameCentOs |
||||
baseInfo.IsExist = true |
||||
baseInfo.IsActive, _ = systemctl.IsActive(clamServiceNameCentOs) |
||||
} |
||||
|
||||
if baseInfo.IsActive { |
||||
version, err := cmd.Exec("clamdscan --version") |
||||
if err != nil { |
||||
return baseInfo, nil |
||||
} |
||||
if strings.Contains(version, "/") { |
||||
baseInfo.Version = strings.TrimPrefix(strings.Split(version, "/")[0], "ClamAV ") |
||||
} else { |
||||
baseInfo.Version = strings.TrimPrefix(version, "ClamAV ") |
||||
} |
||||
} |
||||
return baseInfo, nil |
||||
} |
||||
|
||||
func (f *ClamService) Operate(operate string) error { |
||||
switch operate { |
||||
case "start", "restart", "stop": |
||||
stdout, err := cmd.Execf("systemctl %s %s", operate, f.serviceName) |
||||
if err != nil { |
||||
return fmt.Errorf("%s the %s failed, err: %s", operate, f.serviceName, stdout) |
||||
} |
||||
return nil |
||||
default: |
||||
return fmt.Errorf("not support such operation: %v", operate) |
||||
} |
||||
} |
||||
|
||||
func (f *ClamService) SearchWithPage(req dto.SearchWithPage) (int64, interface{}, error) { |
||||
total, commands, err := clamRepo.Page(req.Page, req.PageSize, commonRepo.WithLikeName(req.Info)) |
||||
if err != nil { |
||||
return 0, nil, err |
||||
} |
||||
var datas []dto.ClamInfo |
||||
for _, command := range commands { |
||||
var item dto.ClamInfo |
||||
if err := copier.Copy(&item, &command); err != nil { |
||||
return 0, nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) |
||||
} |
||||
item.LastHandleDate = "-" |
||||
datas = append(datas, item) |
||||
} |
||||
nyc, _ := time.LoadLocation(common.LoadTimeZone()) |
||||
for i := 0; i < len(datas); i++ { |
||||
logPaths := loadFileByName(datas[i].Name) |
||||
sort.Slice(logPaths, func(i, j int) bool { |
||||
return logPaths[i] > logPaths[j] |
||||
}) |
||||
if len(logPaths) != 0 { |
||||
t1, err := time.ParseInLocation("20060102150405", logPaths[0], nyc) |
||||
if err != nil { |
||||
continue |
||||
} |
||||
datas[i].LastHandleDate = t1.Format("2006-01-02 15:04:05") |
||||
} |
||||
} |
||||
return total, datas, err |
||||
} |
||||
|
||||
func (f *ClamService) Create(req dto.ClamCreate) error { |
||||
clam, _ := clamRepo.Get(commonRepo.WithByName(req.Name)) |
||||
if clam.ID != 0 { |
||||
return constant.ErrRecordExist |
||||
} |
||||
if err := copier.Copy(&clam, &req); err != nil { |
||||
return errors.WithMessage(constant.ErrStructTransform, err.Error()) |
||||
} |
||||
if err := clamRepo.Create(&clam); err != nil { |
||||
return err |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func (f *ClamService) Update(req dto.ClamUpdate) error { |
||||
clam, _ := clamRepo.Get(commonRepo.WithByName(req.Name)) |
||||
if clam.ID == 0 { |
||||
return constant.ErrRecordNotFound |
||||
} |
||||
upMap := map[string]interface{}{} |
||||
upMap["name"] = req.Name |
||||
upMap["path"] = req.Path |
||||
upMap["description"] = req.Description |
||||
if err := clamRepo.Update(req.ID, upMap); err != nil { |
||||
return err |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func (u *ClamService) Delete(ids []uint) error { |
||||
if len(ids) == 1 { |
||||
clam, _ := clamRepo.Get(commonRepo.WithByID(ids[0])) |
||||
if clam.ID == 0 { |
||||
return constant.ErrRecordNotFound |
||||
} |
||||
return clamRepo.Delete(commonRepo.WithByID(ids[0])) |
||||
} |
||||
return clamRepo.Delete(commonRepo.WithIdsIn(ids)) |
||||
} |
||||
|
||||
func (u *ClamService) HandleOnce(req dto.OperateByID) error { |
||||
clam, _ := clamRepo.Get(commonRepo.WithByID(req.ID)) |
||||
if clam.ID == 0 { |
||||
return constant.ErrRecordNotFound |
||||
} |
||||
if cmd.CheckIllegal(clam.Path) { |
||||
return buserr.New(constant.ErrCmdIllegal) |
||||
} |
||||
logFile := path.Join(global.CONF.System.DataDir, scanDir, clam.Name, time.Now().Format("20060102150405")) |
||||
if _, err := os.Stat(path.Dir(logFile)); err != nil { |
||||
_ = os.MkdirAll(path.Dir(logFile), os.ModePerm) |
||||
} |
||||
go func() { |
||||
cmd := exec.Command("clamdscan", "--fdpass", clam.Path, "-l", logFile) |
||||
_, _ = cmd.CombinedOutput() |
||||
}() |
||||
return nil |
||||
} |
||||
|
||||
func (u *ClamService) LoadRecords(req dto.ClamLogSearch) (int64, interface{}, error) { |
||||
clam, _ := clamRepo.Get(commonRepo.WithByID(req.ClamID)) |
||||
if clam.ID == 0 { |
||||
return 0, nil, constant.ErrRecordNotFound |
||||
} |
||||
logPaths := loadFileByName(clam.Name) |
||||
if len(logPaths) == 0 { |
||||
return 0, nil, nil |
||||
} |
||||
|
||||
var filterFiles []string |
||||
nyc, _ := time.LoadLocation(common.LoadTimeZone()) |
||||
for _, item := range logPaths { |
||||
t1, err := time.ParseInLocation("20060102150405", item, nyc) |
||||
if err != nil { |
||||
continue |
||||
} |
||||
if t1.After(req.StartTime) && t1.Before(req.EndTime) { |
||||
filterFiles = append(filterFiles, item) |
||||
} |
||||
} |
||||
if len(filterFiles) == 0 { |
||||
return 0, nil, nil |
||||
} |
||||
|
||||
sort.Slice(filterFiles, func(i, j int) bool { |
||||
return filterFiles[i] > filterFiles[j] |
||||
}) |
||||
|
||||
var records []string |
||||
total, start, end := len(filterFiles), (req.Page-1)*req.PageSize, req.Page*req.PageSize |
||||
if start > total { |
||||
records = make([]string, 0) |
||||
} else { |
||||
if end >= total { |
||||
end = total |
||||
} |
||||
records = filterFiles[start:end] |
||||
} |
||||
|
||||
var datas []dto.ClamLog |
||||
for i := 0; i < len(records); i++ { |
||||
item := loadResultFromLog(path.Join(global.CONF.System.DataDir, scanDir, clam.Name, records[i])) |
||||
datas = append(datas, item) |
||||
} |
||||
return int64(total), datas, nil |
||||
} |
||||
|
||||
func (u *ClamService) CleanRecord(req dto.OperateByID) error { |
||||
clam, _ := clamRepo.Get(commonRepo.WithByID(req.ID)) |
||||
if clam.ID == 0 { |
||||
return constant.ErrRecordNotFound |
||||
} |
||||
pathItem := path.Join(global.CONF.System.DataDir, scanDir, clam.Name) |
||||
_ = os.RemoveAll(pathItem) |
||||
return nil |
||||
} |
||||
|
||||
func (u *ClamService) LoadFile(req dto.OperationWithName) (string, error) { |
||||
filePath := "" |
||||
switch req.Name { |
||||
case "clamd": |
||||
if u.serviceName == clamServiceNameCentOs { |
||||
filePath = "/etc/clamav/clamd.conf" |
||||
} else { |
||||
filePath = "/etc/clamd.d/scan.conf" |
||||
} |
||||
case "clamd-log": |
||||
if u.serviceName == clamServiceNameCentOs { |
||||
filePath = "/var/log/clamav/clamav.log" |
||||
} else { |
||||
filePath = "/var/log/clamd.scan" |
||||
} |
||||
case "freshclam": |
||||
if u.serviceName == clamServiceNameCentOs { |
||||
filePath = "/etc/clamav/freshclam.conf" |
||||
} else { |
||||
filePath = "/etc/freshclam.conf" |
||||
} |
||||
case "freshclam-log": |
||||
if u.serviceName == clamServiceNameCentOs { |
||||
filePath = "/var/log/clamav/freshclam.log" |
||||
} else { |
||||
filePath = "/var/log/clamav/freshclam.log" |
||||
} |
||||
default: |
||||
return "", fmt.Errorf("not support such type") |
||||
} |
||||
if _, err := os.Stat(filePath); err != nil { |
||||
return "", buserr.New("ErrHttpReqNotFound") |
||||
} |
||||
content, err := os.ReadFile(filePath) |
||||
if err != nil { |
||||
return "", err |
||||
} |
||||
return string(content), nil |
||||
} |
||||
|
||||
func (u *ClamService) UpdateFile(req dto.UpdateByNameAndFile) error { |
||||
filePath := "" |
||||
service := "" |
||||
switch req.Name { |
||||
case "clamd": |
||||
if u.serviceName == clamServiceNameCentOs { |
||||
service = clamServiceNameCentOs |
||||
filePath = "/etc/clamav/clamd.conf" |
||||
} else { |
||||
service = clamServiceNameCentOs |
||||
filePath = "/etc/clamd.d/scan.conf" |
||||
} |
||||
case "freshclam": |
||||
if u.serviceName == clamServiceNameCentOs { |
||||
filePath = "/etc/clamav/freshclam.conf" |
||||
} else { |
||||
filePath = "/etc/freshclam.conf" |
||||
} |
||||
service = "clamav-freshclam.service" |
||||
default: |
||||
return fmt.Errorf("not support such type") |
||||
} |
||||
file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_TRUNC, 0640) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
defer file.Close() |
||||
write := bufio.NewWriter(file) |
||||
_, _ = write.WriteString(req.File) |
||||
write.Flush() |
||||
|
||||
_ = systemctl.Restart(service) |
||||
return nil |
||||
} |
||||
|
||||
func loadFileByName(name string) []string { |
||||
var logPaths []string |
||||
pathItem := path.Join(global.CONF.System.DataDir, scanDir, name) |
||||
_ = filepath.Walk(pathItem, func(path string, info os.FileInfo, err error) error { |
||||
if err != nil { |
||||
return nil |
||||
} |
||||
if info.IsDir() || info.Name() == name { |
||||
return nil |
||||
} |
||||
logPaths = append(logPaths, info.Name()) |
||||
return nil |
||||
}) |
||||
return logPaths |
||||
} |
||||
func loadResultFromLog(pathItem string) dto.ClamLog { |
||||
var data dto.ClamLog |
||||
data.Name = path.Base(pathItem) |
||||
data.Status = constant.StatusWaiting |
||||
file, err := os.ReadFile(pathItem) |
||||
if err != nil { |
||||
return data |
||||
} |
||||
data.Log = string(file) |
||||
lines := strings.Split(string(file), "\n") |
||||
for _, line := range lines { |
||||
if strings.Contains(line, "- SCAN SUMMARY -") { |
||||
data.Status = constant.StatusDone |
||||
} |
||||
if data.Status != constant.StatusDone { |
||||
continue |
||||
} |
||||
switch { |
||||
case strings.HasPrefix(line, "Infected files:"): |
||||
data.InfectedFiles = strings.TrimPrefix(line, "Infected files:") |
||||
case strings.HasPrefix(line, "Time:"): |
||||
if strings.Contains(line, "(") { |
||||
data.ScanTime = strings.ReplaceAll(strings.Split(line, "(")[1], ")", "") |
||||
continue |
||||
} |
||||
data.ScanTime = strings.TrimPrefix(line, "Time:") |
||||
case strings.HasPrefix(line, "Start Date:"): |
||||
data.ScanDate = strings.TrimPrefix(line, "Start Date:") |
||||
} |
||||
} |
||||
return data |
||||
} |
@ -0,0 +1,246 @@
|
||||
<template> |
||||
<div> |
||||
<LayoutContent v-loading="loading" v-if="!isRecordShow && !isSettingShow" :title="$t('toolbox.clam.clam')"> |
||||
<template #app> |
||||
<ClamStatus |
||||
@setting="setting" |
||||
v-model:loading="loading" |
||||
@get-status="getStatus" |
||||
v-model:mask-show="maskShow" |
||||
/> |
||||
</template> |
||||
<template #toolbar> |
||||
<el-row> |
||||
<el-col :xs="24" :sm="16" :md="16" :lg="16" :xl="16"> |
||||
<el-button type="primary" :disabled="!form.isActive" @click="onOpenDialog('add')"> |
||||
{{ $t('toolbox.clam.clamCreate') }} |
||||
</el-button> |
||||
<el-button plain :disabled="selects.length === 0 || !form.isActive" @click="onDelete(null)"> |
||||
{{ $t('commons.button.delete') }} |
||||
</el-button> |
||||
</el-col> |
||||
<el-col :xs="24" :sm="8" :md="8" :lg="8" :xl="8"> |
||||
<TableSearch @search="search()" v-model:searchName="searchName" /> |
||||
</el-col> |
||||
</el-row> |
||||
</template> |
||||
<template #main> |
||||
<ComplexTable |
||||
v-if="!isSettingShow" |
||||
:pagination-config="paginationConfig" |
||||
v-model:selects="selects" |
||||
@sort-change="search" |
||||
@search="search" |
||||
:data="data" |
||||
> |
||||
<el-table-column type="selection" fix /> |
||||
<el-table-column |
||||
:label="$t('commons.table.name')" |
||||
:min-width="60" |
||||
prop="name" |
||||
show-overflow-tooltip |
||||
/> |
||||
<el-table-column :label="$t('file.path')" :min-width="120" prop="path" show-overflow-tooltip> |
||||
<template #default="{ row }"> |
||||
<el-button text type="primary" @click="toFolder(row.path)">{{ row.path }}</el-button> |
||||
</template> |
||||
</el-table-column> |
||||
<el-table-column |
||||
:label="$t('cronjob.lastRecordTime')" |
||||
:min-width="100" |
||||
prop="lastHandleDate" |
||||
show-overflow-tooltip |
||||
/> |
||||
<el-table-column :label="$t('commons.table.description')" prop="description" show-overflow-tooltip> |
||||
<template #default="{ row }"> |
||||
<fu-input-rw-switch v-model="row.description" @blur="onChange(row)" /> |
||||
</template> |
||||
</el-table-column> |
||||
<fu-table-operations |
||||
width="200px" |
||||
:buttons="buttons" |
||||
:ellipsis="10" |
||||
:label="$t('commons.table.operate')" |
||||
fix |
||||
/> |
||||
</ComplexTable> |
||||
</template> |
||||
</LayoutContent> |
||||
|
||||
<OpDialog ref="opRef" @search="search" @submit="onSubmitDelete()" /> |
||||
<OperateDialog @search="search" ref="dialogRef" /> |
||||
<LogDialog ref="dialogLogRef" /> |
||||
<SettingDialog v-if="isSettingShow" /> |
||||
</div> |
||||
</template> |
||||
|
||||
<script lang="ts" setup> |
||||
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 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'; |
||||
|
||||
const loading = ref(); |
||||
const selects = ref<any>([]); |
||||
|
||||
const data = ref(); |
||||
const paginationConfig = reactive({ |
||||
cacheSizeKey: 'clam-page-size', |
||||
currentPage: 1, |
||||
pageSize: Number(localStorage.getItem('ftp-page-size')) || 10, |
||||
total: 0, |
||||
orderBy: 'created_at', |
||||
order: 'null', |
||||
}); |
||||
const searchName = ref(); |
||||
|
||||
const form = reactive({ |
||||
isActive: true, |
||||
isExist: true, |
||||
}); |
||||
|
||||
const opRef = ref(); |
||||
const dialogRef = ref(); |
||||
const operateIDs = ref(); |
||||
const dialogLogRef = ref(); |
||||
const isRecordShow = ref(); |
||||
|
||||
const isSettingShow = ref(); |
||||
const maskShow = ref(true); |
||||
const clamStatus = ref({ |
||||
isExist: false, |
||||
version: false, |
||||
isActive: true, |
||||
}); |
||||
|
||||
const search = async () => { |
||||
loading.value = true; |
||||
let params = { |
||||
info: searchName.value, |
||||
page: paginationConfig.currentPage, |
||||
pageSize: paginationConfig.pageSize, |
||||
}; |
||||
await searchClam(params) |
||||
.then((res) => { |
||||
loading.value = false; |
||||
data.value = res.data.items || []; |
||||
paginationConfig.total = res.data.total; |
||||
}) |
||||
.catch(() => { |
||||
loading.value = false; |
||||
}); |
||||
}; |
||||
|
||||
const setting = () => { |
||||
router.push({ name: 'Clam-Setting' }); |
||||
}; |
||||
const getStatus = (status: any) => { |
||||
clamStatus.value = status; |
||||
search(); |
||||
}; |
||||
|
||||
const toFolder = (folder: string) => { |
||||
router.push({ path: '/hosts/files', query: { path: folder } }); |
||||
}; |
||||
|
||||
const onChange = async (row: any) => { |
||||
await await updateClam(row); |
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess')); |
||||
}; |
||||
|
||||
const onOpenDialog = async (title: string, rowData: Partial<Toolbox.ClamInfo> = {}) => { |
||||
let params = { |
||||
title, |
||||
rowData: { ...rowData }, |
||||
}; |
||||
dialogRef.value!.acceptParams(params); |
||||
}; |
||||
|
||||
const onDelete = async (row: Toolbox.ClamInfo | null) => { |
||||
let names = []; |
||||
let ids = []; |
||||
if (row) { |
||||
ids = [row.id]; |
||||
names = [row.name]; |
||||
} else { |
||||
for (const item of selects.value) { |
||||
names.push(item.name); |
||||
ids.push(item.id); |
||||
} |
||||
} |
||||
operateIDs.value = ids; |
||||
opRef.value.acceptParams({ |
||||
title: i18n.global.t('commons.button.delete'), |
||||
names: names, |
||||
msg: i18n.global.t('commons.msg.operatorHelper', [ |
||||
i18n.global.t('cronjob.cronTask'), |
||||
i18n.global.t('commons.button.delete'), |
||||
]), |
||||
api: null, |
||||
params: null, |
||||
}); |
||||
}; |
||||
|
||||
const onSubmitDelete = async () => { |
||||
loading.value = true; |
||||
await deleteClam({ ids: operateIDs.value }) |
||||
.then(() => { |
||||
loading.value = false; |
||||
MsgSuccess(i18n.global.t('commons.msg.deleteSuccess')); |
||||
search(); |
||||
}) |
||||
.catch(() => { |
||||
loading.value = false; |
||||
}); |
||||
}; |
||||
|
||||
const buttons = [ |
||||
{ |
||||
label: i18n.global.t('commons.button.handle'), |
||||
click: async (row: Toolbox.ClamInfo) => { |
||||
loading.value = true; |
||||
await handleClamScan(row.id) |
||||
.then(() => { |
||||
loading.value = false; |
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess')); |
||||
search(); |
||||
}) |
||||
.catch(() => { |
||||
loading.value = false; |
||||
}); |
||||
}, |
||||
}, |
||||
{ |
||||
label: i18n.global.t('commons.button.edit'), |
||||
click: (row: Toolbox.ClamInfo) => { |
||||
onOpenDialog('edit', row); |
||||
}, |
||||
}, |
||||
{ |
||||
label: i18n.global.t('cronjob.record'), |
||||
click: (row: Toolbox.ClamInfo) => { |
||||
isRecordShow.value = true; |
||||
let params = { |
||||
rowData: { ...row }, |
||||
}; |
||||
dialogLogRef.value!.acceptParams(params); |
||||
}, |
||||
}, |
||||
{ |
||||
label: i18n.global.t('commons.button.delete'), |
||||
click: (row: Toolbox.ClamInfo) => { |
||||
onDelete(row); |
||||
}, |
||||
}, |
||||
]; |
||||
|
||||
onMounted(() => { |
||||
search(); |
||||
}); |
||||
</script> |
@ -0,0 +1,133 @@
|
||||
<template> |
||||
<el-drawer |
||||
v-model="drawerVisible" |
||||
:destroy-on-close="true" |
||||
:close-on-click-modal="false" |
||||
:close-on-press-escape="false" |
||||
size="50%" |
||||
> |
||||
<template #header> |
||||
<DrawerHeader |
||||
:header="title" |
||||
:hideResource="dialogData.title === 'add'" |
||||
:resource="dialogData.rowData?.name" |
||||
:back="handleClose" |
||||
/> |
||||
</template> |
||||
<el-form ref="formRef" label-position="top" :model="dialogData.rowData" :rules="rules" v-loading="loading"> |
||||
<el-row type="flex" justify="center"> |
||||
<el-col :span="22"> |
||||
<el-form-item :label="$t('commons.table.name')" prop="name"> |
||||
<el-input |
||||
:disabled="dialogData.title === 'edit'" |
||||
clearable |
||||
v-model.trim="dialogData.rowData!.name" |
||||
/> |
||||
</el-form-item> |
||||
<el-form-item :label="$t('file.root')" prop="path"> |
||||
<el-input v-model="dialogData.rowData!.path"> |
||||
<template #prepend> |
||||
<FileList @choose="loadDir" :dir="true"></FileList> |
||||
</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> |
||||
</el-col> |
||||
</el-row> |
||||
</el-form> |
||||
<template #footer> |
||||
<span class="dialog-footer"> |
||||
<el-button @click="drawerVisible = false">{{ $t('commons.button.cancel') }}</el-button> |
||||
<el-button :disabled="loading" type="primary" @click="onSubmit(formRef)"> |
||||
{{ $t('commons.button.confirm') }} |
||||
</el-button> |
||||
</span> |
||||
</template> |
||||
</el-drawer> |
||||
</template> |
||||
|
||||
<script lang="ts" setup> |
||||
import { reactive, ref } from 'vue'; |
||||
import { Rules } from '@/global/form-rules'; |
||||
import FileList from '@/components/file-list/index.vue'; |
||||
import i18n from '@/lang'; |
||||
import { ElForm } from 'element-plus'; |
||||
import DrawerHeader from '@/components/drawer-header/index.vue'; |
||||
import { MsgSuccess } from '@/utils/message'; |
||||
import { Toolbox } from '@/api/interface/toolbox'; |
||||
import { createClam, updateClam } from '@/api/modules/toolbox'; |
||||
|
||||
interface DialogProps { |
||||
title: string; |
||||
rowData?: Toolbox.ClamInfo; |
||||
getTableList?: () => Promise<any>; |
||||
} |
||||
const loading = ref(); |
||||
const title = ref<string>(''); |
||||
const drawerVisible = ref(false); |
||||
const dialogData = ref<DialogProps>({ |
||||
title: '', |
||||
}); |
||||
|
||||
const acceptParams = (params: DialogProps): void => { |
||||
dialogData.value = params; |
||||
title.value = i18n.global.t('commons.button.' + dialogData.value.title); |
||||
drawerVisible.value = true; |
||||
}; |
||||
const emit = defineEmits<{ (e: 'search'): void }>(); |
||||
|
||||
const handleClose = () => { |
||||
drawerVisible.value = false; |
||||
}; |
||||
|
||||
const rules = reactive({ |
||||
name: [Rules.simpleName], |
||||
path: [Rules.requiredInput, Rules.noSpace], |
||||
}); |
||||
|
||||
type FormInstance = InstanceType<typeof ElForm>; |
||||
const formRef = ref<FormInstance>(); |
||||
|
||||
const loadDir = async (path: string) => { |
||||
dialogData.value.rowData!.path = path; |
||||
}; |
||||
|
||||
const onSubmit = async (formEl: FormInstance | undefined) => { |
||||
if (!formEl) return; |
||||
formEl.validate(async (valid) => { |
||||
if (!valid) return; |
||||
loading.value = true; |
||||
if (dialogData.value.title === 'edit') { |
||||
await updateClam(dialogData.value.rowData) |
||||
.then(() => { |
||||
loading.value = false; |
||||
drawerVisible.value = false; |
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess')); |
||||
emit('search'); |
||||
}) |
||||
.catch(() => { |
||||
loading.value = false; |
||||
}); |
||||
|
||||
return; |
||||
} |
||||
|
||||
await createClam(dialogData.value.rowData) |
||||
.then(() => { |
||||
loading.value = false; |
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess')); |
||||
emit('search'); |
||||
drawerVisible.value = false; |
||||
}) |
||||
.catch(() => { |
||||
loading.value = false; |
||||
}); |
||||
}); |
||||
}; |
||||
|
||||
defineExpose({ |
||||
acceptParams, |
||||
}); |
||||
</script> |
@ -0,0 +1,345 @@
|
||||
<template> |
||||
<div v-if="recordShow" v-loading="loading"> |
||||
<div class="app-status p-mt-20"> |
||||
<el-card> |
||||
<div> |
||||
<el-tag class="float-left" effect="dark" type="success"> |
||||
{{ dialogData.rowData.name }} |
||||
</el-tag> |
||||
<el-popover |
||||
v-if="dialogData.rowData.path.length >= 35" |
||||
placement="top-start" |
||||
trigger="hover" |
||||
width="250" |
||||
:content="dialogData.rowData.path" |
||||
> |
||||
<template #reference> |
||||
<el-tag style="float: left" effect="dark" type="success"> |
||||
{{ dialogData.rowData.path.substring(0, 20) }}... |
||||
</el-tag> |
||||
</template> |
||||
</el-popover> |
||||
<el-tag |
||||
v-if="dialogData.rowData.path.length < 35" |
||||
class="float-left ml-5" |
||||
effect="dark" |
||||
type="success" |
||||
> |
||||
{{ dialogData.rowData.path }} |
||||
</el-tag> |
||||
|
||||
<span class="buttons"> |
||||
<el-button type="primary" @click="onHandle(dialogData.rowData)" link> |
||||
{{ $t('commons.button.handle') }} |
||||
</el-button> |
||||
<el-divider direction="vertical" /> |
||||
<el-button :disabled="!hasRecords" type="primary" @click="onClean" link> |
||||
{{ $t('commons.button.clean') }} |
||||
</el-button> |
||||
</span> |
||||
</div> |
||||
</el-card> |
||||
</div> |
||||
|
||||
<LayoutContent :title="$t('cronjob.record')" :reload="true"> |
||||
<template #search> |
||||
<el-row :gutter="20"> |
||||
<el-col :span="8"> |
||||
<el-date-picker |
||||
style="width: calc(100% - 20px)" |
||||
@change="search()" |
||||
v-model="timeRangeLoad" |
||||
type="datetimerange" |
||||
:range-separator="$t('commons.search.timeRange')" |
||||
:start-placeholder="$t('commons.search.timeStart')" |
||||
:end-placeholder="$t('commons.search.timeEnd')" |
||||
:shortcuts="shortcuts" |
||||
></el-date-picker> |
||||
</el-col> |
||||
</el-row> |
||||
</template> |
||||
<template #main> |
||||
<div class="mainClass"> |
||||
<el-row :gutter="20" v-show="hasRecords" class="mainRowClass"> |
||||
<el-col :span="7"> |
||||
<div class="infinite-list" style="overflow: auto"> |
||||
<el-table |
||||
style="cursor: pointer" |
||||
:data="records" |
||||
border |
||||
:show-header="false" |
||||
@row-click="clickRow" |
||||
> |
||||
<el-table-column> |
||||
<template #default="{ row }"> |
||||
<span v-if="row.name === currentRecord.name" class="select-sign"></span> |
||||
<el-tag v-if="row.status === 'Done'" type="success"> |
||||
{{ $t('commons.status.done') }} |
||||
</el-tag> |
||||
<el-tag v-if="row.status === 'Waiting'" type="info"> |
||||
{{ $t('commons.status.scanFailed') }} |
||||
</el-tag> |
||||
<span> |
||||
{{ row.name }} |
||||
</span> |
||||
</template> |
||||
</el-table-column> |
||||
</el-table> |
||||
</div> |
||||
<div class="page-item"> |
||||
<el-pagination |
||||
:page-size="searchInfo.pageSize" |
||||
:current-page="searchInfo.page" |
||||
@current-change="handleCurrentChange" |
||||
@size-change="handleSizeChange" |
||||
:pager-count="3" |
||||
:page-sizes="[6, 8, 10, 12, 14]" |
||||
small |
||||
layout="total, sizes, prev, pager, next" |
||||
:total="searchInfo.recordTotal" |
||||
/> |
||||
</div> |
||||
</el-col> |
||||
<el-col :span="17"> |
||||
<el-form label-position="top" :v-key="refresh"> |
||||
<el-row v-if="currentRecord?.status === 'Done'"> |
||||
<el-form-item class="descriptionWide"> |
||||
<template #label> |
||||
<span class="status-label">{{ $t('toolbox.clam.scanTime') }}</span> |
||||
</template> |
||||
<span class="status-count"> |
||||
{{ currentRecord?.scanTime }} |
||||
</span> |
||||
</el-form-item> |
||||
<el-form-item class="descriptionWide"> |
||||
<template #label> |
||||
<span class="status-label">{{ $t('toolbox.clam.infectedFiles') }}</span> |
||||
</template> |
||||
<span class="status-count"> |
||||
{{ currentRecord?.infectedFiles }} |
||||
</span> |
||||
</el-form-item> |
||||
</el-row> |
||||
<el-row v-if="currentRecord?.log"> |
||||
<span>{{ $t('commons.table.records') }}</span> |
||||
<codemirror |
||||
ref="mymirror" |
||||
:autofocus="true" |
||||
:placeholder="$t('cronjob.noLogs')" |
||||
:indent-with-tab="true" |
||||
:tabSize="4" |
||||
style="height: calc(100vh - 488px); width: 100%; margin-top: 5px" |
||||
:lineWrapping="true" |
||||
:matchBrackets="true" |
||||
theme="cobalt" |
||||
:styleActiveLine="true" |
||||
:extensions="extensions" |
||||
@ready="handleReady" |
||||
v-model="currentRecord.log" |
||||
:disabled="true" |
||||
/> |
||||
</el-row> |
||||
</el-form> |
||||
</el-col> |
||||
</el-row> |
||||
</div> |
||||
<div class="app-warn" v-show="!hasRecords"> |
||||
<div> |
||||
<span>{{ $t('cronjob.noRecord') }}</span> |
||||
<div> |
||||
<img src="@/assets/images/no_app.svg" /> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
</LayoutContent> |
||||
</div> |
||||
</template> |
||||
|
||||
<script lang="ts" setup> |
||||
import { onBeforeUnmount, reactive, ref, shallowRef } from 'vue'; |
||||
import i18n from '@/lang'; |
||||
import { ElMessageBox } from 'element-plus'; |
||||
import { Codemirror } from 'vue-codemirror'; |
||||
import { javascript } from '@codemirror/lang-javascript'; |
||||
import { oneDark } from '@codemirror/theme-one-dark'; |
||||
import { MsgSuccess } from '@/utils/message'; |
||||
import { shortcuts } from '@/utils/shortcuts'; |
||||
import { Toolbox } from '@/api/interface/toolbox'; |
||||
import { cleanClamRecord, handleClamScan, searchClamRecord } from '@/api/modules/toolbox'; |
||||
|
||||
const loading = ref(); |
||||
const refresh = ref(false); |
||||
const hasRecords = ref(); |
||||
|
||||
let timer: NodeJS.Timer | null = null; |
||||
|
||||
const mymirror = ref(); |
||||
const extensions = [javascript(), oneDark]; |
||||
const view = shallowRef(); |
||||
const handleReady = (payload) => { |
||||
view.value = payload.view; |
||||
}; |
||||
|
||||
const recordShow = ref(false); |
||||
interface DialogProps { |
||||
rowData: Toolbox.ClamInfo; |
||||
} |
||||
const dialogData = ref(); |
||||
const records = ref<Array<Toolbox.ClamLog>>([]); |
||||
const currentRecord = ref<Toolbox.ClamLog>(); |
||||
|
||||
const acceptParams = async (params: DialogProps): Promise<void> => { |
||||
let itemSize = Number(localStorage.getItem(searchInfo.cacheSizeKey)); |
||||
if (itemSize) { |
||||
searchInfo.pageSize = itemSize; |
||||
} |
||||
|
||||
recordShow.value = true; |
||||
dialogData.value = params; |
||||
search(); |
||||
timer = setInterval(() => { |
||||
search(); |
||||
}, 1000 * 5); |
||||
}; |
||||
|
||||
const handleSizeChange = (val: number) => { |
||||
searchInfo.pageSize = val; |
||||
localStorage.setItem(searchInfo.cacheSizeKey, val + ''); |
||||
search(); |
||||
}; |
||||
const handleCurrentChange = (val: number) => { |
||||
searchInfo.page = val; |
||||
search(); |
||||
}; |
||||
|
||||
const timeRangeLoad = ref<[Date, Date]>([ |
||||
new Date(new Date(new Date().getTime() - 3600 * 1000 * 24 * 7).setHours(0, 0, 0, 0)), |
||||
new Date(new Date().setHours(23, 59, 59, 999)), |
||||
]); |
||||
const searchInfo = reactive({ |
||||
cacheSizeKey: 'clam-record-page-size', |
||||
page: 1, |
||||
pageSize: 8, |
||||
recordTotal: 0, |
||||
cronjobID: 0, |
||||
startTime: new Date(), |
||||
endTime: new Date(), |
||||
}); |
||||
|
||||
const onHandle = async (row: Toolbox.ClamInfo) => { |
||||
loading.value = true; |
||||
await handleClamScan(row.id) |
||||
.then(() => { |
||||
loading.value = false; |
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess')); |
||||
search(); |
||||
}) |
||||
.catch(() => { |
||||
loading.value = false; |
||||
}); |
||||
}; |
||||
|
||||
const search = async () => { |
||||
if (timeRangeLoad.value && timeRangeLoad.value.length === 2) { |
||||
searchInfo.startTime = timeRangeLoad.value[0]; |
||||
searchInfo.endTime = timeRangeLoad.value[1]; |
||||
} else { |
||||
searchInfo.startTime = new Date(new Date().setHours(0, 0, 0, 0)); |
||||
searchInfo.endTime = new Date(); |
||||
} |
||||
let params = { |
||||
page: searchInfo.page, |
||||
pageSize: searchInfo.pageSize, |
||||
clamID: dialogData.value.rowData!.id, |
||||
startTime: searchInfo.startTime, |
||||
endTime: searchInfo.endTime, |
||||
}; |
||||
const res = await searchClamRecord(params); |
||||
records.value = res.data.items; |
||||
searchInfo.recordTotal = res.data.total; |
||||
hasRecords.value = searchInfo.recordTotal !== 0; |
||||
if (!hasRecords.value) { |
||||
return; |
||||
} |
||||
if (!currentRecord.value) { |
||||
currentRecord.value = records.value[0]; |
||||
} |
||||
}; |
||||
|
||||
const clickRow = async (row: Toolbox.ClamLog) => { |
||||
currentRecord.value = row; |
||||
}; |
||||
|
||||
const onClean = async () => { |
||||
ElMessageBox.confirm(i18n.global.t('commons.msg.clean'), i18n.global.t('commons.msg.deleteTitle'), { |
||||
confirmButtonText: i18n.global.t('commons.button.confirm'), |
||||
cancelButtonText: i18n.global.t('commons.button.cancel'), |
||||
type: 'warning', |
||||
}).then(async () => { |
||||
loading.value = true; |
||||
console.log(dialogData.value.id); |
||||
cleanClamRecord(dialogData.value.rowData.id) |
||||
.then(() => { |
||||
loading.value = false; |
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess')); |
||||
search(); |
||||
}) |
||||
.catch(() => { |
||||
loading.value = false; |
||||
}); |
||||
}); |
||||
}; |
||||
|
||||
onBeforeUnmount(() => { |
||||
clearInterval(Number(timer)); |
||||
timer = null; |
||||
}); |
||||
|
||||
defineExpose({ |
||||
acceptParams, |
||||
}); |
||||
</script> |
||||
|
||||
<style lang="scss" scoped> |
||||
.infinite-list { |
||||
height: calc(100vh - 420px); |
||||
.select-sign { |
||||
&::before { |
||||
float: left; |
||||
margin-left: -3px; |
||||
position: relative; |
||||
width: 3px; |
||||
height: 24px; |
||||
content: ''; |
||||
background: $primary-color; |
||||
border-radius: 20px; |
||||
} |
||||
} |
||||
.el-tag { |
||||
margin-left: 20px; |
||||
margin-right: 20px; |
||||
} |
||||
} |
||||
|
||||
.descriptionWide { |
||||
width: 40%; |
||||
} |
||||
.description { |
||||
width: 30%; |
||||
} |
||||
.page-item { |
||||
margin-top: 10px; |
||||
font-size: 12px; |
||||
float: right; |
||||
} |
||||
|
||||
@media only screen and (max-width: 1400px) { |
||||
.mainClass { |
||||
overflow: auto; |
||||
} |
||||
.mainRowClass { |
||||
min-width: 1200px; |
||||
} |
||||
} |
||||
</style> |
@ -0,0 +1,138 @@
|
||||
<template> |
||||
<div v-loading="loading"> |
||||
<LayoutContent> |
||||
<template #app> |
||||
<ClamStatus v-model:loading="loading" /> |
||||
</template> |
||||
<template #title> |
||||
<back-button name="Clam" header="Clamav"> |
||||
<template #buttons> |
||||
<el-button type="primary" :plain="activeName !== 'clamd'" @click="search('clamd')"> |
||||
{{ $t('toolbox.clam.clamConf') }} |
||||
</el-button> |
||||
<el-button type="primary" :plain="activeName !== 'freshclam'" @click="search('freshclam')"> |
||||
{{ $t('toolbox.clam.freshClam') }} |
||||
</el-button> |
||||
<el-button type="primary" :plain="activeName !== 'clamd-log'" @click="search('clamd-log')"> |
||||
{{ $t('toolbox.clam.clamLog') }} |
||||
</el-button> |
||||
<el-button |
||||
type="primary" |
||||
:plain="activeName !== 'freshclam-log'" |
||||
@click="search('freshclam-log')" |
||||
> |
||||
{{ $t('toolbox.clam.freshClamLog') }} |
||||
</el-button> |
||||
</template> |
||||
</back-button> |
||||
</template> |
||||
|
||||
<template #main> |
||||
<div> |
||||
<codemirror |
||||
:autofocus="true" |
||||
:placeholder="$t('commons.msg.noneData')" |
||||
:indent-with-tab="true" |
||||
:tabSize="4" |
||||
:style="{ height: `calc(100vh - ${loadHeight()})`, 'margin-top': '10px' }" |
||||
:lineWrapping="true" |
||||
:matchBrackets="true" |
||||
theme="cobalt" |
||||
:styleActiveLine="true" |
||||
@ready="handleReady" |
||||
:extensions="extensions" |
||||
v-model="content" |
||||
:disabled="canUpdate()" |
||||
/> |
||||
<el-button type="primary" style="margin-top: 10px" v-if="!canUpdate()" @click="onSave"> |
||||
{{ $t('commons.button.save') }} |
||||
</el-button> |
||||
</div> |
||||
</template> |
||||
</LayoutContent> |
||||
|
||||
<ConfirmDialog ref="confirmRef" @confirm="onSubmit"></ConfirmDialog> |
||||
</div> |
||||
</template> |
||||
|
||||
<script lang="ts" setup> |
||||
import { nextTick, onMounted, ref, shallowRef } from 'vue'; |
||||
import { Codemirror } from 'vue-codemirror'; |
||||
import { javascript } from '@codemirror/lang-javascript'; |
||||
import ClamStatus from '@/views/toolbox/clam/status/index.vue'; |
||||
import { searchClamFile, updateClamFile } from '@/api/modules/toolbox'; |
||||
import { oneDark } from '@codemirror/theme-one-dark'; |
||||
import { GlobalStore } from '@/store'; |
||||
import i18n from '@/lang'; |
||||
import { MsgSuccess } from '@/utils/message'; |
||||
const globalStore = GlobalStore(); |
||||
|
||||
const loading = ref(false); |
||||
const extensions = [javascript(), oneDark]; |
||||
const view = shallowRef(); |
||||
const handleReady = (payload) => { |
||||
view.value = payload.view; |
||||
}; |
||||
|
||||
const activeName = ref('clamd'); |
||||
const content = ref(); |
||||
const confirmRef = ref(); |
||||
|
||||
const loadHeight = () => { |
||||
let height = globalStore.openMenuTabs ? '405px' : '375px'; |
||||
if (canUpdate()) { |
||||
height = globalStore.openMenuTabs ? '363px' : '333px'; |
||||
} |
||||
return height; |
||||
}; |
||||
|
||||
const canUpdate = () => { |
||||
return activeName.value.indexOf('-log') !== -1; |
||||
}; |
||||
|
||||
const search = async (itemName: string) => { |
||||
loading.value = true; |
||||
activeName.value = itemName; |
||||
await searchClamFile(activeName.value) |
||||
.then((res) => { |
||||
loading.value = false; |
||||
content.value = res.data; |
||||
nextTick(() => { |
||||
const state = view.value.state; |
||||
view.value.dispatch({ |
||||
selection: { anchor: state.doc.length, head: state.doc.length }, |
||||
scrollIntoView: true, |
||||
}); |
||||
}); |
||||
}) |
||||
.catch(() => { |
||||
loading.value = false; |
||||
}); |
||||
}; |
||||
|
||||
const onSave = async () => { |
||||
let params = { |
||||
header: i18n.global.t('database.confChange'), |
||||
operationInfo: i18n.global.t('database.restartNowHelper'), |
||||
submitInputInfo: i18n.global.t('database.restartNow'), |
||||
}; |
||||
confirmRef.value!.acceptParams(params); |
||||
}; |
||||
|
||||
const onSubmit = async () => { |
||||
loading.value = true; |
||||
await updateClamFile(activeName.value, content.value) |
||||
.then(() => { |
||||
loading.value = false; |
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess')); |
||||
search(activeName.value); |
||||
}) |
||||
.catch(() => { |
||||
loading.value = false; |
||||
}); |
||||
}; |
||||
|
||||
onMounted(() => { |
||||
search(activeName.value); |
||||
}); |
||||
</script> |
@ -0,0 +1,129 @@
|
||||
<template> |
||||
<div> |
||||
<div class="app-status tool-status" v-if="data.isExist"> |
||||
<el-card> |
||||
<div> |
||||
<el-tag effect="dark" type="success">Clamav</el-tag> |
||||
<el-tag round class="status-content" v-if="data.isActive" type="success"> |
||||
{{ $t('commons.status.running') }} |
||||
</el-tag> |
||||
<el-tag round class="status-content" v-if="!data.isActive" type="info"> |
||||
{{ $t('commons.status.stopped') }} |
||||
</el-tag> |
||||
<el-tag class="status-content">{{ $t('app.version') }}:{{ data.version }}</el-tag> |
||||
<span class="buttons"> |
||||
<el-button type="primary" v-if="!data.isActive" link @click="onOperate('start')"> |
||||
{{ $t('app.start') }} |
||||
</el-button> |
||||
<el-button type="primary" v-if="data.isActive" link @click="onOperate('stop')"> |
||||
{{ $t('app.stop') }} |
||||
</el-button> |
||||
<el-divider direction="vertical" /> |
||||
<el-button type="primary" link @click="onOperate('restart')"> |
||||
{{ $t('app.restart') }} |
||||
</el-button> |
||||
<el-divider direction="vertical" /> |
||||
<el-button type="primary" link @click="setting"> |
||||
{{ $t('commons.button.set') }} |
||||
</el-button> |
||||
</span> |
||||
</div> |
||||
</el-card> |
||||
</div> |
||||
<LayoutContent :title="$t('tool.supervisor.list')" :divider="true" v-if="!data.isExist" v-loading="loading"> |
||||
<template #main> |
||||
<div class="app-warn"> |
||||
<div> |
||||
<span v-if="!data.isExist">{{ $t('tool.supervisor.notSupport') }}</span> |
||||
<span @click="toDoc()" v-if="!data.isExist"> |
||||
<el-icon class="ml-2"><Position /></el-icon> |
||||
{{ $t('firewall.quickJump') }} |
||||
</span> |
||||
<div> |
||||
<img alt="" src="@/assets/images/no_app.svg" /> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
</LayoutContent> |
||||
</div> |
||||
</template> |
||||
<script lang="ts" setup> |
||||
import { searchClamBaseInfo, updateClamBaseInfo } from '@/api/modules/toolbox'; |
||||
import { onMounted, ref } from 'vue'; |
||||
import { ElMessageBox } from 'element-plus'; |
||||
import i18n from '@/lang'; |
||||
import { MsgSuccess } from '@/utils/message'; |
||||
|
||||
const data = ref({ |
||||
isExist: false, |
||||
isActive: false, |
||||
version: '', |
||||
}); |
||||
const loading = ref(false); |
||||
|
||||
const em = defineEmits(['setting', 'getStatus', 'update:loading', 'update:maskShow']); |
||||
|
||||
const setting = () => { |
||||
em('setting', true); |
||||
}; |
||||
|
||||
const toDoc = async () => { |
||||
window.open('https://1panel.cn/docs/user_manual/toolbox/supervisor/', '_blank', 'noopener,noreferrer'); |
||||
}; |
||||
|
||||
const onOperate = async (operation: string) => { |
||||
em('update:maskShow', false); |
||||
ElMessageBox.confirm( |
||||
i18n.global.t('commons.msg.operatorHelper', [' Clamav ', i18n.global.t('app.' + operation)]), |
||||
i18n.global.t('app.' + operation), |
||||
{ |
||||
confirmButtonText: i18n.global.t('commons.button.confirm'), |
||||
cancelButtonText: i18n.global.t('commons.button.cancel'), |
||||
type: 'info', |
||||
}, |
||||
) |
||||
.then(() => { |
||||
em('update:loading', true); |
||||
updateClamBaseInfo(operation) |
||||
.then(() => { |
||||
em('update:maskShow', true); |
||||
getStatus(); |
||||
em('update:loading', false); |
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess')); |
||||
}) |
||||
.catch(() => { |
||||
em('update:loading', false); |
||||
}); |
||||
}) |
||||
.catch(() => { |
||||
em('update:maskShow', true); |
||||
}); |
||||
}; |
||||
|
||||
const getStatus = async () => { |
||||
try { |
||||
loading.value = true; |
||||
em('update:loading', true); |
||||
const res = await searchClamBaseInfo(); |
||||
data.value = res.data; |
||||
const status = { |
||||
isExist: data.value.isExist, |
||||
isRunning: data.value.isActive, |
||||
}; |
||||
em('getStatus', status); |
||||
} catch (error) {} |
||||
em('update:loading', false); |
||||
loading.value = false; |
||||
}; |
||||
|
||||
onMounted(() => { |
||||
getStatus(); |
||||
}); |
||||
</script> |
||||
|
||||
<style lang="scss" scoped> |
||||
.tool-status { |
||||
margin-top: 20px; |
||||
} |
||||
</style> |
Loading…
Reference in new issue