Compare commits

...
Author SHA1 Message Date
ssongliu 16a35141dd fix: 解决病毒定时扫描校验国际化的问题 (#5856) 2024-07-18 03:27:28 +00:00
John Bro f6c334bdad feat: 计划任务备份应用/网站/数据库支持多选 (#5849) 2024-07-17 13:35:27 +00:00
蘭 cbb119117b fix: 多个文件同时计算大小,计算状态独自显示 (#5851) 2024-07-17 11:09:30 +00:00
ssongliu cae0a6118f pref: 优化远程数据库解绑提示信息 (#5850) 2024-07-17 09:53:28 +00:00
ssongliu 3c0dc7459c feat: 工具箱病毒扫描支持定时扫描 (#5847) 2024-07-17 08:55:28 +00:00
zhengkunwang ca0c96cb12 fix: 解决网站按过期时间排序之后导致的日期控件错误问题 (#5846) 2024-07-17 06:57:29 +00:00
蘭 ed599265e4 fix: 查看文件/文件夹属性详情 (#5844)
Refs #5842

查看文件/文件夹属性详情,不再获取文件内容。
2024-07-17 06:43:27 +00:00
wanghe-fit2cloud 864bff574a chore: 删除 FUNDING.yml 2024-07-17 12:10:43 +08:00
蘭 3b1df05ae0 fix: 关闭 pdf 预览 (#5838)
pdfjs 安全漏洞,后续版本增加预览 pdf
2024-07-16 11:17:27 +00:00
蘭 464392f7e1 fix: 文本管理计算文件夹大小请求超时 (#5836)
文本管理计算文件夹大小调整为局部加载,增加请求时长
2024-07-16 07:05:27 +00:00
zhengkunwang b562d9f8e8 feat: 网站过期时间增加排序 (#5833) 2024-07-16 04:03:28 +00:00
ssongliu 74bd997f07 pref: 优化远程数据库删除提示信息 (#5824)
Refs #5819 #5818
2024-07-15 10:43:27 +00:00
ssongliu 12c6fe6331 feat: 病毒扫描增加状态判断 (#5817)
Refs #5769
2024-07-15 04:05:26 +00:00
maoball eb4d550305 fix: 修复变量名拼错 (#5792)
* fix: 修复变量名拼错

* fix: 修复变量名拼错

* fix: 修复变量名拼错

* fix: 修复变量名拼错

* fix: 修复变量名拼错
2024-07-13 21:58:02 +08:00
zhengkunwang 9a635e99b2 feat: 增加 clamdscan 服务检测 (#5784) 2024-07-12 06:11:25 +00:00
蘭 03b04eea6b feat: 文本编辑器新增右侧缩略图开关 (#5781) 2024-07-12 02:19:25 +00:00
蘭 d2740308a1 fix: 关闭抽屉同时销毁全部子元素 (#5776) 2024-07-11 13:59:24 +00:00
Anye 6f86dda1aa Add port to vscodeUrl in SSH remote connection (#5775)
修改了 vscodeUrl 模板以在主机和路径之间包含来自 addForm.port 的端口,确保正确设置 SSH 远程连接。
2024-07-11 18:45:52 +08:00
zhengkunwang 0194362a35 feat: 增加 Go 运行环境跳转 (#5764) 2024-07-11 08:21:25 +00:00
59 changed files with 1205 additions and 301 deletions
-1
View File
@@ -1 +0,0 @@
custom: ["https://afdian.net/a/1Panel"]
+24 -2
View File
@@ -51,16 +51,38 @@ func (b *BaseApi) UpdateClam(c *gin.Context) {
helper.SuccessWithData(c, nil)
}
// @Tags Clam
// @Summary Update clam status
// @Description 修改扫描规则状态
// @Accept json
// @Param request body dto.ClamUpdateStatus true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /toolbox/clam/status/update [post]
// @x-panel-log {"bodyKeys":["id","status"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"clams","output_column":"name","output_value":"name"}],"formatZH":"修改扫描规则 [name] 状态为 [status]","formatEN":"change the status of clam [name] to [status]."}
func (b *BaseApi) UpdateClamStatus(c *gin.Context) {
var req dto.ClamUpdateStatus
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := clamService.UpdateStatus(req.ID, req.Status); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}
// @Tags Clam
// @Summary Page clam
// @Description 获取扫描规则列表分页
// @Accept json
// @Param request body dto.SearchWithPage true "request"
// @Param request body dto.SearchClamWithPage true "request"
// @Success 200 {object} dto.PageResult
// @Security ApiKeyAuth
// @Router /toolbox/clam/search [post]
func (b *BaseApi) SearchClam(c *gin.Context) {
var req dto.SearchWithPage
var req dto.SearchClamWithPage
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
+17
View File
@@ -4,6 +4,13 @@ import (
"time"
)
type SearchClamWithPage struct {
PageInfo
Info string `json:"info"`
OrderBy string `json:"orderBy" validate:"required,oneof=name status created_at"`
Order string `json:"order" validate:"required,oneof=null ascending descending"`
}
type ClamBaseInfo struct {
Version string `json:"version"`
IsActive bool `json:"isActive"`
@@ -19,10 +26,12 @@ type ClamInfo struct {
CreatedAt time.Time `json:"createdAt"`
Name string `json:"name"`
Status string `json:"status"`
Path string `json:"path"`
InfectedStrategy string `json:"infectedStrategy"`
InfectedDir string `json:"infectedDir"`
LastHandleDate string `json:"lastHandleDate"`
Spec string `json:"spec"`
Description string `json:"description"`
}
@@ -56,9 +65,11 @@ type ClamLog struct {
type ClamCreate struct {
Name string `json:"name"`
Status string `json:"status"`
Path string `json:"path"`
InfectedStrategy string `json:"infectedStrategy"`
InfectedDir string `json:"infectedDir"`
Spec string `json:"spec"`
Description string `json:"description"`
}
@@ -69,9 +80,15 @@ type ClamUpdate struct {
Path string `json:"path"`
InfectedStrategy string `json:"infectedStrategy"`
InfectedDir string `json:"infectedDir"`
Spec string `json:"spec"`
Description string `json:"description"`
}
type ClamUpdateStatus struct {
ID uint `json:"id"`
Status string `json:"status"`
}
type ClamDelete struct {
RemoveRecord bool `json:"removeRecord"`
RemoveInfected bool `json:"removeInfected"`
+2 -2
View File
@@ -7,7 +7,7 @@ import (
type WebsiteSearch struct {
dto.PageInfo
Name string `json:"name"`
OrderBy string `json:"orderBy" validate:"required,oneof=primary_domain type status created_at"`
OrderBy string `json:"orderBy" validate:"required,oneof=primary_domain type status created_at expire_date"`
Order string `json:"order" validate:"required,oneof=null ascending descending"`
WebsiteGroupID uint `json:"websiteGroupId"`
}
@@ -55,7 +55,7 @@ type WebsiteUpdate struct {
ID uint `json:"id" validate:"required"`
PrimaryDomain string `json:"primaryDomain" validate:"required"`
Remark string `json:"remark"`
WebsiteGroupID uint `json:"webSiteGroupID" validate:"required"`
WebsiteGroupID uint `json:"webSiteGroupID"`
ExpireDate string `json:"expireDate"`
IPV6 bool `json:"IPV6"`
}
+3
View File
@@ -4,8 +4,11 @@ type Clam struct {
BaseModel
Name string `gorm:"type:varchar(64);not null" json:"name"`
Status string `gorm:"type:varchar(64)" json:"status"`
Path string `gorm:"type:varchar(64);not null" json:"path"`
InfectedStrategy string `gorm:"type:varchar(64)" json:"infectedStrategy"`
InfectedDir string `gorm:"type:varchar(64)" json:"infectedDir"`
Spec string `gorm:"type:varchar(64)" json:"spec"`
EntryID int `gorm:"type:varchar(64)" json:"entryID"`
Description string `gorm:"type:varchar(64)" json:"description"`
}
+11
View File
@@ -13,6 +13,7 @@ type IClamRepo interface {
Update(id uint, vars map[string]interface{}) error
Delete(opts ...DBOption) error
Get(opts ...DBOption) (model.Clam, error)
List(opts ...DBOption) ([]model.Clam, error)
}
func NewIClamRepo() IClamRepo {
@@ -29,6 +30,16 @@ func (u *ClamRepo) Get(opts ...DBOption) (model.Clam, error) {
return clam, err
}
func (u *ClamRepo) List(opts ...DBOption) ([]model.Clam, error) {
var clam []model.Clam
db := global.DB
for _, opt := range opts {
db = opt(db)
}
err := db.Find(&clam).Error
return clam, err
}
func (u *ClamRepo) Page(page, size int, opts ...DBOption) (int64, []model.Clam, error) {
var users []model.Clam
db := global.DB.Model(&model.Clam{})
+1 -1
View File
@@ -96,7 +96,7 @@ func (w *WebsiteRepo) Page(page, size int, opts ...DBOption) (int64, []model.Web
db := getDb(opts...).Model(&model.Website{})
count := int64(0)
db = db.Count(&count)
err := db.Limit(size).Offset(size * (page - 1)).Preload("WebsiteSSL").Find(&websites).Error
err := db.Debug().Limit(size).Offset(size * (page - 1)).Preload("WebsiteSSL").Find(&websites).Error
return count, websites, err
}
+108 -45
View File
@@ -12,13 +12,16 @@ import (
"time"
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/app/model"
"github.com/1Panel-dev/1Panel/backend/buserr"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
"github.com/1Panel-dev/1Panel/backend/utils/common"
"github.com/1Panel-dev/1Panel/backend/utils/systemctl"
"github.com/1Panel-dev/1Panel/backend/utils/xpack"
"github.com/jinzhu/copier"
"github.com/robfig/cron/v3"
"github.com/pkg/errors"
)
@@ -37,9 +40,10 @@ type ClamService struct {
type IClamService interface {
LoadBaseInfo() (dto.ClamBaseInfo, error)
Operate(operate string) error
SearchWithPage(search dto.SearchWithPage) (int64, interface{}, error)
SearchWithPage(search dto.SearchClamWithPage) (int64, interface{}, error)
Create(req dto.ClamCreate) error
Update(req dto.ClamUpdate) error
UpdateStatus(id uint, status string) error
Delete(req dto.ClamDelete) error
HandleOnce(req dto.OperateByID) error
LoadFile(req dto.ClamFileReq) (string, error)
@@ -54,19 +58,19 @@ func NewIClamService() IClamService {
return &ClamService{}
}
func (f *ClamService) LoadBaseInfo() (dto.ClamBaseInfo, error) {
func (c *ClamService) LoadBaseInfo() (dto.ClamBaseInfo, error) {
var baseInfo dto.ClamBaseInfo
baseInfo.Version = "-"
baseInfo.FreshVersion = "-"
exist1, _ := systemctl.IsExist(clamServiceNameCentOs)
if exist1 {
f.serviceName = clamServiceNameCentOs
c.serviceName = clamServiceNameCentOs
baseInfo.IsExist = true
baseInfo.IsActive, _ = systemctl.IsActive(clamServiceNameCentOs)
}
exist2, _ := systemctl.IsExist(clamServiceNameUbuntu)
if exist2 {
f.serviceName = clamServiceNameUbuntu
c.serviceName = clamServiceNameUbuntu
baseInfo.IsExist = true
baseInfo.IsActive, _ = systemctl.IsActive(clamServiceNameUbuntu)
}
@@ -75,44 +79,45 @@ func (f *ClamService) LoadBaseInfo() (dto.ClamBaseInfo, error) {
baseInfo.FreshIsExist = true
baseInfo.FreshIsActive, _ = systemctl.IsActive(freshClamService)
}
if !cmd.Which("clamdscan") {
baseInfo.IsActive = false
}
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 ")
if err == nil {
if strings.Contains(version, "/") {
baseInfo.Version = strings.TrimPrefix(strings.Split(version, "/")[0], "ClamAV ")
} else {
baseInfo.Version = strings.TrimPrefix(version, "ClamAV ")
}
}
}
if baseInfo.FreshIsActive {
version, err := cmd.Exec("freshclam --version")
if err != nil {
return baseInfo, nil
}
if strings.Contains(version, "/") {
baseInfo.FreshVersion = strings.TrimPrefix(strings.Split(version, "/")[0], "ClamAV ")
} else {
baseInfo.FreshVersion = strings.TrimPrefix(version, "ClamAV ")
if err == nil {
if strings.Contains(version, "/") {
baseInfo.FreshVersion = strings.TrimPrefix(strings.Split(version, "/")[0], "ClamAV ")
} else {
baseInfo.FreshVersion = strings.TrimPrefix(version, "ClamAV ")
}
}
}
return baseInfo, nil
}
func (f *ClamService) Operate(operate string) error {
func (c *ClamService) Operate(operate string) error {
switch operate {
case "start", "restart", "stop":
stdout, err := cmd.Execf("systemctl %s %s", operate, f.serviceName)
stdout, err := cmd.Execf("systemctl %s %s", operate, c.serviceName)
if err != nil {
return fmt.Errorf("%s the %s failed, err: %s", operate, f.serviceName, stdout)
return fmt.Errorf("%s the %s failed, err: %s", operate, c.serviceName, stdout)
}
return nil
case "fresh-start", "fresh-restart", "fresh-stop":
stdout, err := cmd.Execf("systemctl %s %s", strings.TrimPrefix(operate, "fresh-"), freshClamService)
if err != nil {
return fmt.Errorf("%s the %s failed, err: %s", operate, f.serviceName, stdout)
return fmt.Errorf("%s the %s failed, err: %s", operate, c.serviceName, stdout)
}
return nil
default:
@@ -120,8 +125,8 @@ func (f *ClamService) Operate(operate string) error {
}
}
func (f *ClamService) SearchWithPage(req dto.SearchWithPage) (int64, interface{}, error) {
total, commands, err := clamRepo.Page(req.Page, req.PageSize, commonRepo.WithLikeName(req.Info))
func (c *ClamService) SearchWithPage(req dto.SearchClamWithPage) (int64, interface{}, error) {
total, commands, err := clamRepo.Page(req.Page, req.PageSize, commonRepo.WithLikeName(req.Info), commonRepo.WithOrderRuleBy(req.OrderBy, req.Order))
if err != nil {
return 0, nil, err
}
@@ -151,7 +156,7 @@ func (f *ClamService) SearchWithPage(req dto.SearchWithPage) (int64, interface{}
return total, datas, err
}
func (f *ClamService) Create(req dto.ClamCreate) error {
func (c *ClamService) Create(req dto.ClamCreate) error {
clam, _ := clamRepo.Get(commonRepo.WithByName(req.Name))
if clam.ID != 0 {
return constant.ErrRecordExist
@@ -162,13 +167,21 @@ func (f *ClamService) Create(req dto.ClamCreate) error {
if clam.InfectedStrategy == "none" || clam.InfectedStrategy == "remove" {
clam.InfectedDir = ""
}
if len(req.Spec) != 0 {
entryID, err := xpack.StartClam(clam, false)
if err != nil {
return err
}
clam.EntryID = entryID
clam.Status = constant.StatusEnable
}
if err := clamRepo.Create(&clam); err != nil {
return err
}
return nil
}
func (f *ClamService) Update(req dto.ClamUpdate) error {
func (c *ClamService) Update(req dto.ClamUpdate) error {
clam, _ := clamRepo.Get(commonRepo.WithByName(req.Name))
if clam.ID == 0 {
return constant.ErrRecordNotFound
@@ -176,11 +189,36 @@ func (f *ClamService) Update(req dto.ClamUpdate) error {
if req.InfectedStrategy == "none" || req.InfectedStrategy == "remove" {
req.InfectedDir = ""
}
var clamItem model.Clam
if err := copier.Copy(&clamItem, &req); err != nil {
return errors.WithMessage(constant.ErrStructTransform, err.Error())
}
clamItem.EntryID = clam.EntryID
upMap := map[string]interface{}{}
if len(clam.Spec) != 0 && clam.EntryID != 0 {
global.Cron.Remove(cron.EntryID(clamItem.EntryID))
upMap["entry_id"] = 0
}
if len(req.Spec) == 0 {
upMap["status"] = ""
upMap["entry_id"] = 0
}
if len(req.Spec) != 0 && clam.Status != constant.StatusDisable {
newEntryID, err := xpack.StartClam(clamItem, true)
if err != nil {
return err
}
upMap["entry_id"] = newEntryID
}
if len(clam.Spec) == 0 && len(req.Spec) != 0 {
upMap["status"] = constant.StatusEnable
}
upMap["name"] = req.Name
upMap["path"] = req.Path
upMap["infected_dir"] = req.InfectedDir
upMap["infected_strategy"] = req.InfectedStrategy
upMap["spec"] = req.Spec
upMap["description"] = req.Description
if err := clamRepo.Update(req.ID, upMap); err != nil {
return err
@@ -188,7 +226,29 @@ func (f *ClamService) Update(req dto.ClamUpdate) error {
return nil
}
func (u *ClamService) Delete(req dto.ClamDelete) error {
func (c *ClamService) UpdateStatus(id uint, status string) error {
clam, _ := clamRepo.Get(commonRepo.WithByID(id))
if clam.ID == 0 {
return constant.ErrRecordNotFound
}
var (
entryID int
err error
)
if status == constant.StatusEnable {
entryID, err = xpack.StartClam(clam, true)
if err != nil {
return err
}
} else {
global.Cron.Remove(cron.EntryID(clam.EntryID))
global.LOG.Infof("stop cronjob entryID: %v", clam.EntryID)
}
return clamRepo.Update(clam.ID, map[string]interface{}{"status": status, "entry_id": entryID})
}
func (c *ClamService) Delete(req dto.ClamDelete) error {
for _, id := range req.Ids {
clam, _ := clamRepo.Get(commonRepo.WithByID(id))
if clam.ID == 0 {
@@ -207,7 +267,10 @@ func (u *ClamService) Delete(req dto.ClamDelete) error {
return nil
}
func (u *ClamService) HandleOnce(req dto.OperateByID) error {
func (c *ClamService) HandleOnce(req dto.OperateByID) error {
if cmd.Which("clamdscan") == false {
return buserr.New("ErrClamdscanNotFound")
}
clam, _ := clamRepo.Get(commonRepo.WithByID(req.ID))
if clam.ID == 0 {
return constant.ErrRecordNotFound
@@ -241,13 +304,13 @@ func (u *ClamService) HandleOnce(req dto.OperateByID) error {
global.LOG.Debugf("clamdscan --fdpass %s %s -l %s", strategy, clam.Path, logFile)
stdout, err := cmd.Execf("clamdscan --fdpass %s %s -l %s", strategy, clam.Path, logFile)
if err != nil {
global.LOG.Errorf("clamdscan failed, stdout: %v, err: %v", string(stdout), err)
global.LOG.Errorf("clamdscan failed, stdout: %v, err: %v", stdout, err)
}
}()
return nil
}
func (u *ClamService) LoadRecords(req dto.ClamLogSearch) (int64, interface{}, error) {
func (c *ClamService) LoadRecords(req dto.ClamLogSearch) (int64, interface{}, error) {
clam, _ := clamRepo.Get(commonRepo.WithByID(req.ClamID))
if clam.ID == 0 {
return 0, nil, constant.ErrRecordNotFound
@@ -294,7 +357,7 @@ func (u *ClamService) LoadRecords(req dto.ClamLogSearch) (int64, interface{}, er
}
return int64(total), datas, nil
}
func (u *ClamService) LoadRecordLog(req dto.ClamLogReq) (string, error) {
func (c *ClamService) LoadRecordLog(req dto.ClamLogReq) (string, error) {
logPath := path.Join(global.CONF.System.DataDir, resultDir, req.ClamName, req.RecordName)
var tail string
if req.Tail != "0" {
@@ -310,7 +373,7 @@ func (u *ClamService) LoadRecordLog(req dto.ClamLogReq) (string, error) {
return string(stdout), nil
}
func (u *ClamService) CleanRecord(req dto.OperateByID) error {
func (c *ClamService) CleanRecord(req dto.OperateByID) error {
clam, _ := clamRepo.Get(commonRepo.WithByID(req.ID))
if clam.ID == 0 {
return constant.ErrRecordNotFound
@@ -320,37 +383,37 @@ func (u *ClamService) CleanRecord(req dto.OperateByID) error {
return nil
}
func (u *ClamService) LoadFile(req dto.ClamFileReq) (string, error) {
func (c *ClamService) LoadFile(req dto.ClamFileReq) (string, error) {
filePath := ""
switch req.Name {
case "clamd":
if u.serviceName == clamServiceNameUbuntu {
if c.serviceName == clamServiceNameUbuntu {
filePath = "/etc/clamav/clamd.conf"
} else {
filePath = "/etc/clamd.d/scan.conf"
}
case "clamd-log":
filePath = u.loadLogPath("clamd-log")
filePath = c.loadLogPath("clamd-log")
if len(filePath) != 0 {
break
}
if u.serviceName == clamServiceNameUbuntu {
if c.serviceName == clamServiceNameUbuntu {
filePath = "/var/log/clamav/clamav.log"
} else {
filePath = "/var/log/clamd.scan"
}
case "freshclam":
if u.serviceName == clamServiceNameUbuntu {
if c.serviceName == clamServiceNameUbuntu {
filePath = "/etc/clamav/freshclam.conf"
} else {
filePath = "/etc/freshclam.conf"
}
case "freshclam-log":
filePath = u.loadLogPath("freshclam-log")
filePath = c.loadLogPath("freshclam-log")
if len(filePath) != 0 {
break
}
if u.serviceName == clamServiceNameUbuntu {
if c.serviceName == clamServiceNameUbuntu {
filePath = "/var/log/clamav/freshclam.log"
} else {
filePath = "/var/log/freshclam.log"
@@ -375,12 +438,12 @@ func (u *ClamService) LoadFile(req dto.ClamFileReq) (string, error) {
return string(stdout), nil
}
func (u *ClamService) UpdateFile(req dto.UpdateByNameAndFile) error {
func (c *ClamService) UpdateFile(req dto.UpdateByNameAndFile) error {
filePath := ""
service := ""
switch req.Name {
case "clamd":
if u.serviceName == clamServiceNameUbuntu {
if c.serviceName == clamServiceNameUbuntu {
service = clamServiceNameUbuntu
filePath = "/etc/clamav/clamd.conf"
} else {
@@ -388,7 +451,7 @@ func (u *ClamService) UpdateFile(req dto.UpdateByNameAndFile) error {
filePath = "/etc/clamd.d/scan.conf"
}
case "freshclam":
if u.serviceName == clamServiceNameUbuntu {
if c.serviceName == clamServiceNameUbuntu {
filePath = "/etc/clamav/freshclam.conf"
} else {
filePath = "/etc/freshclam.conf"
@@ -458,16 +521,16 @@ func loadResultFromLog(pathItem string) dto.ClamLog {
}
return data
}
func (u *ClamService) loadLogPath(name string) string {
func (c *ClamService) loadLogPath(name string) string {
confPath := ""
if name == "clamd-log" {
if u.serviceName == clamServiceNameUbuntu {
if c.serviceName == clamServiceNameUbuntu {
confPath = "/etc/clamav/clamd.conf"
} else {
confPath = "/etc/clamd.d/scan.conf"
}
} else {
if u.serviceName == clamServiceNameUbuntu {
if c.serviceName == clamServiceNameUbuntu {
confPath = "/etc/clamav/freshclam.conf"
} else {
confPath = "/etc/freshclam.conf"
+36 -25
View File
@@ -17,15 +17,20 @@ import (
func (u *CronjobService) handleApp(cronjob model.Cronjob, startTime time.Time) error {
var apps []model.AppInstall
if cronjob.AppID == "all" {
if strings.Contains(cronjob.AppID, "all") {
apps, _ = appInstallRepo.ListBy()
} else {
itemID, _ := strconv.Atoi(cronjob.AppID)
app, err := appInstallRepo.GetFirst(commonRepo.WithByID(uint(itemID)))
appIds := strings.Split(cronjob.AppID, ",")
var idItems []uint
for i := 0; i < len(appIds); i++ {
itemID, _ := strconv.Atoi(appIds[i])
idItems = append(idItems, uint(itemID))
}
appItems, err := appInstallRepo.ListBy(commonRepo.WithIdsIn(idItems))
if err != nil {
return err
}
apps = append(apps, app)
apps = appItems
}
accountMap, err := loadClientMap(cronjob.BackupAccounts)
if err != nil {
@@ -232,7 +237,7 @@ type databaseHelper struct {
func loadDbsForJob(cronjob model.Cronjob) []databaseHelper {
var dbs []databaseHelper
if cronjob.DBName == "all" {
if strings.Contains(cronjob.DBName, "all") {
if cronjob.DBType == "mysql" || cronjob.DBType == "mariadb" {
mysqlItems, _ := mysqlRepo.List()
for _, mysql := range mysqlItems {
@@ -254,36 +259,42 @@ func loadDbsForJob(cronjob model.Cronjob) []databaseHelper {
}
return dbs
}
itemID, _ := strconv.Atoi(cronjob.DBName)
if cronjob.DBType == "mysql" || cronjob.DBType == "mariadb" {
mysqlItem, _ := mysqlRepo.Get(commonRepo.WithByID(uint(itemID)))
dbs = append(dbs, databaseHelper{
DBType: cronjob.DBType,
Database: mysqlItem.MysqlName,
Name: mysqlItem.Name,
})
} else {
pgItem, _ := postgresqlRepo.Get(commonRepo.WithByID(uint(itemID)))
dbs = append(dbs, databaseHelper{
DBType: cronjob.DBType,
Database: pgItem.PostgresqlName,
Name: pgItem.Name,
})
dbNames := strings.Split(cronjob.DBName, ",")
for _, name := range dbNames {
itemID, _ := strconv.Atoi(name)
if cronjob.DBType == "mysql" || cronjob.DBType == "mariadb" {
mysqlItem, _ := mysqlRepo.Get(commonRepo.WithByID(uint(itemID)))
dbs = append(dbs, databaseHelper{
DBType: cronjob.DBType,
Database: mysqlItem.MysqlName,
Name: mysqlItem.Name,
})
} else {
pgItem, _ := postgresqlRepo.Get(commonRepo.WithByID(uint(itemID)))
dbs = append(dbs, databaseHelper{
DBType: cronjob.DBType,
Database: pgItem.PostgresqlName,
Name: pgItem.Name,
})
}
}
return dbs
}
func loadWebsForJob(cronjob model.Cronjob) []model.Website {
var weblist []model.Website
if cronjob.Website == "all" {
if strings.Contains(cronjob.Website, "all") {
weblist, _ = websiteRepo.List()
return weblist
}
itemID, _ := strconv.Atoi(cronjob.Website)
webItem, _ := websiteRepo.GetFirst(commonRepo.WithByID(uint(itemID)))
if webItem.ID != 0 {
weblist = append(weblist, webItem)
websites := strings.Split(cronjob.Website, ",")
var idItems []uint
for i := 0; i < len(websites); i++ {
itemID, _ := strconv.Atoi(websites[i])
idItems = append(idItems, uint(itemID))
}
weblist, _ = websiteRepo.List(commonRepo.WithIdsIn(idItems))
return weblist
}
+2 -2
View File
@@ -183,7 +183,7 @@ func (f *FileService) Create(op request.FileCreate) error {
}
fo := files.NewFileOp()
if fo.Stat(op.Path) {
return buserr.New(constant.ErrFileIsExit)
return buserr.New(constant.ErrFileIsExist)
}
mode := op.Mode
if mode == 0 {
@@ -273,7 +273,7 @@ func (f *FileService) ChangeOwner(req request.FileRoleUpdate) error {
func (f *FileService) Compress(c request.FileCompress) error {
fo := files.NewFileOp()
if !c.Replace && fo.Stat(filepath.Join(c.Dst, c.Name)) {
return buserr.New(constant.ErrFileIsExit)
return buserr.New(constant.ErrFileIsExist)
}
return fo.Compress(c.Files, c.Dst, c.Name, files.CompressType(c.Type), c.Secret)
}
+1 -1
View File
@@ -95,7 +95,7 @@ var (
ErrPathNotFound = "ErrPathNotFound"
ErrMovePathFailed = "ErrMovePathFailed"
ErrLinkPathNotFound = "ErrLinkPathNotFound"
ErrFileIsExit = "ErrFileIsExit"
ErrFileIsExist = "ErrFileIsExist"
ErrFileUpload = "ErrFileUpload"
ErrFileDownloadDir = "ErrFileDownloadDir"
ErrCmdNotFound = "ErrCmdNotFound"
+2 -1
View File
@@ -74,7 +74,7 @@ ErrFileToLarge: "file is too large"
ErrPathNotFound: "Path is not found"
ErrMovePathFailed: "The target path cannot contain the original path!"
ErrLinkPathNotFound: "Target path does not exist!"
ErrFileIsExit: "File or directory already exists!"
ErrFileIsExist: "File or directory already exists!"
ErrFileUpload: "Failed to upload file {{.name}} {{.detail}}"
ErrFileDownloadDir: "Download folder not supported"
ErrCmdNotFound: "{{ .name}} command does not exist, please install this command on the host first"
@@ -174,6 +174,7 @@ CutWebsiteLogSuccess: "{{ .name }} website log cut successfully, backup path {{
#toolbox
ErrNotExistUser: "The current user does not exist. Please modify and retry!"
ErrBanAction: "Setting failed, the current {{ .name }} service is unavailable, please check and try again!"
ErrClamdscanNotFound: "The clamdscan command was not detected, please refer to the documentation to install it!"
#waf
ErrScope: "Modification of this configuration is not supported"
+2 -1
View File
@@ -75,7 +75,7 @@ ErrFileToLarge: "文件超過10M,無法打開"
ErrPathNotFound: "目錄不存在"
ErrMovePathFailed: "目標路徑不能包含原路徑!"
ErrLinkPathNotFound: "目標路徑不存在!"
ErrFileIsExit: "文件或文件夾已存在!"
ErrFileIsExist: "文件或文件夾已存在!"
ErrFileUpload: "{{ .name }} 上傳文件失敗 {{ .detail}}"
ErrFileDownloadDir: "不支持下載文件夾"
ErrCmdNotFound: "{{ .name}} 命令不存在,請先在宿主機安裝此命令"
@@ -175,6 +175,7 @@ CutWebsiteLogSuccess: "{{ .name }} 網站日誌切割成功,備份路徑 {{ .p
#toolbox
ErrNotExistUser: "當前使用者不存在,請修改後重試!"
ErrBanAction: "設置失敗,當前 {{ .name }} 服務不可用,請檢查後重試!"
ErrClamdscanNotFound: "未偵測到 clamdscan 指令,請參考文件安裝!"
#waf
ErrScope: "不支援修改此配置"
+2 -1
View File
@@ -74,7 +74,7 @@ ErrFileToLarge: "文件超过10M,无法打开"
ErrPathNotFound: "目录不存在"
ErrMovePathFailed: "目标路径不能包含原路径!"
ErrLinkPathNotFound: "目标路径不存在!"
ErrFileIsExit: "文件或文件夹已存在!"
ErrFileIsExist: "文件或文件夹已存在!"
ErrFileUpload: "{{ .name }} 上传文件失败 {{ .detail}}"
ErrFileDownloadDir: "不支持下载文件夹"
ErrCmdNotFound: "{{ .name}} 命令不存在,请先在宿主机安装此命令"
@@ -177,6 +177,7 @@ CutWebsiteLogSuccess: "{{ .name }} 网站日志切割成功,备份路径 {{ .p
#toolbox
ErrNotExistUser: "当前用户不存在,请修改后重试!"
ErrBanAction: "设置失败,当前 {{ .name }} 服务不可用,请检查后重试!"
ErrClamdscanNotFound: "未检测到 clamdscan 命令,请参考文档安装!"
#waf
ErrScope: "不支持修改此配置"
+1
View File
@@ -92,6 +92,7 @@ func Init() {
migrations.AddForward,
migrations.AddShellColumn,
migrations.AddClam,
migrations.AddClamStatus,
})
if err := m.Migrate(); err != nil {
global.LOG.Error(err)
@@ -278,3 +278,13 @@ var AddClam = &gormigrate.Migration{
return nil
},
}
var AddClamStatus = &gormigrate.Migration{
ID: "20240716-add-clam-status",
Migrate: func(tx *gorm.DB) error {
if err := tx.AutoMigrate(&model.Clam{}); err != nil {
return err
}
return nil
},
}
+1
View File
@@ -56,6 +56,7 @@ func (s *ToolboxRouter) InitRouter(Router *gin.RouterGroup) {
toolboxRouter.POST("/clam/base", baseApi.LoadClamBaseInfo)
toolboxRouter.POST("/clam/operate", baseApi.OperateClam)
toolboxRouter.POST("/clam/update", baseApi.UpdateClam)
toolboxRouter.POST("/clam/status/update", baseApi.UpdateClamStatus)
toolboxRouter.POST("/clam/del", baseApi.DeleteClam)
toolboxRouter.POST("/clam/handle", baseApi.HandleClamScan)
}
+5 -2
View File
@@ -203,8 +203,11 @@ func SudoHandleCmd() string {
}
func Which(name string) bool {
_, err := exec.LookPath(name)
return err == nil
stdout, err := Execf("which %s", name)
if err != nil || (len(strings.ReplaceAll(stdout, "\n", "")) == 0 && strings.HasPrefix(stdout, "/")) {
return false
}
return true
}
func ExecShellWithTimeOut(cmdStr, workdir string, logger *log.Logger, timeout time.Duration) error {
+9 -5
View File
@@ -230,13 +230,17 @@ func RemoveRepeatElement(a interface{}) (ret []interface{}) {
}
func LoadSizeUnit(value float64) string {
if value > 1048576 {
return fmt.Sprintf("%vM", value/1048576)
val := int64(value)
if val%1024 != 0 {
return fmt.Sprintf("%v", val)
}
if value > 1024 {
return fmt.Sprintf("%vK", value/1024)
if val > 1048576 {
return fmt.Sprintf("%vM", val/1048576)
}
return fmt.Sprintf("%v", value)
if val > 1024 {
return fmt.Sprintf("%vK", val/1024)
}
return fmt.Sprintf("%v", val)
}
func LoadSizeUnit2F(value float64) string {
+122 -71
View File
@@ -104,20 +104,25 @@ func NewFileInfo(op FileOption) (*FileInfo, error) {
file.LinkPath = GetSymlink(op.Path)
}
if op.Expand {
if file.IsDir {
if err := file.listChildren(op); err != nil {
return nil, err
}
return file, nil
} else {
if err := file.getContent(); err != nil {
return nil, err
}
if err := handleExpansion(file, op); err != nil {
return nil, err
}
}
return file, nil
}
func handleExpansion(file *FileInfo, op FileOption) error {
if file.IsDir {
return file.listChildren(op)
}
if !file.IsDetail {
return file.getContent()
}
return nil
}
func (f *FileInfo) search(search string, count int) (files []FileSearchInfo, total int, err error) {
cmd := exec.Command("find", f.Path, "-name", fmt.Sprintf("*%s*", search))
output, err := cmd.StdoutPipe()
@@ -203,63 +208,82 @@ func (f *FileInfo) listChildren(option FileOption) error {
return err
}
} else {
dirFiles, err := afs.ReadDir(f.Path)
files, err = f.getFiles(afs, option)
if err != nil {
return err
}
var (
dirs []FileSearchInfo
fileList []FileSearchInfo
)
for _, file := range dirFiles {
info := FileSearchInfo{
Path: f.Path,
FileInfo: file,
}
if file.IsDir() {
dirs = append(dirs, info)
} else {
fileList = append(fileList, info)
}
}
sortFileList(dirs, option.SortBy, option.SortOrder)
sortFileList(fileList, option.SortBy, option.SortOrder)
files = append(dirs, fileList...)
}
items, err := f.processFiles(files, option)
if err != nil {
return err
}
if option.ContainSub {
f.ItemTotal = total
}
start := (option.Page - 1) * option.PageSize
end := option.PageSize + start
var result []*FileInfo
if start < 0 || start > f.ItemTotal || end < 0 || start > end {
result = items
} else {
if end > f.ItemTotal {
result = items[start:]
} else {
result = items[start:end]
}
}
f.Items = result
return nil
}
func (f *FileInfo) getFiles(afs *afero.Afero, option FileOption) ([]FileSearchInfo, error) {
dirFiles, err := afs.ReadDir(f.Path)
if err != nil {
return nil, err
}
var (
dirs []FileSearchInfo
fileList []FileSearchInfo
)
for _, file := range dirFiles {
info := FileSearchInfo{
Path: f.Path,
FileInfo: file,
}
if file.IsDir() {
dirs = append(dirs, info)
} else {
fileList = append(fileList, info)
}
}
sortFileList(dirs, option.SortBy, option.SortOrder)
sortFileList(fileList, option.SortBy, option.SortOrder)
return append(dirs, fileList...), nil
}
func (f *FileInfo) processFiles(files []FileSearchInfo, option FileOption) ([]*FileInfo, error) {
var items []*FileInfo
for _, df := range files {
if option.Dir && !df.IsDir() {
if shouldSkipFile(df, option) {
continue
}
name := df.Name()
fPath := path.Join(df.Path, df.Name())
if option.Search != "" {
if option.ContainSub {
fPath = df.Path
name = strings.TrimPrefix(strings.TrimPrefix(fPath, f.Path), "/")
} else {
lowerName := strings.ToLower(name)
lowerSearch := strings.ToLower(option.Search)
if !strings.Contains(lowerName, lowerSearch) {
continue
}
}
}
name, fPath := f.getFilePathAndName(option, df)
if !option.ShowHidden && IsHidden(name) {
continue
}
f.ItemTotal++
isSymlink, isInvalidLink := false, false
if IsSymlink(df.Mode()) {
isSymlink = true
info, err := f.Fs.Stat(fPath)
if err == nil {
df.FileInfo = info
} else {
isInvalidLink = true
}
}
isSymlink, isInvalidLink := f.checkSymlink(df)
file := &FileInfo{
Fs: f.Fs,
@@ -294,24 +318,53 @@ func (f *FileInfo) listChildren(option FileOption) error {
}
items = append(items, file)
}
if option.ContainSub {
f.ItemTotal = total
return items, nil
}
func shouldSkipFile(df FileSearchInfo, option FileOption) bool {
if option.Dir && !df.IsDir() {
return true
}
start := (option.Page - 1) * option.PageSize
end := option.PageSize + start
var result []*FileInfo
if start < 0 || start > f.ItemTotal || end < 0 || start > end {
result = items
} else {
if end > f.ItemTotal {
result = items[start:]
} else {
result = items[start:end]
if option.Search != "" && !option.ContainSub {
lowerName := strings.ToLower(df.Name())
lowerSearch := strings.ToLower(option.Search)
if !strings.Contains(lowerName, lowerSearch) {
return true
}
}
f.Items = result
return nil
return false
}
func (f *FileInfo) getFilePathAndName(option FileOption, df FileSearchInfo) (string, string) {
name := df.Name()
fPath := path.Join(df.Path, df.Name())
if option.Search != "" && option.ContainSub {
fPath = df.Path
name = strings.TrimPrefix(strings.TrimPrefix(fPath, f.Path), "/")
}
return name, fPath
}
func (f *FileInfo) checkSymlink(df FileSearchInfo) (bool, bool) {
isSymlink := false
isInvalidLink := false
if IsSymlink(df.Mode()) {
isSymlink = true
info, err := f.Fs.Stat(path.Join(df.Path, df.Name()))
if err == nil {
df.FileInfo = info
} else {
isInvalidLink = true
}
}
return isSymlink, isInvalidLink
}
func (f *FileInfo) getContent() error {
@@ -326,10 +379,8 @@ func (f *FileInfo) getContent() error {
if err != nil {
return nil
}
if !f.IsDetail {
if len(cByte) > 0 && DetectBinary(cByte) {
return buserr.New(constant.ErrFileCanNotRead)
}
if len(cByte) > 0 && DetectBinary(cByte) {
return buserr.New(constant.ErrFileCanNotRead)
}
f.Content = string(cByte)
return nil
+8
View File
@@ -7,6 +7,10 @@ import (
"net"
"net/http"
"time"
"github.com/1Panel-dev/1Panel/backend/app/model"
"github.com/1Panel-dev/1Panel/backend/buserr"
"github.com/1Panel-dev/1Panel/backend/constant"
)
func RemoveTamper(website string) {}
@@ -27,3 +31,7 @@ func LoadRequestTransport() *http.Transport {
func LoadGpuInfo() []interface{} {
return nil
}
func StartClam(startClam model.Clam, isUpdate bool) (int, error) {
return 0, buserr.New(constant.ErrXpackNotFound)
}
+76 -4
View File
@@ -11500,6 +11500,58 @@ const docTemplate = `{
}
}
},
"/toolbox/clam/status/update": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "修改扫描规则状态",
"consumes": [
"application/json"
],
"tags": [
"Clam"
],
"summary": "Update clam status",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.ClamUpdateStatus"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [
{
"db": "clams",
"input_column": "id",
"input_value": "id",
"isList": false,
"output_column": "name",
"output_value": "name"
}
],
"bodyKeys": [
"id",
"status"
],
"formatEN": "change the status of clam [name] to [status].",
"formatZH": "修改扫描规则 [name] 状态为 [status]",
"paramKeys": []
}
}
},
"/toolbox/clam/update": {
"post": {
"security": [
@@ -15570,6 +15622,12 @@ const docTemplate = `{
},
"path": {
"type": "string"
},
"spec": {
"type": "string"
},
"status": {
"type": "string"
}
}
},
@@ -15665,6 +15723,20 @@ const docTemplate = `{
},
"path": {
"type": "string"
},
"spec": {
"type": "string"
}
}
},
"dto.ClamUpdateStatus": {
"type": "object",
"properties": {
"id": {
"type": "integer"
},
"status": {
"type": "string"
}
}
},
@@ -18468,7 +18540,7 @@ const docTemplate = `{
"type": "string",
"enum": [
"name",
"status",
"state",
"created_at"
]
},
@@ -22601,7 +22673,8 @@ const docTemplate = `{
"primary_domain",
"type",
"status",
"created_at"
"created_at",
"expire_date"
]
},
"page": {
@@ -22619,8 +22692,7 @@ const docTemplate = `{
"type": "object",
"required": [
"id",
"primaryDomain",
"webSiteGroupID"
"primaryDomain"
],
"properties": {
"IPV6": {
+76 -4
View File
@@ -11493,6 +11493,58 @@
}
}
},
"/toolbox/clam/status/update": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "修改扫描规则状态",
"consumes": [
"application/json"
],
"tags": [
"Clam"
],
"summary": "Update clam status",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.ClamUpdateStatus"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [
{
"db": "clams",
"input_column": "id",
"input_value": "id",
"isList": false,
"output_column": "name",
"output_value": "name"
}
],
"bodyKeys": [
"id",
"status"
],
"formatEN": "change the status of clam [name] to [status].",
"formatZH": "修改扫描规则 [name] 状态为 [status]",
"paramKeys": []
}
}
},
"/toolbox/clam/update": {
"post": {
"security": [
@@ -15563,6 +15615,12 @@
},
"path": {
"type": "string"
},
"spec": {
"type": "string"
},
"status": {
"type": "string"
}
}
},
@@ -15658,6 +15716,20 @@
},
"path": {
"type": "string"
},
"spec": {
"type": "string"
}
}
},
"dto.ClamUpdateStatus": {
"type": "object",
"properties": {
"id": {
"type": "integer"
},
"status": {
"type": "string"
}
}
},
@@ -18461,7 +18533,7 @@
"type": "string",
"enum": [
"name",
"status",
"state",
"created_at"
]
},
@@ -22594,7 +22666,8 @@
"primary_domain",
"type",
"status",
"created_at"
"created_at",
"expire_date"
]
},
"page": {
@@ -22612,8 +22685,7 @@
"type": "object",
"required": [
"id",
"primaryDomain",
"webSiteGroupID"
"primaryDomain"
],
"properties": {
"IPV6": {
+49 -2
View File
@@ -243,6 +243,10 @@ definitions:
type: string
path:
type: string
spec:
type: string
status:
type: string
type: object
dto.ClamDelete:
properties:
@@ -305,6 +309,15 @@ definitions:
type: string
path:
type: string
spec:
type: string
type: object
dto.ClamUpdateStatus:
properties:
id:
type: integer
status:
type: string
type: object
dto.Clean:
properties:
@@ -2198,7 +2211,7 @@ definitions:
orderBy:
enum:
- name
- status
- state
- created_at
type: string
page:
@@ -4974,6 +4987,7 @@ definitions:
- type
- status
- created_at
- expire_date
type: string
page:
type: integer
@@ -5004,7 +5018,6 @@ definitions:
required:
- id
- primaryDomain
- webSiteGroupID
type: object
request.WebsiteUpdateDir:
properties:
@@ -12767,6 +12780,40 @@ paths:
summary: Page clam
tags:
- Clam
/toolbox/clam/status/update:
post:
consumes:
- application/json
description: 修改扫描规则状态
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.ClamUpdateStatus'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
summary: Update clam status
tags:
- Clam
x-panel-log:
BeforeFunctions:
- db: clams
input_column: id
input_value: id
isList: false
output_column: name
output_value: name
bodyKeys:
- id
- status
formatEN: change the status of clam [name] to [status].
formatZH: 修改扫描规则 [name] 状态为 [status]
paramKeys: []
/toolbox/clam/update:
post:
consumes:
-1
View File
@@ -30,7 +30,6 @@
"@highlightjs/vue-plugin": "^2.1.0",
"@vue-office/docx": "^1.6.2",
"@vue-office/excel": "^1.7.8",
"@vue-office/pdf": "^2.0.2",
"@vueuse/core": "^8.9.4",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.5.0",
+3
View File
@@ -24,6 +24,9 @@ export namespace Cronjob {
backupAccounts: string;
defaultDownload: string;
backupAccountList: Array<string>;
appIdList: Array<string>;
websiteList: Array<string>;
dbNameList: Array<string>;
retainCopies: number;
status: string;
secret: string;
+9
View File
@@ -1,4 +1,5 @@
import { ReqPage } from '.';
import { Cronjob } from './cronjob';
export namespace Toolbox {
export interface DeviceBaseInfo {
@@ -129,10 +130,14 @@ export namespace Toolbox {
export interface ClamInfo {
id: number;
name: string;
status: string;
path: string;
infectedStrategy: string;
infectedDir: string;
lastHandleDate: string;
hasSpec: boolean;
spec: string;
specObj: Cronjob.SpecObj;
description: string;
}
export interface ClamCreate {
@@ -140,6 +145,8 @@ export namespace Toolbox {
path: string;
infectedStrategy: string;
infectedDir: string;
spec: string;
specObj: Cronjob.SpecObj;
description: string;
}
export interface ClamUpdate {
@@ -148,6 +155,8 @@ export namespace Toolbox {
path: string;
infectedStrategy: string;
infectedDir: string;
spec: string;
specObj: Cronjob.SpecObj;
description: string;
}
export interface ClamSearchLog extends ReqPage {
+1 -1
View File
@@ -82,7 +82,7 @@ export const DownloadFile = (params: File.FileDownload) => {
};
export const ComputeDirSize = (params: File.DirSizeReq) => {
return http.post<File.DirSizeRes>('files/size', params);
return http.post<File.DirSizeRes>('files/size', params, TimeoutEnum.T_5M);
};
export const FileKeys = () => {
+3
View File
@@ -138,6 +138,9 @@ export const createClam = (params: Toolbox.ClamCreate) => {
export const updateClam = (params: Toolbox.ClamUpdate) => {
return http.post(`/toolbox/clam/update`, params);
};
export const updateClamStatus = (id: number, status: string) => {
return http.post(`/toolbox/clam/status/update`, { id: id, status: status });
};
export const deleteClam = (params: { ids: number[]; removeRecord: boolean; removeInfected: boolean }) => {
return http.post(`/toolbox/clam/del`, params);
};
@@ -74,7 +74,7 @@ const submit = async (formEl: FormInstance | undefined) => {
}
localStorage.setItem('VscodeConnectInfo', JSON.stringify(addForm));
dialogVisible.value = false;
const vscodeUrl = `vscode://vscode-remote/ssh-remote+${addForm.username}@${addForm.host}${addForm.path}?windowId=_blank`;
const vscodeUrl = `vscode://vscode-remote/ssh-remote+${addForm.username}@${addForm.host}:${addForm.port}${addForm.path}?windowId=_blank`;
window.open(vscodeUrl);
});
};
+15
View File
@@ -69,6 +69,7 @@ const message = {
createNewFolder: 'Create new folder',
createNewFile: 'Create new file',
helpDoc: 'Help Document',
unbind: 'UnBind',
},
search: {
timeStart: 'Time start',
@@ -437,6 +438,11 @@ const message = {
remote: 'Remote',
remoteDB: 'Remote DB',
createRemoteDB: 'Create Remote Server',
unBindRemoteDB: 'Unbind remote server',
unBindForce: 'Force unbind',
unBindForceHelper: 'Ignore all errors during the unbinding process to ensure the final operation is successful',
unBindRemoteHelper:
'Unbinding the remote database will only remove the binding relationship and will not directly delete the remote database',
editRemoteDB: 'Edit Remote Server',
localDB: 'Local DB',
address: 'DB address',
@@ -1077,6 +1083,13 @@ const message = {
},
clam: {
clam: 'Virus Scan',
cron: 'Scheduled scan',
cronHelper: 'Professional version supports scheduled scan feature',
specErr: 'Execution schedule format error, please check and retry!',
disableMsg:
'Stopping scheduled execution will prevent this scan task from running automatically. Do you want to continue?',
enableMsg:
'Enabling scheduled execution will allow this scan task to run automatically at regular intervals. Do you want to continue?',
showFresh: 'Show Virus Database Service',
hideFresh: 'Hide Virus Database Service',
clamHelper:
@@ -1262,6 +1275,7 @@ const message = {
noEdit: 'The file has not been modified, no need to do this!',
noNameFolder: 'Untitled Folder',
noNameFile: 'Untitled File',
minimap: 'Code Mini Map',
},
ssh: {
autoStart: 'Auto Start',
@@ -1571,6 +1585,7 @@ const message = {
recoverDetail: 'Recover detail',
createSnapshot: 'Create Snapshot',
importSnapshot: 'Sync Snapshot',
importHelper: 'Snapshot directory:',
recover: 'Recover',
lastRecoverAt: 'Last recovery time',
lastRollbackAt: 'Last rollback time',
+12
View File
@@ -68,6 +68,7 @@ const message = {
createNewFolder: '新建資料夾',
createNewFile: '新建檔案',
helpDoc: '幫助文档',
unbind: '解綁',
},
search: {
timeStart: '開始時間',
@@ -424,6 +425,10 @@ const message = {
remote: '遠程',
remoteDB: '遠程服務器',
createRemoteDB: '添加遠程服務器',
unBindRemoteDB: '解綁遠程服務器',
unBindForce: '強制解綁',
unBindForceHelper: '忽略解綁過程中的所有錯誤,確保最終操作成功',
unBindRemoteHelper: '解綁遠程數據庫只會刪除綁定關係,不會直接刪除遠程數據庫',
editRemoteDB: '編輯遠程服務器',
localDB: '本地數據庫',
address: '數據庫地址',
@@ -1019,6 +1024,11 @@ const message = {
},
clam: {
clam: '病毒掃描',
cron: '定時掃描',
cronHelper: '專業版支持定時掃描功能',
specErr: '執行周期格式錯誤,請檢查後重試!',
disableMsg: '停止定時執行會導致該掃描任務不再自動執行。是否繼續?',
enableMsg: '啟用定時執行會讓該掃描任務定期自動執行。是否繼續?',
showFresh: '顯示病毒庫服務',
hideFresh: '隱藏病毒庫服務',
clamHelper:
@@ -1199,6 +1209,7 @@ const message = {
noEdit: '檔案未修改,無需此操作!',
noNameFolder: '未命名資料夾',
noNameFile: '未命名檔案',
minimap: '縮略圖',
},
ssh: {
autoStart: '開機自啟',
@@ -1390,6 +1401,7 @@ const message = {
recoverDetail: '恢復詳情',
createSnapshot: '創建快照',
importSnapshot: '同步快照',
importHelper: '快照文件目錄:',
recover: '恢復',
lastRecoverAt: '上次恢復時間',
lastRollbackAt: '上次回滾時間',
+12
View File
@@ -68,6 +68,7 @@ const message = {
createNewFolder: '新建文件夹',
createNewFile: '新建文件',
helpDoc: '帮助文档',
unbind: '解绑',
},
search: {
timeStart: '开始时间',
@@ -424,6 +425,10 @@ const message = {
remote: '远程',
remoteDB: '远程服务器',
createRemoteDB: '添加远程服务器',
unBindRemoteDB: '解绑远程服务器',
unBindForce: '强制解绑',
unBindForceHelper: '忽略解绑过程中的所有错误,确保最终操作成功',
unBindRemoteHelper: '解绑远程数据库只会删除绑定关系,不会直接删除远程数据库',
editRemoteDB: '编辑远程服务器',
localDB: '本地数据库',
address: '数据库地址',
@@ -1020,6 +1025,11 @@ const message = {
},
clam: {
clam: '病毒扫描',
cron: '定时扫描',
cronHelper: '专业版支持定时扫描功能 ',
specErr: '执行周期格式错误,请检查后重试!',
disableMsg: '停止定时执行会导致该扫描任务不再自动执行。是否继续?',
enableMsg: '启用定时执行会让该扫描任务定期自动执行。是否继续?',
showFresh: '显示病毒库服务',
hideFresh: '隐藏病毒库服务',
clamHelper:
@@ -1201,6 +1211,7 @@ const message = {
noEdit: '文件未修改,无需此操作!',
noNameFolder: '未命名文件夹',
noNameFile: '未命名文件',
minimap: '缩略图',
},
ssh: {
autoStart: '开机自启',
@@ -1392,6 +1403,7 @@ const message = {
recoverDetail: '恢复详情',
createSnapshot: '创建快照',
importSnapshot: '同步快照',
importHelper: '快照文件目录:',
recover: '恢复',
lastRecoverAt: '上次恢复时间',
lastRollbackAt: '上次回滚时间',
+4
View File
@@ -447,6 +447,10 @@ html {
background-color: #e5eefd;
}
.monaco-editor-tree-light .el-tree-node.is-current > .el-tree-node__content {
background-color: #e5eefd;
}
.monaco-editor-tree-dark .el-tree-node__content:hover {
background-color: #111417;
}
@@ -241,6 +241,9 @@ const openInstall = (app: App.App) => {
case 'java':
router.push({ path: '/websites/runtimes/java' });
break;
case 'go':
router.push({ path: '/websites/runtimes/go' });
break;
default:
const params = {
app: app,
+37 -6
View File
@@ -186,9 +186,9 @@
<el-form-item
v-if="dialogData.rowData!.type === 'website' || dialogData.rowData!.type === 'cutWebsiteLog'"
:label="dialogData.rowData!.type === 'website' ? $t('cronjob.website'):$t('website.website')"
prop="website"
prop="websiteList"
>
<el-select class="selectClass" v-model="dialogData.rowData!.website">
<el-select class="selectClass" multiple v-model="dialogData.rowData!.websiteList">
<el-option
:disabled="websiteOptions.length === 0"
:label="$t('commons.table.all')"
@@ -212,8 +212,8 @@
</el-form-item>
<div v-if="dialogData.rowData!.type === 'app'">
<el-form-item :label="$t('cronjob.app')" prop="appID">
<el-select class="selectClass" clearable v-model="dialogData.rowData!.appID">
<el-form-item :label="$t('cronjob.app')" prop="appIdList">
<el-select class="selectClass" multiple clearable v-model="dialogData.rowData!.appIdList">
<el-option
:disabled="appOptions.length === 0"
:label="$t('commons.table.all')"
@@ -239,8 +239,8 @@
<el-radio value="postgresql">PostgreSQL</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item :label="$t('cronjob.database')" prop="dbName">
<el-select class="selectClass" clearable v-model="dialogData.rowData!.dbName">
<el-form-item :label="$t('cronjob.database')" prop="dbNameList">
<el-select class="selectClass" multiple clearable v-model="dialogData.rowData!.dbNameList">
<el-option
:disabled="dbInfo.dbs.length === 0"
:label="$t('commons.table.all')"
@@ -378,6 +378,7 @@ import { listContainer } from '@/api/modules/container';
import { Database } from '@/api/interface/database';
import { ListAppInstalled } from '@/api/modules/app';
import { loadDefaultSpec, specOptions, transObjToSpec, transSpecToObj, weekOptions } from './../helper';
const router = useRouter();
interface DialogProps {
@@ -385,6 +386,7 @@ interface DialogProps {
rowData?: Cronjob.CronjobInfo;
getTableList?: () => Promise<any>;
}
const title = ref<string>('');
const drawerVisible = ref(false);
const dialogData = ref<DialogProps>({
@@ -408,6 +410,15 @@ const acceptParams = (params: DialogProps): void => {
if (dialogData.value.rowData.backupAccounts) {
dialogData.value.rowData.backupAccountList = dialogData.value.rowData.backupAccounts.split(',');
}
if (dialogData.value.rowData.appID) {
dialogData.value.rowData.appIdList = dialogData.value.rowData.appID.split(',');
}
if (dialogData.value.rowData.website) {
dialogData.value.rowData.websiteList = dialogData.value.rowData.website.split(',');
}
if (dialogData.value.rowData.dbName) {
dialogData.value.rowData.dbNameList = dialogData.value.rowData.dbName.split(',');
}
dialogData.value.rowData!.command = dialogData.value.rowData!.command || 'sh';
dialogData.value.rowData!.isCustom =
dialogData.value.rowData!.command !== 'sh' &&
@@ -718,6 +729,17 @@ const onSubmit = async (formEl: FormInstance | undefined) => {
specs.push(itemSpec);
}
dialogData.value.rowData.backupAccounts = dialogData.value.rowData.backupAccountList.join(',');
if (dialogData.value.rowData.appIdList) {
dialogData.value.rowData.appID = dialogData.value.rowData.appIdList.join(',');
}
if (dialogData.value.rowData.websiteList) {
dialogData.value.rowData.website = dialogData.value.rowData.websiteList.join(',');
}
if (dialogData.value.rowData.dbNameList) {
dialogData.value.rowData.dbName = dialogData.value.rowData.dbNameList.join(',');
}
dialogData.value.rowData.spec = specs.join(',');
if (!formEl) return;
formEl.validate(async (valid) => {
@@ -750,40 +772,49 @@ defineExpose({
.specClass {
width: 20% !important;
margin-left: 20px;
.append {
width: 20px;
}
}
@media only screen and (max-width: 1000px) {
.specClass {
width: 100% !important;
margin-top: 20px;
margin-left: 0;
.append {
width: 43px;
}
}
}
.specTypeClass {
width: 22% !important;
}
@media only screen and (max-width: 1000px) {
.specTypeClass {
width: 100% !important;
}
}
.selectClass {
width: 100%;
}
.tagClass {
float: right;
margin-right: 10px;
font-size: 12px;
margin-top: 5px;
}
.logText {
line-height: 22px;
font-size: 12px;
.link {
font-size: 12px;
margin-top: -3px;
@@ -1,15 +1,15 @@
<template>
<el-dialog
v-model="dialogVisible"
:title="$t('commons.button.delete') + ' - ' + deleteMysqlReq.database"
:title="$t('database.unBindRemoteDB') + ' - ' + deleteMysqlReq.database"
width="30%"
:close-on-click-modal="false"
>
<el-form ref="deleteForm" v-loading="loading" @submit.prevent>
<el-form-item>
<el-checkbox v-model="deleteMysqlReq.forceDelete" :label="$t('app.forceDelete')" />
<el-checkbox v-model="deleteMysqlReq.forceDelete" :label="$t('database.unBindForce')" />
<span class="input-help">
{{ $t('app.forceDeleteHelper') }}
{{ $t('database.unBindForceHelper') }}
</span>
</el-form-item>
<el-form-item>
@@ -18,25 +18,15 @@
{{ $t('database.deleteBackupHelper') }}
</span>
</el-form-item>
<el-form-item>
<div>
<span style="font-size: 12px">{{ $t('database.delete') }}</span>
<span style="font-size: 12px; color: red; font-weight: 500">{{ deleteMysqlReq.database }}</span>
<span style="font-size: 12px">{{ $t('database.deleteHelper') }}</span>
</div>
<el-input v-model="delMysqlInfo" :placeholder="deleteMysqlReq.database"></el-input>
</el-form-item>
<span style="font-size: 12px">{{ $t('database.unBindRemoteHelper') }}</span>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false" :disabled="loading">
{{ $t('commons.button.cancel') }}
</el-button>
<el-button
type="primary"
@click="submit"
:disabled="delMysqlInfo != deleteMysqlReq.database || loading"
>
<el-button type="primary" @click="submit" :disabled="loading">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
@@ -58,7 +48,6 @@ let deleteMysqlReq = ref({
});
let dialogVisible = ref(false);
let loading = ref(false);
let delMysqlInfo = ref('');
const deleteForm = ref<FormInstance>();
@@ -70,7 +59,6 @@ interface DialogProps {
const emit = defineEmits<{ (e: 'search'): void }>();
const acceptParams = async (prop: DialogProps) => {
delMysqlInfo.value = '';
deleteMysqlReq.value = {
id: prop.id,
database: prop.database,
@@ -86,7 +74,7 @@ const submit = async () => {
.then(() => {
loading.value = false;
emit('search');
MsgSuccess(i18n.global.t('commons.msg.deleteSuccess'));
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
dialogVisible.value = false;
})
.catch(() => {
@@ -163,7 +163,7 @@ const buttons = [
},
},
{
label: i18n.global.t('commons.button.delete'),
label: i18n.global.t('commons.button.unbind'),
click: (row: Database.DatabaseInfo) => {
onDelete(row);
},
@@ -1,15 +1,15 @@
<template>
<el-dialog
v-model="dialogVisible"
:title="$t('commons.button.delete') + ' - ' + deleteReq.database"
:title="$t('database.unBindRemoteDB') + ' - ' + deleteReq.database"
width="30%"
:close-on-click-modal="false"
>
<el-form ref="deleteForm" v-loading="loading" @submit.prevent>
<el-form-item>
<el-checkbox v-model="deleteReq.forceDelete" :label="$t('app.forceDelete')" />
<el-checkbox v-model="deleteReq.forceDelete" :label="$t('database.unBindForce')" />
<span class="input-help">
{{ $t('app.forceDeleteHelper') }}
{{ $t('database.unBindForceHelper') }}
</span>
</el-form-item>
<el-form-item>
@@ -18,21 +18,15 @@
{{ $t('database.deleteBackupHelper') }}
</span>
</el-form-item>
<el-form-item>
<div>
<span style="font-size: 12px">{{ $t('database.delete') }}</span>
<span style="font-size: 12px; color: red; font-weight: 500">{{ deleteReq.database }}</span>
<span style="font-size: 12px">{{ $t('database.deleteHelper') }}</span>
</div>
<el-input v-model="deleteInfo" :placeholder="deleteReq.database"></el-input>
</el-form-item>
<span style="font-size: 12px">{{ $t('database.unBindRemoteHelper') }}</span>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false" :disabled="loading">
{{ $t('commons.button.cancel') }}
</el-button>
<el-button type="primary" @click="submit" :disabled="deleteInfo != deleteReq.database || loading">
<el-button type="primary" @click="submit" :disabled="loading">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
@@ -54,7 +48,6 @@ let deleteReq = ref({
});
let dialogVisible = ref(false);
let loading = ref(false);
let deleteInfo = ref('');
const deleteForm = ref<FormInstance>();
@@ -66,7 +59,6 @@ interface DialogProps {
const emit = defineEmits<{ (e: 'search'): void }>();
const acceptParams = async (prop: DialogProps) => {
deleteInfo.value = '';
deleteReq.value = {
id: prop.id,
database: prop.database,
@@ -82,7 +74,7 @@ const submit = async () => {
.then(() => {
loading.value = false;
emit('search');
MsgSuccess(i18n.global.t('commons.msg.deleteSuccess'));
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
dialogVisible.value = false;
})
.catch(() => {
@@ -163,7 +163,7 @@ const buttons = [
},
},
{
label: i18n.global.t('commons.button.delete'),
label: i18n.global.t('commons.button.unbind'),
click: (row: Database.DatabaseInfo) => {
onDelete(row);
},
@@ -1,15 +1,15 @@
<template>
<el-dialog
v-model="dialogVisible"
:title="$t('commons.button.delete') + ' - ' + deleteMysqlReq.database"
:title="$t('database.unBindRemoteDB') + ' - ' + deleteMysqlReq.database"
width="30%"
:close-on-click-modal="false"
>
<el-form ref="deleteForm" v-loading="loading" @submit.prevent>
<el-form-item>
<el-checkbox v-model="deleteMysqlReq.forceDelete" :label="$t('app.forceDelete')" />
<el-checkbox v-model="deleteMysqlReq.forceDelete" :label="$t('database.unBindForce')" />
<span class="input-help">
{{ $t('app.forceDeleteHelper') }}
{{ $t('database.unBindForceHelper') }}
</span>
</el-form-item>
<el-form-item>
@@ -18,25 +18,15 @@
{{ $t('database.deleteBackupHelper') }}
</span>
</el-form-item>
<el-form-item>
<div>
<span style="font-size: 12px">{{ $t('database.delete') }}</span>
<span style="font-size: 12px; color: red; font-weight: 500">{{ deleteMysqlReq.database }}</span>
<span style="font-size: 12px">{{ $t('database.deleteHelper') }}</span>
</div>
<el-input v-model="delMysqlInfo" :placeholder="deleteMysqlReq.database"></el-input>
</el-form-item>
<span style="font-size: 12px">{{ $t('database.unBindRemoteHelper') }}</span>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false" :disabled="loading">
{{ $t('commons.button.cancel') }}
</el-button>
<el-button
type="primary"
@click="submit"
:disabled="delMysqlInfo != deleteMysqlReq.database || loading"
>
<el-button type="primary" @click="submit" :disabled="loading">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
@@ -58,7 +48,6 @@ let deleteMysqlReq = ref({
});
let dialogVisible = ref(false);
let loading = ref(false);
let delMysqlInfo = ref('');
const deleteForm = ref<FormInstance>();
@@ -70,7 +59,6 @@ interface DialogProps {
const emit = defineEmits<{ (e: 'search'): void }>();
const acceptParams = async (prop: DialogProps) => {
delMysqlInfo.value = '';
deleteMysqlReq.value = {
id: prop.id,
database: prop.database,
@@ -86,7 +74,7 @@ const submit = async () => {
.then(() => {
loading.value = false;
emit('search');
MsgSuccess(i18n.global.t('commons.msg.deleteSuccess'));
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
dialogVisible.value = false;
})
.catch(() => {
@@ -163,7 +163,7 @@ const buttons = [
},
},
{
label: i18n.global.t('commons.button.delete'),
label: i18n.global.t('commons.button.unbind'),
click: (row: Database.DatabaseInfo) => {
onDelete(row);
},
@@ -57,6 +57,12 @@
<el-option :label="$t('commons.button.disable')" value="off"></el-option>
</el-select>
</el-form-item>
<el-form-item :label="$t('file.minimap')">
<el-select v-model="config.minimap" @change="changeMinimap()" class="p-w-100">
<el-option :label="$t('commons.button.enable')" :value="true"></el-option>
<el-option :label="$t('commons.button.disable')" :value="false"></el-option>
</el-select>
</el-form-item>
</el-form>
</div>
<div v-loading="loading">
@@ -209,6 +215,7 @@ interface EditorConfig {
language: string;
eol: number;
wordWrap: WordWrapOptions;
minimap: boolean;
}
interface TreeNode {
@@ -227,6 +234,7 @@ const loading = ref(false);
const fileName = ref('');
const codeThemeKey = 'code-theme';
const warpKey = 'code-warp';
const minimapKey = 'code-minimap';
const directoryPath = ref('');
const fileExtension = ref('');
const baseDir = ref();
@@ -261,6 +269,7 @@ const config = reactive<EditorConfig>({
language: 'plaintext',
eol: monaco.editor.EndOfLineSequence.LF,
wordWrap: 'on',
minimap: false,
});
const eols = [
@@ -403,6 +412,15 @@ const changeWarp = () => {
});
};
const changeMinimap = () => {
localStorage.setItem(minimapKey, JSON.stringify(config.minimap));
editor.updateOptions({
minimap: {
enabled: config.minimap,
},
});
};
const initEditor = () => {
if (editor) {
editor.dispose();
@@ -418,6 +436,9 @@ const initEditor = () => {
roundedSelection: false,
overviewRulerBorder: false,
wordWrap: config.wordWrap,
minimap: {
enabled: config.minimap,
},
});
if (editor.getModel().getValue() === '') {
let defaultContent = '\n\n\n\n';
@@ -470,6 +491,7 @@ const acceptParams = (props: EditProps) => {
config.eol = monaco.editor.EndOfLineSequence.LF;
config.theme = localStorage.getItem(codeThemeKey) || 'vs-dark';
config.wordWrap = (localStorage.getItem(warpKey) as WordWrapOptions) || 'on';
config.minimap = localStorage.getItem(minimapKey) !== null ? localStorage.getItem(minimapKey) === 'true' : true;
open.value = true;
};
@@ -11,7 +11,7 @@
<el-descriptions-item :label="$t('file.path')">{{ data.path }}</el-descriptions-item>
<el-descriptions-item :label="$t('file.size')">
<span v-if="data.isDir">
<el-button type="primary" link small @click="getDirSize(data)">
<el-button type="primary" link small @click="getDirSize(data)" :loading="loading">
<span v-if="data.dirSize == undefined">
{{ $t('file.calculate') }}
</span>
@@ -260,7 +260,13 @@
<el-table-column :label="$t('file.size')" prop="size" max-width="50" sortable>
<template #default="{ row, $index }">
<span v-if="row.isDir">
<el-button type="primary" link small @click="getDirSize(row, $index)">
<el-button
type="primary"
link
small
@click="getDirSize(row, $index)"
:loading="btnLoading.includes($index)"
>
<span v-if="row.dirSize == undefined">
{{ $t('file.calculate') }}
</span>
@@ -381,6 +387,7 @@ const initData = () => ({
});
let req = reactive(initData());
let loading = ref(false);
let btnLoading = ref([]);
const paths = ref<FilePaths[]>([]);
let pathWidth = ref(0);
const history: string[] = [];
@@ -631,7 +638,7 @@ const getDirSize = async (row: any, index: number) => {
const req = {
path: row.path,
};
loading.value = true;
btnLoading.value.push(index);
await ComputeDirSize(req)
.then(async (res) => {
let newData = [...data.value];
@@ -639,7 +646,7 @@ const getDirSize = async (row: any, index: number) => {
data.value = newData;
})
.finally(() => {
loading.value = false;
btnLoading.value = btnLoading.value.filter((item) => item !== index);
});
};
@@ -688,7 +695,7 @@ const openDeCompress = (item: File.File) => {
const openView = (item: File.File) => {
const fileType = getFileType(item.extension);
const previewTypes = ['image', 'video', 'audio', 'pdf', 'word', 'excel'];
const previewTypes = ['image', 'video', 'audio', 'word', 'excel'];
if (previewTypes.includes(fileType)) {
return openPreview(item, fileType);
}
@@ -35,15 +35,6 @@
<audio v-else-if="fileType === 'audio'" :src="fileUrl" controls></audio>
<vue-office-pdf
v-else-if="fileType === 'pdf'"
:src="fileUrl"
:style="isFullscreen ? 'height: 90vh' : 'height: 80vh'"
class="w-full"
@rendered="renderedHandler"
@error="errorHandler"
/>
<vue-office-docx
v-else-if="fileType === 'word'"
:src="fileUrl"
@@ -73,7 +64,6 @@ import { ref } from 'vue';
import { Close, FullScreen } from '@element-plus/icons-vue';
import VueOfficeDocx from '@vue-office/docx';
import VueOfficeExcel from '@vue-office/excel';
import VueOfficePdf from '@vue-office/pdf';
import '@vue-office/docx/lib/index.css';
import '@vue-office/excel/lib/index.css';
import { MsgError } from '@/utils/message';
@@ -1,6 +1,7 @@
<template>
<el-drawer
v-model="open"
:destroy-on-close="true"
:before-close="handleClose"
size="50%"
:close-on-click-modal="false"
@@ -16,6 +16,10 @@
:label="item.label"
/>
</el-select>
<div v-if="form.from === 'LOCAL'">
<span class="import-help">{{ $t('setting.importHelper') }}</span>
<span @click="toFolder()" class="import-link-help">{{ backupPath }}</span>
</div>
</el-form-item>
<el-form-item :label="$t('commons.table.name')" prop="names">
<el-select style="width: 100%" v-model="form.names" multiple clearable>
@@ -57,6 +61,7 @@ import { snapshotImport } from '@/api/modules/setting';
import { getBackupList, getFilesFromBackup } from '@/api/modules/setting';
import { Rules } from '@/global/form-rules';
import { MsgSuccess } from '@/utils/message';
import router from '@/routers';
const drawerVisible = ref(false);
const loading = ref();
@@ -65,6 +70,7 @@ const formRef = ref();
const backupOptions = ref();
const fileNames = ref();
const existNames = ref();
const backupPath = ref('');
const form = reactive({
from: '',
@@ -102,6 +108,9 @@ const checkDisable = (val: string) => {
}
return false;
};
const toFolder = async () => {
router.push({ path: '/hosts/files', query: { path: backupPath.value } });
};
const submitImport = async (formEl: FormInstance | undefined) => {
loading.value = true;
@@ -131,6 +140,10 @@ const loadBackups = async () => {
if (item.id !== 0) {
backupOptions.value.push({ label: i18n.global.t('setting.' + item.type), value: item.type });
}
if (item.type === 'LOCAL') {
item.varsJson = JSON.parse(item.vars);
backupPath.value = item.varsJson['dir'] + '/system_snapshot';
}
}
})
.catch(() => {
@@ -148,3 +161,18 @@ defineExpose({
acceptParams,
});
</script>
<style lang="scss" scoped>
.import-help {
font-size: 12px;
color: #8f959e;
}
.import-link-help {
color: $primary-color;
cursor: pointer;
}
.import-link-help:hover {
opacity: 0.6;
}
</style>
+11 -4
View File
@@ -405,10 +405,17 @@ const search = async () => {
page: paginationConfig.currentPage,
pageSize: paginationConfig.pageSize,
};
const res = await searchSnapshotPage(params);
cleanData.value = false;
data.value = res.data.items || [];
paginationConfig.total = res.data.total;
loading.value = true;
await searchSnapshotPage(params)
.then((res) => {
loading.value = false;
cleanData.value = false;
data.value = res.data.items || [];
paginationConfig.total = res.data.total;
})
.catch(() => {
loading.value = false;
});
};
onMounted(() => {
+73 -2
View File
@@ -56,6 +56,7 @@
:label="$t('commons.table.name')"
:min-width="60"
prop="name"
sortable
show-overflow-tooltip
>
<template #default="{ row }">
@@ -74,6 +75,47 @@
<el-button link type="primary" @click="toFolder(row.path)">{{ row.path }}</el-button>
</template>
</el-table-column>
<el-table-column
v-if="isProductPro"
:label="$t('commons.table.status')"
:min-width="70"
prop="status"
sortable
>
<template #default="{ row }">
<el-button
v-if="row.status === 'Enable'"
@click="onChangeStatus(row.id, 'disable')"
link
icon="VideoPlay"
type="success"
>
{{ $t('commons.status.enabled') }}
</el-button>
<el-button
v-if="row.status === 'Disable'"
icon="VideoPause"
link
type="danger"
@click="onChangeStatus(row.id, 'enable')"
>
{{ $t('commons.status.disabled') }}
</el-button>
<span v-if="row.status === ''">-</span>
</template>
</el-table-column>
<el-table-column
v-if="isProductPro"
:label="$t('cronjob.cronSpec')"
show-overflow-tooltip
:min-width="120"
>
<template #default="{ row }">
<span>
{{ row.spec !== '' ? transSpecToStr(row.spec) : '-' }}
</span>
</template>
</el-table-column>
<el-table-column
:label="$t('toolbox.clam.infectedDir')"
:min-width="120"
@@ -138,17 +180,22 @@
import { onMounted, reactive, ref } from 'vue';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
import { deleteClam, handleClamScan, searchClam, updateClam } from '@/api/modules/toolbox';
import { deleteClam, handleClamScan, searchClam, updateClam, updateClamStatus } from '@/api/modules/toolbox';
import OperateDialog from '@/views/toolbox/clam/operate/index.vue';
import LogDialog from '@/views/toolbox/clam/record/index.vue';
import ClamStatus from '@/views/toolbox/clam/status/index.vue';
import SettingDialog from '@/views/toolbox/clam/setting/index.vue';
import { Toolbox } from '@/api/interface/toolbox';
import router from '@/routers';
import { transSpecToStr } from '../../cronjob/helper';
import { GlobalStore } from '@/store';
import { storeToRefs } from 'pinia';
const loading = ref();
const selects = ref<any>([]);
const globalStore = GlobalStore();
const { isProductPro } = storeToRefs(globalStore);
const data = ref();
const paginationConfig = reactive({
cacheSizeKey: 'clam-page-size',
@@ -176,12 +223,16 @@ const clamStatus = ref({
isRunning: true,
});
const search = async () => {
const search = async (column?: any) => {
paginationConfig.orderBy = column?.order ? column.prop : paginationConfig.orderBy;
paginationConfig.order = column?.order ? column.order : paginationConfig.order;
loading.value = true;
let params = {
info: searchName.value,
page: paginationConfig.currentPage,
pageSize: paginationConfig.pageSize,
orderBy: paginationConfig.orderBy,
order: paginationConfig.order,
};
await searchClam(params)
.then((res) => {
@@ -218,6 +269,14 @@ const onOpenDialog = async (
title: string,
rowData: Partial<Toolbox.ClamInfo> = {
infectedStrategy: 'none',
specObj: {
specType: 'perDay',
week: 1,
day: 3,
hour: 1,
minute: 30,
second: 30,
},
},
) => {
let params = {
@@ -272,6 +331,18 @@ const onSubmitDelete = async () => {
});
};
const onChangeStatus = async (id: number, status: string) => {
ElMessageBox.confirm(i18n.global.t('toolbox.clam.' + status + 'Msg'), i18n.global.t('cronjob.changeStatus'), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
}).then(async () => {
let itemStatus = status === 'enable' ? 'Enable' : 'Disable';
await updateClamStatus(id, itemStatus);
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
search();
});
};
const buttons = [
{
label: i18n.global.t('commons.button.handle'),
@@ -50,6 +50,77 @@
</template>
</el-input>
</el-form-item>
<el-form-item prop="hasSpec">
<el-checkbox v-model="dialogData.rowData!.hasSpec" :label="$t('toolbox.clam.cron')" />
</el-form-item>
<el-form-item v-if="dialogData.rowData!.hasSpec && !isProductPro">
<span>{{ $t('toolbox.clam.cronHelper') }}</span>
<el-button link type="primary" @click="toUpload">
{{ $t('license.levelUpPro') }}
</el-button>
</el-form-item>
<el-form-item prop="spec" v-if="dialogData.rowData!.hasSpec && isProductPro">
<el-select
class="specTypeClass"
v-model="dialogData.rowData!.specObj.specType"
@change="changeSpecType()"
>
<el-option
v-for="item in specOptions"
:key="item.label"
:value="item.value"
:label="item.label"
/>
</el-select>
<el-select
v-if="dialogData.rowData!.specObj.specType === 'perWeek'"
class="specClass"
v-model="dialogData.rowData!.specObj.week"
>
<el-option
v-for="item in weekOptions"
:key="item.label"
:value="item.value"
:label="item.label"
/>
</el-select>
<el-input
v-if="hasDay(dialogData.rowData!.specObj)"
class="specClass"
v-model.number="dialogData.rowData!.specObj.day"
>
<template #append>
<div class="append">{{ $t('cronjob.day') }}</div>
</template>
</el-input>
<el-input
v-if="hasHour(dialogData.rowData!.specObj)"
class="specClass"
v-model.number="dialogData.rowData!.specObj.hour"
>
<template #append>
<div class="append">{{ $t('commons.units.hour') }}</div>
</template>
</el-input>
<el-input
v-if="dialogData.rowData!.specObj.specType !== 'perNSecond'"
class="specClass"
v-model.number="dialogData.rowData!.specObj.minute"
>
<template #append>
<div class="append">{{ $t('commons.units.minute') }}</div>
</template>
</el-input>
<el-input
v-if="dialogData.rowData!.specObj.specType === 'perNSecond'"
class="specClass"
v-model.number="dialogData.rowData!.specObj.second"
>
<template #append>
<div class="append">{{ $t('commons.units.second') }}</div>
</template>
</el-input>
</el-form-item>
<el-form-item :label="$t('commons.table.description')" prop="description">
<el-input type="textarea" :rows="3" clearable v-model="dialogData.rowData!.description" />
</el-form-item>
@@ -64,6 +135,7 @@
</el-button>
</span>
</template>
<LicenseImport ref="licenseRef" />
</el-drawer>
</template>
@@ -73,11 +145,18 @@ import { Rules } from '@/global/form-rules';
import FileList from '@/components/file-list/index.vue';
import i18n from '@/lang';
import { ElForm } from 'element-plus';
import LicenseImport from '@/components/license-import/index.vue';
import DrawerHeader from '@/components/drawer-header/index.vue';
import { MsgSuccess } from '@/utils/message';
import { MsgError, MsgSuccess } from '@/utils/message';
import { Toolbox } from '@/api/interface/toolbox';
import { createClam, updateClam } from '@/api/modules/toolbox';
import { specOptions, transObjToSpec, transSpecToObj, weekOptions } from '../../../cronjob/helper';
import { storeToRefs } from 'pinia';
import { GlobalStore } from '@/store';
const globalStore = GlobalStore();
const licenseRef = ref();
const { isProductPro } = storeToRefs(globalStore);
interface DialogProps {
title: string;
rowData?: Toolbox.ClamInfo;
@@ -92,6 +171,19 @@ const dialogData = ref<DialogProps>({
const acceptParams = (params: DialogProps): void => {
dialogData.value = params;
if (dialogData.value.rowData?.spec) {
dialogData.value.rowData.hasSpec = true;
dialogData.value.rowData.specObj = transSpecToObj(dialogData.value.rowData.spec);
} else {
dialogData.value.rowData.specObj = {
specType: 'perDay',
week: 1,
day: 3,
hour: 1,
minute: 30,
second: 30,
};
}
title.value = i18n.global.t('commons.button.' + dialogData.value.title);
drawerVisible.value = true;
};
@@ -101,9 +193,97 @@ const handleClose = () => {
drawerVisible.value = false;
};
const verifySpec = (rule: any, value: any, callback: any) => {
let item = dialogData.value.rowData!.specObj;
if (
!Number.isInteger(item.day) ||
!Number.isInteger(item.hour) ||
!Number.isInteger(item.minute) ||
!Number.isInteger(item.second) ||
!Number.isInteger(item.week)
) {
callback(new Error(i18n.global.t('toolbox.clam.specErr')));
return;
}
switch (item.specType) {
case 'perMonth':
if (
item.day < 0 ||
item.day > 31 ||
item.hour < 0 ||
item.hour > 23 ||
item.minute < 0 ||
item.minute > 59
) {
callback(new Error(i18n.global.t('toolbox.clam.specErr')));
return;
}
break;
case 'perNDay':
if (
item.day < 0 ||
item.day > 366 ||
item.hour < 0 ||
item.hour > 23 ||
item.minute < 0 ||
item.minute > 59
) {
callback(new Error(i18n.global.t('toolbox.clam.specErr')));
return;
}
break;
case 'perWeek':
if (
item.week < 0 ||
item.week > 6 ||
item.hour < 0 ||
item.hour > 23 ||
item.minute < 0 ||
item.minute > 59
) {
callback(new Error(i18n.global.t('toolbox.clam.specErr')));
return;
}
break;
case 'perDay':
if (item.hour < 0 || item.hour > 23 || item.minute < 0 || item.minute > 59) {
callback(new Error(i18n.global.t('toolbox.clam.specErr')));
return;
}
break;
case 'perNHour':
if (item.hour < 0 || item.hour > 8784 || item.minute < 0 || item.minute > 59) {
callback(new Error(i18n.global.t('toolbox.clam.specErr')));
return;
}
break;
case 'perHour':
if (item.minute < 0 || item.minute > 59) {
callback(new Error(i18n.global.t('toolbox.clam.specErr')));
return;
}
case 'perNMinute':
if (item.minute < 0 || item.minute > 527040) {
callback(new Error(i18n.global.t('toolbox.clam.specErr')));
return;
}
break;
case 'perNSecond':
if (item.second < 0 || item.second > 31622400) {
callback(new Error(i18n.global.t('toolbox.clam.specErr')));
return;
}
break;
}
callback();
};
const rules = reactive({
name: [Rules.simpleName],
path: [Rules.requiredInput, Rules.noSpace],
spec: [
{ validator: verifySpec, trigger: 'blur', required: true },
{ validator: verifySpec, trigger: 'change', required: true },
],
});
type FormInstance = InstanceType<typeof ElForm>;
@@ -120,12 +300,62 @@ const loadDir = async (path: string) => {
const loadInfectedDir = async (path: string) => {
dialogData.value.rowData!.infectedDir = path;
};
const hasDay = (item: any) => {
return item.specType === 'perMonth' || item.specType === 'perNDay';
};
const hasHour = (item: any) => {
return item.specType !== 'perHour' && item.specType !== 'perNMinute' && item.specType !== 'perNSecond';
};
const toUpload = () => {
licenseRef.value.acceptParams();
};
const changeSpecType = () => {
let item = dialogData.value.rowData!.specObj;
switch (item.specType) {
case 'perMonth':
case 'perNDay':
item.day = 3;
item.hour = 1;
item.minute = 30;
break;
case 'perWeek':
item.week = 1;
item.hour = 1;
item.minute = 30;
break;
case 'perDay':
case 'perNHour':
item.hour = 2;
item.minute = 30;
break;
case 'perHour':
case 'perNMinute':
item.minute = 30;
break;
case 'perNSecond':
item.second = 30;
break;
}
};
const onSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate(async (valid) => {
if (!valid) return;
loading.value = true;
let spec = '';
let item = dialogData.value.rowData.specObj;
if (dialogData.value.rowData!.hasSpec) {
spec = transObjToSpec(item.specType, item.week, item.day, item.hour, item.minute, item.second);
if (spec === '') {
MsgError(i18n.global.t('cronjob.cronSpecHelper'));
return;
}
}
dialogData.value.rowData.spec = spec;
if (dialogData.value.title === 'edit') {
await updateClam(dialogData.value.rowData)
.then(() => {
@@ -158,3 +388,31 @@ defineExpose({
acceptParams,
});
</script>
<style scoped lang="scss">
.specClass {
width: 20% !important;
margin-left: 20px;
.append {
width: 20px;
}
}
@media only screen and (max-width: 1000px) {
.specClass {
width: 100% !important;
margin-top: 20px;
margin-left: 0;
.append {
width: 43px;
}
}
}
.specTypeClass {
width: 22% !important;
}
@media only screen and (max-width: 1000px) {
.specTypeClass {
width: 100% !important;
}
}
</style>
@@ -1,5 +1,11 @@
<template>
<el-drawer :close-on-click-modal="false" :close-on-press-escape="false" v-model="open" size="30%">
<el-drawer
:destroy-on-close="true"
:close-on-click-modal="false"
:close-on-press-escape="false"
v-model="open"
size="30%"
>
<template #header>
<DrawerHeader
:header="process.operate == 'create' ? $t('commons.button.create') : $t('commons.button.edit')"
@@ -1,5 +1,11 @@
<template>
<el-drawer :close-on-click-modal="false" :close-on-press-escape="false" v-model="open" size="50%">
<el-drawer
:destroy-on-close="true"
:close-on-click-modal="false"
:close-on-press-escape="false"
v-model="open"
size="50%"
>
<template #header>
<DrawerHeader
:header="$t('runtime.' + mode)"
@@ -1,5 +1,11 @@
<template>
<el-drawer :close-on-click-modal="false" :close-on-press-escape="false" v-model="open" size="50%">
<el-drawer
:destroy-on-close="true"
:close-on-click-modal="false"
:close-on-press-escape="false"
v-model="open"
size="50%"
>
<template #header>
<DrawerHeader
:header="$t('runtime.' + mode)"
@@ -1,5 +1,11 @@
<template>
<el-drawer :close-on-click-modal="false" :close-on-press-escape="false" v-model="open" size="50%">
<el-drawer
:destroy-on-close="true"
:close-on-click-modal="false"
:close-on-press-escape="false"
v-model="open"
size="50%"
>
<template #header>
<DrawerHeader
:header="$t('runtime.' + mode)"
@@ -5,6 +5,7 @@
:close-on-click-modal="false"
width="40%"
:before-close="handleClose"
:destroy-on-close="true"
>
<el-row v-loading="loading">
<el-col :span="22" :offset="1">
@@ -1,5 +1,11 @@
<template>
<el-drawer :close-on-click-modal="false" :close-on-press-escape="false" v-model="open" size="50%">
<el-drawer
:destroy-on-close="true"
:close-on-click-modal="false"
:close-on-press-escape="false"
v-model="open"
size="50%"
>
<template #header>
<DrawerHeader :header="$t('ssl.' + operate)" :back="handleClose" />
</template>
@@ -1,5 +1,11 @@
<template>
<el-drawer :close-on-click-modal="false" :close-on-press-escape="false" v-model="open" size="50%">
<el-drawer
:destroy-on-close="true"
:close-on-click-modal="false"
:close-on-press-escape="false"
v-model="open"
size="50%"
>
<template #header>
<DrawerHeader :header="$t('ssl.upload')" :back="handleClose" />
</template>
+64 -38
View File
@@ -128,25 +128,31 @@
prop="protocol"
width="90px"
></el-table-column>
<el-table-column :label="$t('website.expireDate')">
<template #default="{ row, $index }">
<div v-show="row.showdate">
<el-table-column
:label="$t('website.expireDate')"
prop="expireDate"
:sortable="'custom'"
min-width="120px"
>
<template #default="{ row }">
<div v-if="row.showdate">
<el-date-picker
style="width: 120px"
:key="row.id"
class="p-w-100"
v-model="row.expireDate"
type="date"
:disabled-date="checkDate"
:shortcuts="shortcuts"
:clearable="false"
:default-value="setDate(row.expireDate)"
:ref="(el) => setdateRefs(el, $index)"
@change="submitDate(row)"
:ref="(el) => setdateRefs(el)"
@visible-change="(visibility:boolean) => pickerVisibility(visibility, row)"
size="small"
:mounted="initDatePicker(row)"
></el-date-picker>
</div>
<div v-show="!row.showdate">
<el-link type="primary" :underline="false" @click="openDatePicker(row, $index)">
<div v-else>
<el-link type="primary" :underline="false" @click.stop="openDatePicker(row)">
<span v-if="isEver(row.expireDate)">
{{ $t('website.neverExpire') }}
</span>
@@ -166,7 +172,7 @@
</el-table-column>
<fu-table-operations
:ellipsis="10"
width="400px"
width="300px"
:buttons="buttons"
:label="$t('commons.table.operate')"
:fixed="mobile ? false : 'right'"
@@ -201,7 +207,7 @@ import GroupDialog from '@/components/group/index.vue';
import AppStatus from '@/components/app-status/index.vue';
import i18n from '@/lang';
import router from '@/routers';
import { onMounted, reactive, ref, computed } from '@vue/runtime-core';
import { onMounted, reactive, ref, computed } from 'vue';
import { OpWebsite, SearchWebsites, UpdateWebsite } from '@/api/modules/website';
import { Website } from '@/api/interface/website';
import { App } from '@/api/interface/app';
@@ -247,8 +253,8 @@ const uploadRef = ref();
const dialogBackupRef = ref();
const defaultRef = ref();
const data = ref();
let dateRefs: Map<number, any> = new Map();
let groups = ref<Group.GroupInfo[]>([]);
const dataRef = ref();
const paginationConfig = reactive({
cacheSizeKey: 'website-page-size',
@@ -270,7 +276,17 @@ const mobile = computed(() => {
const changeSort = ({ prop, order }) => {
if (order) {
req.orderBy = prop == 'primaryDomain' ? 'primary_domain' : prop;
switch (prop) {
case 'primaryDomain':
prop = 'primary_domain';
break;
case 'expireDate':
prop = 'expire_date';
break;
default:
break;
}
req.orderBy = prop;
req.order = order;
} else {
req.orderBy = 'created_at';
@@ -284,6 +300,7 @@ const search = async () => {
req.pageSize = paginationConfig.pageSize;
loading.value = true;
data.value = [];
await SearchWebsites(req)
.then((res) => {
data.value = res.data.items;
@@ -316,40 +333,50 @@ const isBeforeNow = (time: string) => {
return new Date() > new Date(time);
};
const setDate = (time: string) => {
if (isEver(time)) {
return new Date();
} else {
return new Date(time);
}
};
const openDatePicker = (row: any, index: number) => {
row.showdate = true;
const ref = dateRefs.get(index);
if (ref != undefined) {
if (isBeforeNow(row.expireDate)) {
row.oldExpireDate = row.expireDate;
const date = new Date().toLocaleDateString();
row.expireDate = date;
const refreshData = () => {
for (let i = 0; i < data.value.length; i++) {
if (data.value[i].showdate) {
data.value[i].showdate = false;
if (data.value[i].oldExpireDate) {
data.value[i].expireDate = data.value[i].oldExpireDate;
data.value[i].oldExpireDate = undefined;
}
}
ref.handleOpen();
}
if (dataRef.value) {
dataRef.value.handleClose();
dataRef.value = undefined;
}
};
const setdateRefs = (ref: any, index: number) => {
dateRefs.set(index, ref);
const openDatePicker = (row: any) => {
refreshData();
row.showdate = true;
};
const setdateRefs = (ref: any) => {
dataRef.value = ref;
if (dataRef.value != undefined) {
dataRef.value.handleOpen();
}
};
const initDatePicker = (row: any) => {
if (dataRef.value == undefined && row.oldExpireDate == undefined && isBeforeNow(row.expireDate)) {
row.oldExpireDate = row.expireDate;
const date = new Date().toLocaleDateString();
row.expireDate = date;
}
};
const pickerVisibility = (visibility: boolean, row: any) => {
if (!visibility) {
row.showdate = false;
if (!row.change) {
if (row.oldExpireDate) {
row.expireDate = row.oldExpireDate;
}
row.change = false;
dataRef.value = undefined;
if (row.oldExpireDate) {
row.expireDate = row.oldExpireDate;
row.oldExpireDate = undefined;
}
row.showdate = false;
}
};
@@ -365,7 +392,6 @@ const submitDate = (row: any) => {
};
UpdateWebsite(req).then(() => {
row.change = true;
MsgSuccess(i18n.global.t('commons.msg.updateSuccess'));
search();
});