1
0
mirror of https://github.com/1Panel-dev/1Panel.git synced 2025-01-19 08:19:15 +08:00

feat: 增加病毒扫描工具 (#5546)

This commit is contained in:
ssongliu 2024-06-24 13:52:42 +08:00 committed by GitHub
parent a08ce58563
commit 97b790a092
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 3455 additions and 2 deletions

251
backend/app/api/v1/clam.go Normal file
View File

@ -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)
}

View File

@ -38,6 +38,7 @@ var (
deviceService = service.NewIDeviceService() deviceService = service.NewIDeviceService()
fail2banService = service.NewIFail2BanService() fail2banService = service.NewIFail2BanService()
ftpService = service.NewIFtpService() ftpService = service.NewIFtpService()
clamService = service.NewIClamService()
settingService = service.NewISettingService() settingService = service.NewISettingService()
backupService = service.NewIBackupService() backupService = service.NewIBackupService()

52
backend/app/dto/clam.go Normal file
View File

@ -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"`
}

View File

@ -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"`
}

58
backend/app/repo/clam.go Normal file
View File

@ -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
}

366
backend/app/service/clam.go Normal file
View File

@ -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
}

View File

@ -25,6 +25,7 @@ var (
groupRepo = repo.NewIGroupRepo() groupRepo = repo.NewIGroupRepo()
commandRepo = repo.NewICommandRepo() commandRepo = repo.NewICommandRepo()
ftpRepo = repo.NewIFtpRepo() ftpRepo = repo.NewIFtpRepo()
clamRepo = repo.NewIClamRepo()
settingRepo = repo.NewISettingRepo() settingRepo = repo.NewISettingRepo()
backupRepo = repo.NewIBackupRepo() backupRepo = repo.NewIBackupRepo()

View File

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

View File

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

View File

@ -45,5 +45,17 @@ func (s *ToolboxRouter) InitRouter(Router *gin.RouterGroup) {
toolboxRouter.POST("/ftp/update", baseApi.UpdateFtp) toolboxRouter.POST("/ftp/update", baseApi.UpdateFtp)
toolboxRouter.POST("/ftp/del", baseApi.DeleteFtp) toolboxRouter.POST("/ftp/del", baseApi.DeleteFtp)
toolboxRouter.POST("/ftp/sync", baseApi.SyncFtp) toolboxRouter.POST("/ftp/sync", baseApi.SyncFtp)
toolboxRouter.POST("/clam/search", baseApi.SearchClam)
toolboxRouter.POST("/clam/record/search", baseApi.SearchClamRecord)
toolboxRouter.POST("/clam/record/clean", baseApi.CleanClamRecord)
toolboxRouter.POST("/clam/file/search", baseApi.SearchClamFile)
toolboxRouter.POST("/clam/file/update", baseApi.UpdateFile)
toolboxRouter.POST("/clam", baseApi.CreateClam)
toolboxRouter.POST("/clam/base", baseApi.LoadClamBaseInfo)
toolboxRouter.POST("/clam/operate", baseApi.OperateClam)
toolboxRouter.POST("/clam/update", baseApi.UpdateClam)
toolboxRouter.POST("/clam/del", baseApi.DeleteClam)
toolboxRouter.POST("/clam/handle", baseApi.HandleClamScan)
} }
} }

View File

@ -11065,6 +11065,445 @@ const docTemplate = `{
} }
} }
}, },
"/toolbox/clam": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "创建扫描规则",
"consumes": [
"application/json"
],
"tags": [
"Clam"
],
"summary": "Create clam",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.ClamCreate"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [
"name",
"path"
],
"formatEN": "create clam [name][path]",
"formatZH": "创建扫描规则 [name][path]",
"paramKeys": []
}
}
},
"/toolbox/clam/base": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取 Clam 基础信息",
"consumes": [
"application/json"
],
"tags": [
"Clam"
],
"summary": "Load clam base info",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.ClamBaseInfo"
}
}
}
}
},
"/toolbox/clam/del": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "删除扫描规则",
"consumes": [
"application/json"
],
"tags": [
"Clam"
],
"summary": "Delete clam",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.BatchDeleteReq"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [
{
"db": "clams",
"input_column": "id",
"input_value": "ids",
"isList": true,
"output_column": "name",
"output_value": "names"
}
],
"bodyKeys": [
"ids"
],
"formatEN": "delete clam [names]",
"formatZH": "删除扫描规则 [names]",
"paramKeys": []
}
}
},
"/toolbox/clam/file/search": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取扫描文件",
"consumes": [
"application/json"
],
"tags": [
"Clam"
],
"summary": "Load clam file",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.OperationWithName"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.PageResult"
}
}
}
}
},
"/toolbox/clam/file/update": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "更新病毒扫描配置文件",
"consumes": [
"application/json"
],
"tags": [
"Clam"
],
"summary": "Update clam file",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.UpdateByNameAndFile"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/toolbox/clam/handle": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "执行病毒扫描",
"consumes": [
"application/json"
],
"tags": [
"Clam"
],
"summary": "Handle clam scan",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.OperateByID"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [
{
"db": "clams",
"input_column": "id",
"input_value": "id",
"isList": true,
"output_column": "name",
"output_value": "name"
}
],
"bodyKeys": [
"id"
],
"formatEN": "handle clam scan [name]",
"formatZH": "执行病毒扫描 [name]",
"paramKeys": []
}
}
},
"/toolbox/clam/operate": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "修改 Clam 状态",
"consumes": [
"application/json"
],
"tags": [
"Clam"
],
"summary": "Operate Clam",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.Operate"
}
}
],
"responses": {},
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [
"operation"
],
"formatEN": "[operation] FTP",
"formatZH": "[operation] Clam",
"paramKeys": []
}
}
},
"/toolbox/clam/record/clean": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "清空扫描报告",
"consumes": [
"application/json"
],
"tags": [
"Clam"
],
"summary": "Clean clam record",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.OperateByID"
}
}
],
"responses": {},
"x-panel-log": {
"BeforeFunctions": [
{
"db": "clams",
"input_column": "id",
"input_value": "id",
"isList": true,
"output_column": "name",
"output_value": "name"
}
],
"bodyKeys": [
"id"
],
"formatEN": "clean clam record [name]",
"formatZH": "清空扫描报告 [name]",
"paramKeys": []
}
}
},
"/toolbox/clam/record/search": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取扫描结果列表分页",
"consumes": [
"application/json"
],
"tags": [
"Clam"
],
"summary": "Page clam record",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.ClamLogSearch"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.PageResult"
}
}
}
}
},
"/toolbox/clam/search": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取扫描规则列表分页",
"consumes": [
"application/json"
],
"tags": [
"Clam"
],
"summary": "Page clam",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.SearchWithPage"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.PageResult"
}
}
}
}
},
"/toolbox/clam/update": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "修改扫描规则",
"consumes": [
"application/json"
],
"tags": [
"Clam"
],
"summary": "Update clam",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.ClamUpdate"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [
"name",
"path"
],
"formatEN": "update clam [name][path]",
"formatZH": "修改扫描规则 [name][path]",
"paramKeys": []
}
}
},
"/toolbox/clean": { "/toolbox/clean": {
"post": { "post": {
"security": [ "security": [
@ -12748,6 +13187,73 @@ const docTemplate = `{
} }
} }
}, },
"/websites/default/html/:type": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取默认 html",
"consumes": [
"application/json"
],
"tags": [
"Website"
],
"summary": "Get default html",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.FileInfo"
}
}
}
}
},
"/websites/default/html/update": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "更新默认 html",
"consumes": [
"application/json"
],
"tags": [
"Website"
],
"summary": "Update default html",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.WebsiteHtmlUpdate"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [
"type"
],
"formatEN": "Update default html",
"formatZH": "更新默认 html",
"paramKeys": []
}
}
},
"/websites/default/server": { "/websites/default/server": {
"post": { "post": {
"security": [ "security": [
@ -14934,6 +15440,75 @@ const docTemplate = `{
} }
} }
}, },
"dto.ClamBaseInfo": {
"type": "object",
"properties": {
"isActive": {
"type": "boolean"
},
"isExist": {
"type": "boolean"
},
"version": {
"type": "string"
}
}
},
"dto.ClamCreate": {
"type": "object",
"properties": {
"description": {
"type": "string"
},
"name": {
"type": "string"
},
"path": {
"type": "string"
}
}
},
"dto.ClamLogSearch": {
"type": "object",
"required": [
"page",
"pageSize"
],
"properties": {
"clamID": {
"type": "integer"
},
"endTime": {
"type": "string"
},
"page": {
"type": "integer"
},
"pageSize": {
"type": "integer"
},
"startTime": {
"type": "string"
}
}
},
"dto.ClamUpdate": {
"type": "object",
"properties": {
"description": {
"type": "string"
},
"id": {
"type": "integer"
},
"name": {
"type": "string"
},
"path": {
"type": "string"
}
}
},
"dto.Clean": { "dto.Clean": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -20144,6 +20719,9 @@ const docTemplate = `{
"ID": { "ID": {
"type": "integer" "type": "integer"
}, },
"latest": {
"type": "boolean"
},
"name": { "name": {
"type": "string" "type": "string"
}, },
@ -21388,6 +21966,21 @@ const docTemplate = `{
} }
} }
}, },
"request.WebsiteHtmlUpdate": {
"type": "object",
"required": [
"content",
"type"
],
"properties": {
"content": {
"type": "string"
},
"type": {
"type": "string"
}
}
},
"request.WebsiteInstallCheckReq": { "request.WebsiteInstallCheckReq": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@ -11058,6 +11058,445 @@
} }
} }
}, },
"/toolbox/clam": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "创建扫描规则",
"consumes": [
"application/json"
],
"tags": [
"Clam"
],
"summary": "Create clam",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.ClamCreate"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [
"name",
"path"
],
"formatEN": "create clam [name][path]",
"formatZH": "创建扫描规则 [name][path]",
"paramKeys": []
}
}
},
"/toolbox/clam/base": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取 Clam 基础信息",
"consumes": [
"application/json"
],
"tags": [
"Clam"
],
"summary": "Load clam base info",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.ClamBaseInfo"
}
}
}
}
},
"/toolbox/clam/del": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "删除扫描规则",
"consumes": [
"application/json"
],
"tags": [
"Clam"
],
"summary": "Delete clam",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.BatchDeleteReq"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [
{
"db": "clams",
"input_column": "id",
"input_value": "ids",
"isList": true,
"output_column": "name",
"output_value": "names"
}
],
"bodyKeys": [
"ids"
],
"formatEN": "delete clam [names]",
"formatZH": "删除扫描规则 [names]",
"paramKeys": []
}
}
},
"/toolbox/clam/file/search": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取扫描文件",
"consumes": [
"application/json"
],
"tags": [
"Clam"
],
"summary": "Load clam file",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.OperationWithName"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.PageResult"
}
}
}
}
},
"/toolbox/clam/file/update": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "更新病毒扫描配置文件",
"consumes": [
"application/json"
],
"tags": [
"Clam"
],
"summary": "Update clam file",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.UpdateByNameAndFile"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/toolbox/clam/handle": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "执行病毒扫描",
"consumes": [
"application/json"
],
"tags": [
"Clam"
],
"summary": "Handle clam scan",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.OperateByID"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [
{
"db": "clams",
"input_column": "id",
"input_value": "id",
"isList": true,
"output_column": "name",
"output_value": "name"
}
],
"bodyKeys": [
"id"
],
"formatEN": "handle clam scan [name]",
"formatZH": "执行病毒扫描 [name]",
"paramKeys": []
}
}
},
"/toolbox/clam/operate": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "修改 Clam 状态",
"consumes": [
"application/json"
],
"tags": [
"Clam"
],
"summary": "Operate Clam",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.Operate"
}
}
],
"responses": {},
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [
"operation"
],
"formatEN": "[operation] FTP",
"formatZH": "[operation] Clam",
"paramKeys": []
}
}
},
"/toolbox/clam/record/clean": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "清空扫描报告",
"consumes": [
"application/json"
],
"tags": [
"Clam"
],
"summary": "Clean clam record",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.OperateByID"
}
}
],
"responses": {},
"x-panel-log": {
"BeforeFunctions": [
{
"db": "clams",
"input_column": "id",
"input_value": "id",
"isList": true,
"output_column": "name",
"output_value": "name"
}
],
"bodyKeys": [
"id"
],
"formatEN": "clean clam record [name]",
"formatZH": "清空扫描报告 [name]",
"paramKeys": []
}
}
},
"/toolbox/clam/record/search": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取扫描结果列表分页",
"consumes": [
"application/json"
],
"tags": [
"Clam"
],
"summary": "Page clam record",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.ClamLogSearch"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.PageResult"
}
}
}
}
},
"/toolbox/clam/search": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取扫描规则列表分页",
"consumes": [
"application/json"
],
"tags": [
"Clam"
],
"summary": "Page clam",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.SearchWithPage"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.PageResult"
}
}
}
}
},
"/toolbox/clam/update": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "修改扫描规则",
"consumes": [
"application/json"
],
"tags": [
"Clam"
],
"summary": "Update clam",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.ClamUpdate"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [
"name",
"path"
],
"formatEN": "update clam [name][path]",
"formatZH": "修改扫描规则 [name][path]",
"paramKeys": []
}
}
},
"/toolbox/clean": { "/toolbox/clean": {
"post": { "post": {
"security": [ "security": [
@ -12741,6 +13180,73 @@
} }
} }
}, },
"/websites/default/html/:type": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取默认 html",
"consumes": [
"application/json"
],
"tags": [
"Website"
],
"summary": "Get default html",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.FileInfo"
}
}
}
}
},
"/websites/default/html/update": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "更新默认 html",
"consumes": [
"application/json"
],
"tags": [
"Website"
],
"summary": "Update default html",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.WebsiteHtmlUpdate"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [
"type"
],
"formatEN": "Update default html",
"formatZH": "更新默认 html",
"paramKeys": []
}
}
},
"/websites/default/server": { "/websites/default/server": {
"post": { "post": {
"security": [ "security": [
@ -14927,6 +15433,75 @@
} }
} }
}, },
"dto.ClamBaseInfo": {
"type": "object",
"properties": {
"isActive": {
"type": "boolean"
},
"isExist": {
"type": "boolean"
},
"version": {
"type": "string"
}
}
},
"dto.ClamCreate": {
"type": "object",
"properties": {
"description": {
"type": "string"
},
"name": {
"type": "string"
},
"path": {
"type": "string"
}
}
},
"dto.ClamLogSearch": {
"type": "object",
"required": [
"page",
"pageSize"
],
"properties": {
"clamID": {
"type": "integer"
},
"endTime": {
"type": "string"
},
"page": {
"type": "integer"
},
"pageSize": {
"type": "integer"
},
"startTime": {
"type": "string"
}
}
},
"dto.ClamUpdate": {
"type": "object",
"properties": {
"description": {
"type": "string"
},
"id": {
"type": "integer"
},
"name": {
"type": "string"
},
"path": {
"type": "string"
}
}
},
"dto.Clean": { "dto.Clean": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -20137,6 +20712,9 @@
"ID": { "ID": {
"type": "integer" "type": "integer"
}, },
"latest": {
"type": "boolean"
},
"name": { "name": {
"type": "string" "type": "string"
}, },
@ -21381,6 +21959,21 @@
} }
} }
}, },
"request.WebsiteHtmlUpdate": {
"type": "object",
"required": [
"content",
"type"
],
"properties": {
"content": {
"type": "string"
},
"type": {
"type": "string"
}
}
},
"request.WebsiteInstallCheckReq": { "request.WebsiteInstallCheckReq": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@ -216,6 +216,51 @@ definitions:
required: required:
- database - database
type: object type: object
dto.ClamBaseInfo:
properties:
isActive:
type: boolean
isExist:
type: boolean
version:
type: string
type: object
dto.ClamCreate:
properties:
description:
type: string
name:
type: string
path:
type: string
type: object
dto.ClamLogSearch:
properties:
clamID:
type: integer
endTime:
type: string
page:
type: integer
pageSize:
type: integer
startTime:
type: string
required:
- page
- pageSize
type: object
dto.ClamUpdate:
properties:
description:
type: string
id:
type: integer
name:
type: string
path:
type: string
type: object
dto.Clean: dto.Clean:
properties: properties:
name: name:
@ -3720,6 +3765,8 @@ definitions:
properties: properties:
ID: ID:
type: integer type: integer
latest:
type: boolean
name: name:
type: string type: string
page: page:
@ -4565,6 +4612,16 @@ definitions:
required: required:
- websiteId - websiteId
type: object type: object
request.WebsiteHtmlUpdate:
properties:
content:
type: string
type:
type: string
required:
- content
- type
type: object
request.WebsiteInstallCheckReq: request.WebsiteInstallCheckReq:
properties: properties:
InstallIds: InstallIds:
@ -12360,6 +12417,285 @@ paths:
formatEN: upgrade system => [version] formatEN: upgrade system => [version]
formatZH: 更新系统 => [version] formatZH: 更新系统 => [version]
paramKeys: [] paramKeys: []
/toolbox/clam:
post:
consumes:
- application/json
description: 创建扫描规则
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.ClamCreate'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
summary: Create clam
tags:
- Clam
x-panel-log:
BeforeFunctions: []
bodyKeys:
- name
- path
formatEN: create clam [name][path]
formatZH: 创建扫描规则 [name][path]
paramKeys: []
/toolbox/clam/base:
get:
consumes:
- application/json
description: 获取 Clam 基础信息
responses:
"200":
description: OK
schema:
$ref: '#/definitions/dto.ClamBaseInfo'
security:
- ApiKeyAuth: []
summary: Load clam base info
tags:
- Clam
/toolbox/clam/del:
post:
consumes:
- application/json
description: 删除扫描规则
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.BatchDeleteReq'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
summary: Delete clam
tags:
- Clam
x-panel-log:
BeforeFunctions:
- db: clams
input_column: id
input_value: ids
isList: true
output_column: name
output_value: names
bodyKeys:
- ids
formatEN: delete clam [names]
formatZH: 删除扫描规则 [names]
paramKeys: []
/toolbox/clam/file/search:
post:
consumes:
- application/json
description: 获取扫描文件
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.OperationWithName'
responses:
"200":
description: OK
schema:
$ref: '#/definitions/dto.PageResult'
security:
- ApiKeyAuth: []
summary: Load clam file
tags:
- Clam
/toolbox/clam/file/update:
post:
consumes:
- application/json
description: 更新病毒扫描配置文件
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.UpdateByNameAndFile'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
summary: Update clam file
tags:
- Clam
/toolbox/clam/handle:
post:
consumes:
- application/json
description: 执行病毒扫描
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.OperateByID'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
summary: Handle clam scan
tags:
- Clam
x-panel-log:
BeforeFunctions:
- db: clams
input_column: id
input_value: id
isList: true
output_column: name
output_value: name
bodyKeys:
- id
formatEN: handle clam scan [name]
formatZH: 执行病毒扫描 [name]
paramKeys: []
/toolbox/clam/operate:
post:
consumes:
- application/json
description: 修改 Clam 状态
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.Operate'
responses: {}
security:
- ApiKeyAuth: []
summary: Operate Clam
tags:
- Clam
x-panel-log:
BeforeFunctions: []
bodyKeys:
- operation
formatEN: '[operation] FTP'
formatZH: '[operation] Clam'
paramKeys: []
/toolbox/clam/record/clean:
post:
consumes:
- application/json
description: 清空扫描报告
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.OperateByID'
responses: {}
security:
- ApiKeyAuth: []
summary: Clean clam record
tags:
- Clam
x-panel-log:
BeforeFunctions:
- db: clams
input_column: id
input_value: id
isList: true
output_column: name
output_value: name
bodyKeys:
- id
formatEN: clean clam record [name]
formatZH: 清空扫描报告 [name]
paramKeys: []
/toolbox/clam/record/search:
post:
consumes:
- application/json
description: 获取扫描结果列表分页
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.ClamLogSearch'
responses:
"200":
description: OK
schema:
$ref: '#/definitions/dto.PageResult'
security:
- ApiKeyAuth: []
summary: Page clam record
tags:
- Clam
/toolbox/clam/search:
post:
consumes:
- application/json
description: 获取扫描规则列表分页
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.SearchWithPage'
responses:
"200":
description: OK
schema:
$ref: '#/definitions/dto.PageResult'
security:
- ApiKeyAuth: []
summary: Page clam
tags:
- Clam
/toolbox/clam/update:
post:
consumes:
- application/json
description: 修改扫描规则
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.ClamUpdate'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
summary: Update clam
tags:
- Clam
x-panel-log:
BeforeFunctions: []
bodyKeys:
- name
- path
formatEN: update clam [name][path]
formatZH: 修改扫描规则 [name][path]
paramKeys: []
/toolbox/clean: /toolbox/clean:
post: post:
consumes: consumes:
@ -13422,6 +13758,48 @@ paths:
formatEN: Nginx conf update [domain] formatEN: Nginx conf update [domain]
formatZH: nginx 配置修改 [domain] formatZH: nginx 配置修改 [domain]
paramKeys: [] paramKeys: []
/websites/default/html/:type:
get:
consumes:
- application/json
description: 获取默认 html
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.FileInfo'
security:
- ApiKeyAuth: []
summary: Get default html
tags:
- Website
/websites/default/html/update:
post:
consumes:
- application/json
description: 更新默认 html
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.WebsiteHtmlUpdate'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
summary: Update default html
tags:
- Website
x-panel-log:
BeforeFunctions: []
bodyKeys:
- type
formatEN: Update default html
formatZH: 更新默认 html
paramKeys: []
/websites/default/server: /websites/default/server:
post: post:
consumes: consumes:

View File

@ -116,4 +116,42 @@ export namespace Toolbox {
status: string; status: string;
size: string; size: string;
} }
export interface ClamBaseInfo {
version: string;
isActive: boolean;
isExist: boolean;
}
export interface ClamInfo {
id: number;
name: string;
path: string;
lastHandleDate: string;
description: string;
}
export interface ClamCreate {
name: string;
path: string;
description: string;
}
export interface ClamUpdate {
id: number;
name: string;
path: string;
description: string;
}
export interface ClamSearchLog extends ReqPage {
clamID: number;
startTime: Date;
endTime: Date;
}
export interface ClamLog {
name: string;
scanDate: string;
scanTime: string;
scannedFiles: string;
infectedFiles: string;
log: string;
status: string;
}
} }

View File

@ -106,3 +106,38 @@ export const updateFtp = (params: Toolbox.FtpUpdate) => {
export const deleteFtp = (params: { ids: number[] }) => { export const deleteFtp = (params: { ids: number[] }) => {
return http.post(`/toolbox/ftp/del`, params); return http.post(`/toolbox/ftp/del`, params);
}; };
// clam
export const cleanClamRecord = (id: number) => {
return http.post(`/toolbox/clam/record/clean`, { id: id });
};
export const searchClamRecord = (param: Toolbox.ClamSearchLog) => {
return http.post<ResPage<Toolbox.ClamLog>>(`/toolbox/clam/record/search`, param);
};
export const searchClamFile = (name: string) => {
return http.post<string>(`/toolbox/clam/file/search`, { name: name });
};
export const updateClamFile = (name: string, file: string) => {
return http.post(`/toolbox/clam/file/update`, { name: name, file: file });
};
export const searchClamBaseInfo = () => {
return http.post<Toolbox.ClamBaseInfo>(`/toolbox/clam/base`);
};
export const updateClamBaseInfo = (operate: string) => {
return http.post(`/toolbox/clam/operate`, { Operation: operate });
};
export const searchClam = (param: ReqPage) => {
return http.post<ResPage<Toolbox.ClamInfo>>(`/toolbox/clam/search`, param);
};
export const createClam = (params: Toolbox.ClamCreate) => {
return http.post(`/toolbox/clam`, params);
};
export const updateClam = (params: Toolbox.ClamUpdate) => {
return http.post(`/toolbox/clam/update`, params);
};
export const deleteClam = (params: { ids: number[] }) => {
return http.post(`/toolbox/clam/del`, params);
};
export const handleClamScan = (id: number) => {
return http.post(`/toolbox/clam/handle`, { id: id });
};

View File

@ -19,7 +19,6 @@ const props = defineProps({
code: String, code: String,
}); });
const loadErrInfo = () => { const loadErrInfo = () => {
console.log(props.code);
switch (props.code) { switch (props.code) {
case '400': case '400':
return '400 Bad Request'; return '400 Bad Request';

View File

@ -229,6 +229,7 @@ const message = {
status: { status: {
running: 'Running', running: 'Running',
done: 'Done', done: 'Done',
scanFailed: 'Incomplete',
success: 'Success', success: 'Success',
waiting: 'Waiting', waiting: 'Waiting',
waiting1: 'Waiting', waiting1: 'Waiting',
@ -1058,6 +1059,19 @@ const message = {
'Disabling the selected FTP account will revoke its access permissions. Do you want to continue?', 'Disabling the selected FTP account will revoke its access permissions. Do you want to continue?',
syncHelper: 'Sync FTP account data between server and database. Do you want to continue?', syncHelper: 'Sync FTP account data between server and database. Do you want to continue?',
}, },
clam: {
clam: 'Virus Scan',
clamCreate: 'Create Scan Rule',
scanDate: 'Scan Date',
scanTime: 'Elapsed Time',
scannedFiles: 'Number of Scanned Files',
infectedFiles: 'Number of Infected Files',
log: 'Details',
clamConf: 'Scan Configuration',
clamLog: 'Scan Log',
freshClam: 'Virus Database Refresh Configuration',
freshClamLog: 'Virus Database Refresh Log',
},
}, },
logs: { logs: {
panelLog: 'Panel logs', panelLog: 'Panel logs',

View File

@ -224,6 +224,7 @@ const message = {
status: { status: {
running: '已啟動', running: '已啟動',
done: '已完成', done: '已完成',
scanFailed: '未完成',
success: '成功', success: '成功',
waiting: '執行中', waiting: '執行中',
waiting1: '等待中', waiting1: '等待中',
@ -1000,6 +1001,19 @@ const message = {
disableHelper: '停用選取的 FTP 帳號後 FTP 帳號將失去訪問權限是否繼續操作', disableHelper: '停用選取的 FTP 帳號後 FTP 帳號將失去訪問權限是否繼續操作',
syncHelper: '同步伺服器與資料庫中的 FTP 帳戶資料是否繼續操作', syncHelper: '同步伺服器與資料庫中的 FTP 帳戶資料是否繼續操作',
}, },
clam: {
clam: '病毒掃描',
clamCreate: '創建掃描規則',
scanDate: '掃描時間',
scanTime: '耗時',
scannedFiles: '掃描文件數',
infectedFiles: '危險文件數',
log: '詳情',
clamConf: '掃描配置',
clamLog: '掃描日誌',
freshClam: '病毒庫刷新配置',
freshClamLog: '病毒庫刷新日誌',
},
}, },
logs: { logs: {
panelLog: '面板日誌', panelLog: '面板日誌',

View File

@ -225,6 +225,7 @@ const message = {
status: { status: {
running: '已启动', running: '已启动',
done: '已完成', done: '已完成',
scanFailed: '未完成',
success: '成功', success: '成功',
waiting: '执行中', waiting: '执行中',
waiting1: '等待中', waiting1: '等待中',
@ -1002,6 +1003,19 @@ const message = {
disableHelper: '停用选中的 FTP 账号后 FTP 账号将失去访问权限是否继续操作', disableHelper: '停用选中的 FTP 账号后 FTP 账号将失去访问权限是否继续操作',
syncHelper: '同步服务器与数据库中的 FTP 账户数据是否继续操作', syncHelper: '同步服务器与数据库中的 FTP 账户数据是否继续操作',
}, },
clam: {
clam: '病毒扫描',
clamCreate: '创建扫描规则',
scanDate: '扫描时间',
scanTime: '耗时',
scannedFiles: '扫描文件数',
infectedFiles: '危险文件数',
log: '详情',
clamConf: '扫描配置',
clamLog: '扫描日志',
freshClam: '病毒库刷新配置',
freshClamLog: '病毒库刷新日志',
},
}, },
logs: { logs: {
panelLog: '面板日志', panelLog: '面板日志',

View File

@ -37,6 +37,26 @@ const toolboxRouter = {
requiresAuth: false, requiresAuth: false,
}, },
}, },
{
path: 'clam',
name: 'Clam',
component: () => import('@/views/toolbox/clam/index.vue'),
hidden: true,
meta: {
activeMenu: '/toolbox',
requiresAuth: false,
},
},
{
path: 'clam/setting',
name: 'Clam-Setting',
component: () => import('@/views/toolbox/clam/setting/index.vue'),
hidden: true,
meta: {
activeMenu: '/toolbox',
requiresAuth: false,
},
},
{ {
path: 'ftp', path: 'ftp',
name: 'FTP', name: 'FTP',

View File

@ -236,7 +236,6 @@ const onSavePort = async (formEl: FormInstance | undefined) => {
submitInputInfo: i18n.global.t('database.restartNow'), submitInputInfo: i18n.global.t('database.restartNow'),
}; };
confirmPortRef.value!.acceptParams(params); confirmPortRef.value!.acceptParams(params);
return;
}; };
function callback(error: any) { function callback(error: any) {
if (error) { if (error) {

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -61,6 +61,10 @@ const buttons = [
label: i18n.global.t('menu.supervisor'), label: i18n.global.t('menu.supervisor'),
path: '/toolbox/supervisor', path: '/toolbox/supervisor',
}, },
{
label: i18n.global.t('toolbox.clam.clam'),
path: '/toolbox/clam',
},
{ {
label: 'FTP', label: 'FTP',
path: '/toolbox/ftp', path: '/toolbox/ftp',