mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-10-11 08:00:31 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
04b9cbd87a | ||
|
|
4565c5b300 | ||
|
|
bf9a9bf17a | ||
|
|
70df4dc6c4 | ||
|
|
ed3e738fff | ||
|
|
bc376ec495 | ||
|
|
40314e9079 | ||
|
|
82f978c683 | ||
|
|
ed288368ea | ||
|
|
db8fa144eb | ||
|
|
57b76d97ec | ||
|
|
28d8210ce6 | ||
|
|
80ce60c56c | ||
|
|
2758ab07dc | ||
|
|
0828804de2 | ||
|
|
134b17d2f0 | ||
|
|
3af254cdda | ||
|
|
f3d3d54722 | ||
|
|
fa419809f6 | ||
|
|
da606826c6 | ||
|
|
b3d9cc156a | ||
|
|
ba03d26b2c | ||
|
|
40f02889fe | ||
|
|
4f8fb26d89 | ||
|
|
0ca11e2b88 | ||
|
|
0daf0d690c |
@@ -466,12 +466,12 @@ func (b *BaseApi) RecoverByUpload(c *gin.Context) {
|
||||
}
|
||||
|
||||
switch req.Type {
|
||||
case "mysql", "mariadb":
|
||||
case "mysql", "mariadb", constant.AppMysqlCluster:
|
||||
if err := backupService.MysqlRecoverByUpload(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
case constant.AppPostgresql:
|
||||
case constant.AppPostgresql, constant.AppPostgresqlCluster:
|
||||
if err := backupService.PostgresqlRecoverByUpload(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||
@@ -53,6 +55,49 @@ func (b *BaseApi) LoadCronjobInfo(c *gin.Context) {
|
||||
helper.SuccessWithData(c, data)
|
||||
}
|
||||
|
||||
// @Tags Cronjob
|
||||
// @Summary Export cronjob list
|
||||
// @Accept json
|
||||
// @Param request body dto.OperateByIDs true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /cronjobs/export [post]
|
||||
func (b *BaseApi) ExportCronjob(c *gin.Context) {
|
||||
var req dto.OperateByIDs
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
content, err := cronjobService.Export(req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
http.ServeContent(c.Writer, c.Request, "", time.Now(), strings.NewReader(content))
|
||||
}
|
||||
|
||||
// @Tags Cronjob
|
||||
// @Summary Import cronjob list
|
||||
// @Accept json
|
||||
// @Param request body dto.CronjobImport true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /cronjobs/import [post]
|
||||
func (b *BaseApi) ImportCronjob(c *gin.Context) {
|
||||
var req dto.CronjobImport
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := cronjobService.Import(req.Cronjobs); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags Cronjob
|
||||
// @Summary Load script options
|
||||
// @Success 200 {array} dto.ScriptOptions
|
||||
|
||||
@@ -28,6 +28,9 @@ type OperationWithName struct {
|
||||
type OperateByID struct {
|
||||
ID uint `json:"id" validate:"required"`
|
||||
}
|
||||
type OperateByIDs struct {
|
||||
IDs []uint `json:"ids"`
|
||||
}
|
||||
|
||||
type Operate struct {
|
||||
Operation string `json:"operation" validate:"required"`
|
||||
|
||||
@@ -123,6 +123,57 @@ type CronjobInfo struct {
|
||||
AlertCount uint `json:"alertCount"`
|
||||
}
|
||||
|
||||
type CronjobImport struct {
|
||||
Cronjobs []CronjobTrans `json:"cronjobs"`
|
||||
}
|
||||
type CronjobTrans struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
SpecCustom bool `json:"specCustom"`
|
||||
Spec string `json:"spec"`
|
||||
|
||||
Executor string `json:"executor"`
|
||||
ScriptMode string `json:"scriptMode"`
|
||||
Script string `json:"script"`
|
||||
Command string `json:"command"`
|
||||
ContainerName string `json:"containerName"`
|
||||
User string `json:"user"`
|
||||
URL string `json:"url"`
|
||||
|
||||
ScriptName string `json:"scriptName"`
|
||||
Apps []TransHelper `json:"apps"`
|
||||
Websites []string `json:"websites"`
|
||||
DBType string `json:"dbType"`
|
||||
DBNames []TransHelper `json:"dbName"`
|
||||
|
||||
ExclusionRules string `json:"exclusionRules"`
|
||||
|
||||
IsDir bool `json:"isDir"`
|
||||
SourceDir string `json:"sourceDir"`
|
||||
|
||||
RetainCopies uint64 `json:"retainCopies"`
|
||||
RetryTimes uint `json:"retryTimes"`
|
||||
Timeout uint `json:"timeout"`
|
||||
IgnoreErr bool `json:"ignoreErr"`
|
||||
SnapshotRule SnapshotTransHelper `json:"snapshotRule"`
|
||||
Secret string `json:"secret"`
|
||||
|
||||
SourceAccounts []string `json:"sourceAccounts"`
|
||||
DownloadAccount string `json:"downloadAccount"`
|
||||
|
||||
AlertCount uint `json:"alertCount"`
|
||||
AlertTitle string `json:"alertTitle"`
|
||||
AlertMethod string `json:"alertMethod"`
|
||||
}
|
||||
type TransHelper struct {
|
||||
Name string `json:"name"`
|
||||
DetailName string `json:"detailName"`
|
||||
}
|
||||
type SnapshotTransHelper struct {
|
||||
WithImage bool `json:"withImage"`
|
||||
IgnoreApps []TransHelper `json:"ignoreApps"`
|
||||
}
|
||||
|
||||
type ScriptOptions struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package response
|
||||
|
||||
import (
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
||||
)
|
||||
|
||||
type FileInfo struct {
|
||||
@@ -37,11 +38,12 @@ type FileWgetRes struct {
|
||||
}
|
||||
|
||||
type FileLineContent struct {
|
||||
Content string `json:"content"`
|
||||
End bool `json:"end"`
|
||||
Path string `json:"path"`
|
||||
Total int `json:"total"`
|
||||
Lines []string `json:"lines"`
|
||||
Content string `json:"content"`
|
||||
End bool `json:"end"`
|
||||
Path string `json:"path"`
|
||||
Total int `json:"total"`
|
||||
TaskStatus string `json:"taskStatus"`
|
||||
Lines []string `json:"lines"`
|
||||
}
|
||||
|
||||
type FileExist struct {
|
||||
|
||||
@@ -36,6 +36,7 @@ type IAppInstallRepo interface {
|
||||
Page(page, size int, opts ...DBOption) (int64, []model.AppInstall, error)
|
||||
BatchUpdateBy(maps map[string]interface{}, opts ...DBOption) error
|
||||
LoadBaseInfo(key string, name string) (*RootInfo, error)
|
||||
LoadInstallAppByKeyAndName(key string, name string) (*model.AppInstall, error)
|
||||
GetFirstByCtx(ctx context.Context, opts ...DBOption) (model.AppInstall, error)
|
||||
}
|
||||
|
||||
@@ -246,3 +247,23 @@ func (a *AppInstallRepo) LoadBaseInfo(key string, name string) (*RootInfo, error
|
||||
info.Status = appInstall.Status
|
||||
return &info, nil
|
||||
}
|
||||
|
||||
func (a *AppInstallRepo) LoadInstallAppByKeyAndName(key string, name string) (*model.AppInstall, error) {
|
||||
var (
|
||||
app model.App
|
||||
appInstall model.AppInstall
|
||||
)
|
||||
if err := global.DB.Where("key = ?", key).First(&app).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(name) == 0 {
|
||||
if err := global.DB.Where("app_id = ?", app.ID).First(&appInstall).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
if err := global.DB.Where("app_id = ? AND name = ?", app.ID, name).First(&appInstall).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &appInstall, nil
|
||||
}
|
||||
|
||||
@@ -1240,6 +1240,32 @@ func checkImageExist(client *client.Client, imageItem string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func checkImageLike(client *client.Client, imageName string) bool {
|
||||
if client == nil {
|
||||
var err error
|
||||
client, err = docker.NewDockerClient()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
images, err := client.ImageList(context.Background(), image.ListOptions{})
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, img := range images {
|
||||
for _, tag := range img.RepoTags {
|
||||
parts := strings.Split(tag, "/")
|
||||
imageNameWithTag := parts[len(parts)-1]
|
||||
|
||||
if imageNameWithTag == imageName {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func pullImages(task *task.Task, client *client.Client, imageName string) error {
|
||||
dockerCli := docker.NewClientWithExist(client)
|
||||
options := image.PullOptions{}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
@@ -15,6 +16,7 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||
"github.com/jinzhu/copier"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/robfig/cron/v3"
|
||||
@@ -35,6 +37,8 @@ type ICronjobService interface {
|
||||
StartJob(cronjob *model.Cronjob, isUpdate bool) (string, error)
|
||||
CleanRecord(req dto.CronjobClean) error
|
||||
|
||||
Export(req dto.OperateByIDs) (string, error)
|
||||
Import(req []dto.CronjobTrans) error
|
||||
LoadScriptOptions() []dto.ScriptOptions
|
||||
|
||||
LoadInfo(req dto.OperateByID) (*dto.CronjobOperate, error)
|
||||
@@ -102,6 +106,290 @@ func (u *CronjobService) LoadInfo(req dto.OperateByID) (*dto.CronjobOperate, err
|
||||
return &item, err
|
||||
}
|
||||
|
||||
func (u *CronjobService) Export(req dto.OperateByIDs) (string, error) {
|
||||
cronjobs, err := cronjobRepo.List(repo.WithByIDs(req.IDs))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var data []dto.CronjobTrans
|
||||
for _, cronjob := range cronjobs {
|
||||
item := dto.CronjobTrans{
|
||||
Name: cronjob.Name,
|
||||
Type: cronjob.Type,
|
||||
SpecCustom: cronjob.SpecCustom,
|
||||
Spec: cronjob.Spec,
|
||||
Executor: cronjob.Executor,
|
||||
ScriptMode: cronjob.ScriptMode,
|
||||
Script: cronjob.Script,
|
||||
Command: cronjob.Command,
|
||||
ContainerName: cronjob.ContainerName,
|
||||
User: cronjob.User,
|
||||
URL: cronjob.URL,
|
||||
DBType: cronjob.DBType,
|
||||
ExclusionRules: cronjob.ExclusionRules,
|
||||
IsDir: cronjob.IsDir,
|
||||
SourceDir: cronjob.SourceDir,
|
||||
RetainCopies: cronjob.RetainCopies,
|
||||
RetryTimes: cronjob.RetryTimes,
|
||||
Timeout: cronjob.Timeout,
|
||||
IgnoreErr: cronjob.IgnoreErr,
|
||||
Secret: cronjob.Secret,
|
||||
}
|
||||
switch cronjob.Type {
|
||||
case "app":
|
||||
if cronjob.AppID == "all" {
|
||||
break
|
||||
}
|
||||
apps := loadAppsForJob(cronjob)
|
||||
for _, app := range apps {
|
||||
item.Apps = append(item.Apps, dto.TransHelper{Name: app.App.Key, DetailName: app.Name})
|
||||
}
|
||||
case "website", "cutWebsiteLog":
|
||||
if cronjob.Website == "all" {
|
||||
break
|
||||
}
|
||||
websites := loadWebsForJob(cronjob)
|
||||
for _, website := range websites {
|
||||
item.Websites = append(item.Websites, website.Alias)
|
||||
}
|
||||
case "database":
|
||||
if cronjob.DBName == "all" {
|
||||
break
|
||||
}
|
||||
databases := loadDbsForJob(cronjob)
|
||||
for _, db := range databases {
|
||||
item.DBNames = append(item.DBNames, dto.TransHelper{Name: db.Database, DetailName: db.Name})
|
||||
}
|
||||
case "shell":
|
||||
if cronjob.ScriptMode == "library" {
|
||||
script, err := scriptRepo.Get(repo.WithByID(cronjob.ScriptID))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
item.ScriptName = script.Name
|
||||
}
|
||||
case "snapshot":
|
||||
if len(cronjob.SnapshotRule) == 0 {
|
||||
break
|
||||
}
|
||||
var snapRule dto.SnapshotRule
|
||||
if err := json.Unmarshal([]byte(cronjob.SnapshotRule), &snapRule); err != nil {
|
||||
return "", err
|
||||
}
|
||||
item.SnapshotRule.WithImage = snapRule.WithImage
|
||||
if len(snapRule.IgnoreAppIDs) != 0 {
|
||||
ignoreApps, _ := appInstallRepo.ListBy(context.Background(), repo.WithByIDs(snapRule.IgnoreAppIDs))
|
||||
for _, app := range ignoreApps {
|
||||
item.SnapshotRule.IgnoreApps = append(item.SnapshotRule.IgnoreApps, dto.TransHelper{Name: app.App.Key, DetailName: app.Name})
|
||||
}
|
||||
}
|
||||
}
|
||||
item.SourceAccounts, item.DownloadAccount, _ = loadBackupNamesByID(cronjob.SourceAccountIDs, cronjob.DownloadAccountID)
|
||||
alertInfo, _ := alertRepo.Get(alertRepo.WithByType(cronjob.Type), alertRepo.WithByProject(strconv.Itoa(int(cronjob.ID))), repo.WithByStatus(constant.AlertEnable))
|
||||
if alertInfo.SendCount != 0 {
|
||||
item.AlertCount = alertInfo.SendCount
|
||||
item.AlertTitle = alertInfo.Title
|
||||
item.AlertMethod = alertInfo.Method
|
||||
} else {
|
||||
item.AlertCount = 0
|
||||
}
|
||||
data = append(data, item)
|
||||
}
|
||||
itemJson, err := json.Marshal(&data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(itemJson), nil
|
||||
}
|
||||
|
||||
func (u *CronjobService) Import(req []dto.CronjobTrans) error {
|
||||
for _, item := range req {
|
||||
cronjobItem, _ := cronjobRepo.Get(repo.WithByName(item.Name))
|
||||
if cronjobItem.ID != 0 {
|
||||
continue
|
||||
}
|
||||
cronjob := model.Cronjob{
|
||||
Name: item.Name,
|
||||
Type: item.Type,
|
||||
SpecCustom: item.SpecCustom,
|
||||
Spec: item.Spec,
|
||||
Executor: item.Executor,
|
||||
ScriptMode: item.ScriptMode,
|
||||
Command: item.Command,
|
||||
ContainerName: item.ContainerName,
|
||||
User: item.User,
|
||||
URL: item.URL,
|
||||
DBType: item.DBType,
|
||||
ExclusionRules: item.ExclusionRules,
|
||||
IsDir: item.IsDir,
|
||||
RetainCopies: item.RetainCopies,
|
||||
RetryTimes: item.RetryTimes,
|
||||
Timeout: item.Timeout,
|
||||
IgnoreErr: item.IgnoreErr,
|
||||
Secret: item.Secret,
|
||||
}
|
||||
hasNotFound := false
|
||||
switch item.Type {
|
||||
case "app":
|
||||
if len(item.Apps) == 0 {
|
||||
cronjob.AppID = "all"
|
||||
break
|
||||
}
|
||||
var appIDs []string
|
||||
for _, app := range item.Apps {
|
||||
appItem, err := appInstallRepo.LoadInstallAppByKeyAndName(app.Name, app.DetailName)
|
||||
if err != nil {
|
||||
hasNotFound = true
|
||||
continue
|
||||
}
|
||||
appIDs = append(appIDs, fmt.Sprintf("%v", appItem.ID))
|
||||
}
|
||||
cronjob.AppID = strings.Join(appIDs, ",")
|
||||
case "website", "cutWebsiteLog":
|
||||
if len(item.Websites) == 0 {
|
||||
cronjob.Website = "all"
|
||||
break
|
||||
}
|
||||
var webIDs []string
|
||||
for _, web := range item.Websites {
|
||||
webItem, err := websiteRepo.GetFirst(websiteRepo.WithAlias(web))
|
||||
if err != nil {
|
||||
hasNotFound = true
|
||||
continue
|
||||
}
|
||||
webIDs = append(webIDs, fmt.Sprintf("%v", webItem.ID))
|
||||
}
|
||||
cronjob.Website = strings.Join(webIDs, ",")
|
||||
case "database":
|
||||
if len(item.DBNames) == 0 {
|
||||
cronjob.DBName = "all"
|
||||
break
|
||||
}
|
||||
var dbIDs []string
|
||||
if strings.Contains(cronjob.DBType, "postgresql") {
|
||||
for _, db := range item.DBNames {
|
||||
dbItem, err := postgresqlRepo.Get(postgresqlRepo.WithByPostgresqlName(db.Name), repo.WithByName(db.DetailName))
|
||||
if err != nil {
|
||||
hasNotFound = true
|
||||
continue
|
||||
}
|
||||
dbIDs = append(dbIDs, fmt.Sprintf("%v", dbItem.ID))
|
||||
}
|
||||
} else {
|
||||
for _, db := range item.DBNames {
|
||||
dbItem, err := mysqlRepo.Get(mysqlRepo.WithByMysqlName(db.Name), repo.WithByName(db.DetailName))
|
||||
if err != nil {
|
||||
hasNotFound = true
|
||||
continue
|
||||
}
|
||||
dbIDs = append(dbIDs, fmt.Sprintf("%v", dbItem.ID))
|
||||
}
|
||||
}
|
||||
cronjob.DBName = strings.Join(dbIDs, ",")
|
||||
case "shell":
|
||||
if len(item.ContainerName) != 0 {
|
||||
cronjob.Script = item.Script
|
||||
client, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
hasNotFound = true
|
||||
break
|
||||
}
|
||||
defer client.Close()
|
||||
if _, err := client.ContainerStats(context.Background(), item.ContainerName, false); err != nil {
|
||||
hasNotFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
switch item.ScriptMode {
|
||||
case "library":
|
||||
library, _ := scriptRepo.Get(repo.WithByName(item.ScriptName))
|
||||
if library.ID == 0 {
|
||||
hasNotFound = true
|
||||
break
|
||||
}
|
||||
cronjob.ScriptID = library.ID
|
||||
case "select":
|
||||
if _, err := os.Stat(item.Script); err != nil {
|
||||
hasNotFound = true
|
||||
break
|
||||
}
|
||||
cronjob.Script = item.Script
|
||||
case "input":
|
||||
cronjob.Script = item.Script
|
||||
}
|
||||
case "directory":
|
||||
if item.IsDir {
|
||||
if _, err := os.Stat(item.SourceDir); err != nil {
|
||||
hasNotFound = true
|
||||
break
|
||||
}
|
||||
cronjob.SourceDir = item.SourceDir
|
||||
} else {
|
||||
fileList := strings.Split(item.SourceDir, ",")
|
||||
var newFiles []string
|
||||
for _, item := range fileList {
|
||||
if len(item) == 0 {
|
||||
continue
|
||||
}
|
||||
if _, err := os.Stat(item); err != nil {
|
||||
hasNotFound = true
|
||||
continue
|
||||
}
|
||||
newFiles = append(newFiles, item)
|
||||
}
|
||||
cronjob.SourceDir = strings.Join(newFiles, ",")
|
||||
}
|
||||
case "snapshot":
|
||||
if len(item.SnapshotRule.IgnoreApps) == 0 && !item.SnapshotRule.WithImage {
|
||||
break
|
||||
}
|
||||
var itemRules dto.SnapshotRule
|
||||
itemRules.WithImage = item.SnapshotRule.WithImage
|
||||
for _, app := range item.SnapshotRule.IgnoreApps {
|
||||
appItem, err := appInstallRepo.LoadInstallAppByKeyAndName(app.Name, app.DetailName)
|
||||
if err != nil {
|
||||
hasNotFound = true
|
||||
continue
|
||||
}
|
||||
itemRules.IgnoreAppIDs = append(itemRules.IgnoreAppIDs, appItem.ID)
|
||||
}
|
||||
itemRulesStr, _ := json.Marshal(itemRules)
|
||||
cronjob.SnapshotRule = string(itemRulesStr)
|
||||
}
|
||||
var acIDs []string
|
||||
for _, ac := range item.SourceAccounts {
|
||||
backup, err := backupRepo.Get(repo.WithByName(ac))
|
||||
if err != nil {
|
||||
hasNotFound = true
|
||||
continue
|
||||
}
|
||||
if ac == item.DownloadAccount {
|
||||
cronjob.DownloadAccountID = backup.ID
|
||||
}
|
||||
acIDs = append(acIDs, fmt.Sprintf("%v", backup.ID))
|
||||
}
|
||||
cronjob.SourceAccountIDs = strings.Join(acIDs, ",")
|
||||
if hasNotFound {
|
||||
cronjob.Status = constant.StatusPending
|
||||
} else {
|
||||
cronjob.Status = constant.StatusDisable
|
||||
}
|
||||
_ = cronjobRepo.Create(&cronjob)
|
||||
if item.AlertCount != 0 {
|
||||
createAlert := dto.AlertCreate{
|
||||
Title: item.AlertTitle,
|
||||
SendCount: item.AlertCount,
|
||||
Method: item.AlertMethod,
|
||||
Type: cronjob.Type,
|
||||
Project: strconv.Itoa(int(cronjob.ID)),
|
||||
Status: constant.AlertEnable,
|
||||
}
|
||||
_ = NewIAlertService().CreateAlert(createAlert)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *CronjobService) LoadScriptOptions() []dto.ScriptOptions {
|
||||
scripts, err := scriptRepo.List()
|
||||
if err != nil {
|
||||
@@ -411,6 +699,9 @@ func (u *CronjobService) Update(id uint, req dto.CronjobOperate) error {
|
||||
}
|
||||
}
|
||||
|
||||
if cronModel.Status == constant.StatusPending {
|
||||
upMap["status"] = constant.StatusEnable
|
||||
}
|
||||
upMap["name"] = req.Name
|
||||
upMap["spec_custom"] = req.SpecCustom
|
||||
upMap["spec"] = spec
|
||||
@@ -428,6 +719,7 @@ func (u *CronjobService) Update(id uint, req dto.CronjobOperate) error {
|
||||
upMap["db_type"] = req.DBType
|
||||
upMap["db_name"] = req.DBName
|
||||
upMap["url"] = req.URL
|
||||
upMap["is_dir"] = req.IsDir
|
||||
upMap["source_dir"] = req.SourceDir
|
||||
upMap["snapshot_rule"] = cronjob.SnapshotRule
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -24,22 +25,7 @@ import (
|
||||
)
|
||||
|
||||
func (u *CronjobService) handleApp(cronjob model.Cronjob, startTime time.Time, taskItem *task.Task) error {
|
||||
var apps []model.AppInstall
|
||||
if cronjob.AppID == "all" {
|
||||
apps, _ = appInstallRepo.ListBy(context.Background())
|
||||
} else {
|
||||
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(context.Background(), repo.WithByIDs(idItems))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
apps = appItems
|
||||
}
|
||||
apps := loadAppsForJob(cronjob)
|
||||
if len(apps) == 0 {
|
||||
return errors.New("no such app in database!")
|
||||
}
|
||||
@@ -214,7 +200,10 @@ func (u *CronjobService) handleDirectory(cronjob model.Cronjob, startTime time.T
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fileName := fmt.Sprintf("%s.tar.gz", startTime.Format(constant.DateTimeSlimLayout)+common.RandStrAndNum(5))
|
||||
fileName := fmt.Sprintf("%s.tar.gz", startTime.Format(constant.DateTimeSlimLayout)+common.RandStrAndNum(2))
|
||||
if cronjob.IsDir || len(strings.Split(cronjob.SourceDir, ",")) == 1 {
|
||||
fileName = loadFileName(cronjob.SourceDir)
|
||||
}
|
||||
backupDir := path.Join(global.Dir.LocalBackupDir, fmt.Sprintf("tmp/%s/%s", cronjob.Type, cronjob.Name))
|
||||
|
||||
fileOp := files.NewFileOp()
|
||||
@@ -344,6 +333,23 @@ func (u *CronjobService) handleSnapshot(cronjob model.Cronjob, jobRecord model.J
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadAppsForJob(cronjob model.Cronjob) []model.AppInstall {
|
||||
var apps []model.AppInstall
|
||||
if cronjob.AppID == "all" {
|
||||
apps, _ = appInstallRepo.ListBy(context.Background())
|
||||
} else {
|
||||
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, _ := appInstallRepo.ListBy(context.Background(), repo.WithByIDs(idItems))
|
||||
apps = appItems
|
||||
}
|
||||
return apps
|
||||
}
|
||||
|
||||
type DatabaseHelper struct {
|
||||
ID uint
|
||||
DBType string
|
||||
@@ -546,3 +552,14 @@ func loadSnapWithRule(cronjob model.Cronjob) (dto.SnapshotData, error) {
|
||||
}
|
||||
return itemData, nil
|
||||
}
|
||||
|
||||
func loadFileName(src string) string {
|
||||
dirs := strings.Split(filepath.ToSlash(src), "/")
|
||||
var keyPart string
|
||||
if len(dirs) >= 3 {
|
||||
keyPart = filepath.Join(dirs[len(dirs)-3], dirs[len(dirs)-2], dirs[len(dirs)-1])
|
||||
}
|
||||
cleanName := strings.ReplaceAll(keyPart, string(filepath.Separator), "_")
|
||||
timestamp := time.Now().Format(constant.DateTimeSlimLayout)
|
||||
return fmt.Sprintf("%s_%s_%s.tar.gz", cleanName, timestamp, common.RandStrAndNum(2))
|
||||
}
|
||||
|
||||
@@ -4,10 +4,6 @@ import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/jinzhu/copier"
|
||||
"golang.org/x/text/encoding"
|
||||
"golang.org/x/text/encoding/simplifiedchinese"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
@@ -20,6 +16,11 @@ import (
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/jinzhu/copier"
|
||||
"golang.org/x/text/encoding"
|
||||
"golang.org/x/text/encoding/simplifiedchinese"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
||||
@@ -478,6 +479,7 @@ func (f *FileService) DepthDirSize(req request.DirSizeReq) ([]response.DepthDirS
|
||||
|
||||
func (f *FileService) ReadLogByLine(req request.FileReadByLineReq) (*response.FileLineContent, error) {
|
||||
logFilePath := ""
|
||||
taskStatus := ""
|
||||
switch req.Type {
|
||||
case constant.TypeWebsite:
|
||||
website, err := websiteRepo.GetFirst(repo.WithByID(req.ID))
|
||||
@@ -533,6 +535,7 @@ func (f *FileService) ReadLogByLine(req request.FileReadByLineReq) (*response.Fi
|
||||
return nil, err
|
||||
}
|
||||
logFilePath = taskModel.LogFile
|
||||
taskStatus = taskModel.Status
|
||||
case "mysql-slow-logs":
|
||||
logFilePath = path.Join(global.Dir.DataDir, fmt.Sprintf("apps/mysql/%s/data/1Panel-slow.log", req.Name))
|
||||
case "mariadb-slow-logs":
|
||||
@@ -551,11 +554,12 @@ func (f *FileService) ReadLogByLine(req request.FileReadByLineReq) (*response.Fi
|
||||
lines = append(preLines, lines...)
|
||||
}
|
||||
res := &response.FileLineContent{
|
||||
Content: strings.Join(lines, "\n"),
|
||||
End: isEndOfFile,
|
||||
Path: logFilePath,
|
||||
Total: total,
|
||||
Lines: lines,
|
||||
Content: strings.Join(lines, "\n"),
|
||||
End: isEndOfFile,
|
||||
Path: logFilePath,
|
||||
Total: total,
|
||||
TaskStatus: taskStatus,
|
||||
Lines: lines,
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
@@ -450,6 +450,7 @@ func (r *RuntimeService) Update(req request.RuntimeUpdate) error {
|
||||
if err := checkContainerName(containerName.(string)); err != nil {
|
||||
return err
|
||||
}
|
||||
runtime.ContainerName = containerName.(string)
|
||||
}
|
||||
|
||||
projectDir := path.Join(global.Dir.RuntimeDir, runtime.Type, runtime.Name)
|
||||
|
||||
@@ -257,7 +257,7 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error)
|
||||
var (
|
||||
domains []model.WebsiteDomain
|
||||
)
|
||||
domains, _, _, err = getWebsiteDomains(create.Domains, defaultHttpPort, 0)
|
||||
domains, _, _, err = getWebsiteDomains(create.Domains, defaultHttpPort, nginxInstall.HttpsPort, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -388,7 +388,7 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error)
|
||||
switch runtime.Type {
|
||||
case constant.RuntimePHP:
|
||||
if runtime.Resource == constant.ResourceAppstore {
|
||||
if !checkImageExist(nil, runtime.Image) {
|
||||
if !checkImageLike(nil, runtime.Image) {
|
||||
return buserr.WithName("ErrImageNotExist", runtime.Name)
|
||||
}
|
||||
website.Proxy = fmt.Sprintf("127.0.0.1:%s", runtime.Port)
|
||||
@@ -680,7 +680,7 @@ func (w WebsiteService) CreateWebsiteDomain(create request.WebsiteDomainCreate)
|
||||
domainModels []model.WebsiteDomain
|
||||
addPorts []int
|
||||
)
|
||||
httpPort, _, err := getAppInstallPort(constant.AppOpenresty)
|
||||
httpPort, httpsPort, err := getAppInstallPort(constant.AppOpenresty)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -689,7 +689,7 @@ func (w WebsiteService) CreateWebsiteDomain(create request.WebsiteDomainCreate)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
domainModels, addPorts, _, err = getWebsiteDomains(create.Domains, httpPort, create.WebsiteID)
|
||||
domainModels, addPorts, _, err = getWebsiteDomains(create.Domains, httpPort, httpsPort, create.WebsiteID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1354,7 +1354,7 @@ func (w WebsiteService) ChangePHPVersion(req request.WebsitePHPVersionReq) error
|
||||
return err
|
||||
}
|
||||
defer client.Close()
|
||||
if !checkImageExist(client, oldRuntime.Image) {
|
||||
if !checkImageLike(client, oldRuntime.Image) {
|
||||
return buserr.WithName("ErrImageNotExist", oldRuntime.Name)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1031,7 +1031,7 @@ func chownRootDir(path string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func getWebsiteDomains(domains []request.WebsiteDomain, defaultPort int, websiteID uint) (domainModels []model.WebsiteDomain, addPorts []int, addDomains []string, err error) {
|
||||
func getWebsiteDomains(domains []request.WebsiteDomain, defaultHTTPPort, defaultHTTPsPort int, websiteID uint) (domainModels []model.WebsiteDomain, addPorts []int, addDomains []string, err error) {
|
||||
var (
|
||||
ports = make(map[int]struct{})
|
||||
existPort = make(map[int]struct{})
|
||||
@@ -1056,7 +1056,7 @@ func getWebsiteDomains(domains []request.WebsiteDomain, defaultPort int, website
|
||||
domainModel.Domain = strings.ToLower(domainModel.Domain)
|
||||
domainModel.Port = domain.Port
|
||||
if domain.Port == 0 {
|
||||
domain.Port = defaultPort
|
||||
domain.Port = defaultHTTPPort
|
||||
}
|
||||
domainModel.SSL = domain.SSL
|
||||
domainModel.WebsiteID = websiteID
|
||||
@@ -1077,7 +1077,7 @@ func getWebsiteDomains(domains []request.WebsiteDomain, defaultPort int, website
|
||||
}
|
||||
|
||||
for port := range ports {
|
||||
if port == defaultPort {
|
||||
if port == defaultHTTPPort || port == defaultHTTPsPort {
|
||||
addPorts = append(addPorts, port)
|
||||
continue
|
||||
}
|
||||
@@ -1098,7 +1098,7 @@ func getWebsiteDomains(domains []request.WebsiteDomain, defaultPort int, website
|
||||
err = buserr.WithMap("ErrPortExist", errMap, nil)
|
||||
return
|
||||
}
|
||||
if port != 443 && common.ScanPort(port) {
|
||||
if port != defaultHTTPsPort && common.ScanPort(port) {
|
||||
err = buserr.WithDetail("ErrPortInUsed", port, nil)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ const (
|
||||
StatusCanceled = "Canceled"
|
||||
StatusDone = "Done"
|
||||
StatusWaiting = "Waiting"
|
||||
StatusPending = "Pending"
|
||||
StatusSuccess = "Success"
|
||||
StatusFailed = "Failed"
|
||||
StatusUploading = "Uploading"
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/xpack"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -17,13 +19,8 @@ func Certificate() gin.HandlerFunc {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
if !c.Request.TLS.HandshakeComplete || len(c.Request.TLS.PeerCertificates) == 0 {
|
||||
helper.InternalServer(c, errors.New("no such tls peer certificates"))
|
||||
return
|
||||
}
|
||||
cert := c.Request.TLS.PeerCertificates[0]
|
||||
if cert.Subject.CommonName != "panel_client" {
|
||||
helper.InternalServer(c, fmt.Errorf("err certificate"))
|
||||
if !xpack.ValidateCertificate(c) {
|
||||
CloseDirectly(c)
|
||||
return
|
||||
}
|
||||
conn := c.Request.Header.Get("Connection")
|
||||
@@ -40,3 +37,18 @@ func Certificate() gin.HandlerFunc {
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func CloseDirectly(c *gin.Context) {
|
||||
hijacker, ok := c.Writer.(http.Hijacker)
|
||||
if !ok {
|
||||
c.AbortWithStatus(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
conn, _, err := hijacker.Hijack()
|
||||
if err != nil {
|
||||
c.AbortWithStatus(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
_ = conn.(*net.TCPConn).SetLinger(0)
|
||||
conn.Close()
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ func (s *CronjobRouter) InitRouter(Router *gin.RouterGroup) {
|
||||
{
|
||||
cmdRouter.POST("", baseApi.CreateCronjob)
|
||||
cmdRouter.POST("/next", baseApi.LoadNextHandle)
|
||||
cmdRouter.POST("/import", baseApi.ImportCronjob)
|
||||
cmdRouter.POST("/export", baseApi.ExportCronjob)
|
||||
cmdRouter.POST("/load/info", baseApi.LoadCronjobInfo)
|
||||
cmdRouter.GET("/script/options", baseApi.LoadScriptOptions)
|
||||
cmdRouter.POST("/del", baseApi.DeleteCronjob)
|
||||
|
||||
+10
-1
@@ -2,6 +2,7 @@ package server
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -78,9 +79,17 @@ func Start() {
|
||||
fmt.Printf("failed to load X.509 key pair: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
server.TLSConfig = &tls.Config{
|
||||
Certificates: []tls.Certificate{tlsCert},
|
||||
ClientAuth: tls.RequireAnyClientCert,
|
||||
ClientAuth: tls.RequireAndVerifyClientCert,
|
||||
}
|
||||
caItem, _ := settingRepo.GetValueByKey("RootCrt")
|
||||
if len(caItem) != 0 {
|
||||
caCertPool := x509.NewCertPool()
|
||||
rootCrt, _ := encrypt.StringDecrypt(caItem)
|
||||
caCertPool.AppendCertsFromPEM([]byte(rootCrt))
|
||||
server.TLSConfig.ClientCAs = caCertPool
|
||||
}
|
||||
business.Init()
|
||||
global.LOG.Infof("listen at https://0.0.0.0:%s", global.CONF.Base.Port)
|
||||
|
||||
@@ -132,7 +132,7 @@ func isValidHeader(key, value string) bool {
|
||||
func sendWithSSL(config SMTPConfig, addr string, toList []string, msg string, transport *http.Transport) error {
|
||||
var err error
|
||||
var conn net.Conn
|
||||
if transport != nil {
|
||||
if transport != nil && transport.DialContext != nil {
|
||||
conn, err = transport.DialContext(context.Background(), "tcp", addr)
|
||||
} else {
|
||||
conn, err = net.Dial("tcp", addr)
|
||||
@@ -163,7 +163,7 @@ func sendWithSSL(config SMTPConfig, addr string, toList []string, msg string, tr
|
||||
func sendWithStartTLS(config SMTPConfig, addr string, toList []string, msg string, transport *http.Transport) error {
|
||||
var err error
|
||||
var conn net.Conn
|
||||
if transport != nil {
|
||||
if transport != nil && transport.DialContext != nil {
|
||||
conn, err = transport.DialContext(context.Background(), "tcp", addr)
|
||||
} else {
|
||||
conn, err = net.Dial("tcp", addr)
|
||||
@@ -192,7 +192,7 @@ func sendWithStartTLS(config SMTPConfig, addr string, toList []string, msg strin
|
||||
func sendPlaintext(config SMTPConfig, addr string, toList []string, msg string, transport *http.Transport) error {
|
||||
var err error
|
||||
var conn net.Conn
|
||||
if transport != nil {
|
||||
if transport != nil && transport.DialContext != nil {
|
||||
conn, err = transport.DialContext(context.Background(), "tcp", addr)
|
||||
} else {
|
||||
conn, err = net.Dial("tcp", addr)
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
@@ -38,20 +39,9 @@ func NewIptables() (*Iptables, error) {
|
||||
return iptables, nil
|
||||
}
|
||||
|
||||
func (iptables *Iptables) run(rule string) error {
|
||||
stdout, err := cmd.RunDefaultWithStdoutBashCf("%s iptables -t nat %s", iptables.CmdStr, rule)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s, %s", err, stdout)
|
||||
}
|
||||
if stdout != "" {
|
||||
return fmt.Errorf("iptables error: %s", stdout)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (iptables *Iptables) outf(tab, rule string, a ...any) (stdout string, err error) {
|
||||
stdout, err = cmd.RunDefaultWithStdoutBashCf("%s iptables -t %s %s", iptables.CmdStr, tab, fmt.Sprintf(rule, a...))
|
||||
cmdMgr := cmd.NewCommandMgr(cmd.WithIgnoreExist1(), cmd.WithTimeout(20*time.Second))
|
||||
stdout, err = cmdMgr.RunWithStdoutBashCf("%s iptables -t %s %s", iptables.CmdStr, tab, fmt.Sprintf(rule, a...))
|
||||
if err != nil && stdout != "" {
|
||||
global.LOG.Errorf("iptables failed, err: %s", stdout)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
)
|
||||
|
||||
@@ -99,7 +100,9 @@ func (f *Ufw) ListPort() ([]FireInfo, error) {
|
||||
}
|
||||
|
||||
func (f *Ufw) ListForward() ([]FireInfo, error) {
|
||||
_ = f.EnableForward()
|
||||
if err := f.EnableForward(); err != nil {
|
||||
global.LOG.Errorf("init port forward failed, err: %v", err)
|
||||
}
|
||||
iptables, err := NewIptables()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -253,7 +253,12 @@ func (r *Local) Backup(info BackupInfo) error {
|
||||
func (r *Local) Recover(info RecoverInfo) error {
|
||||
fi, _ := os.Open(info.SourceFile)
|
||||
defer fi.Close()
|
||||
cmd := exec.Command("docker", "exec", "-i", r.ContainerName, r.Type, "-uroot", "-p"+r.Password, "--default-character-set="+info.Format, info.Name)
|
||||
mysqlCli := r.Type
|
||||
if mysqlCli == "mysql-cluster" {
|
||||
mysqlCli = "mysql"
|
||||
}
|
||||
|
||||
cmd := exec.Command("docker", "exec", "-i", r.ContainerName, mysqlCli, "-uroot", "-p"+r.Password, "--default-character-set="+info.Format, info.Name)
|
||||
if strings.HasSuffix(info.SourceFile, ".gz") {
|
||||
gzipFile, err := os.Open(info.SourceFile)
|
||||
if err != nil {
|
||||
@@ -271,7 +276,7 @@ func (r *Local) Recover(info RecoverInfo) error {
|
||||
}
|
||||
stdout, err := cmd.CombinedOutput()
|
||||
stdStr := strings.ReplaceAll(string(stdout), "mysql: [Warning] Using a password on the command line interface can be insecure.\n", "")
|
||||
if err != nil || strings.HasPrefix(string(stdStr), "ERROR ") {
|
||||
if err != nil || strings.HasPrefix(stdStr, "ERROR ") {
|
||||
return errors.New(stdStr)
|
||||
}
|
||||
|
||||
|
||||
@@ -136,7 +136,11 @@ func (r *Local) Backup(info BackupInfo) error {
|
||||
}
|
||||
defer outfile.Close()
|
||||
global.LOG.Infof("start to pg_dump | gzip > %s.gzip", info.TargetDir+"/"+info.FileName)
|
||||
cmd := exec.Command("docker", "exec", "-e", fmt.Sprintf("PGPASSWORD=%s", r.Password), r.ContainerName, "pg_dump", "-F", "c", "-U", r.Username, "-d", info.Name)
|
||||
cmd := exec.Command(
|
||||
"docker", "exec", "-i", r.ContainerName,
|
||||
"sh", "-c",
|
||||
fmt.Sprintf("PGPASSWORD=%s pg_dump -F c -U %s -d %s", r.Password, r.Username, info.Name),
|
||||
)
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
@@ -155,7 +159,9 @@ func (r *Local) Backup(info BackupInfo) error {
|
||||
func (r *Local) Recover(info RecoverInfo) error {
|
||||
fi, _ := os.Open(info.SourceFile)
|
||||
defer fi.Close()
|
||||
cmd := exec.Command("docker", "exec", "-i", r.ContainerName, "pg_restore", "-F", "c", "-c", "-U", r.Username, "-d", info.Name)
|
||||
cmd := exec.Command("docker", "exec", r.ContainerName, "sh", "-c",
|
||||
fmt.Sprintf("PGPASSWORD=%s pg_dump -F c -U %s -d %s", r.Password, r.Username, info.Name),
|
||||
)
|
||||
if strings.HasSuffix(info.SourceFile, ".gz") {
|
||||
gzipFile, err := os.Open(info.SourceFile)
|
||||
if err != nil {
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func RemoveTamper(website string) {}
|
||||
@@ -72,3 +73,7 @@ func LoadRequestTransport() *http.Transport {
|
||||
IdleConnTimeout: 15 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
func ValidateCertificate(c *gin.Context) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -56,6 +56,9 @@ func NewClient(c ConnInfo) (*SSHClient, error) {
|
||||
return nil, err
|
||||
}
|
||||
sshClient := &SSHClient{Client: client}
|
||||
if c.User == "root" {
|
||||
return sshClient, nil
|
||||
}
|
||||
if _, err := sshClient.Run("sudo -n ls"); err == nil {
|
||||
sshClient.SudoItem = "sudo"
|
||||
}
|
||||
|
||||
@@ -101,6 +101,52 @@ export namespace Cronjob {
|
||||
alertTitle: string;
|
||||
alertMethod: string;
|
||||
}
|
||||
export interface CronjobTrans {
|
||||
name: string;
|
||||
type: string;
|
||||
specCustom: boolean;
|
||||
spec: string;
|
||||
group: string;
|
||||
|
||||
executor: string;
|
||||
scriptMode: string;
|
||||
script: string;
|
||||
command: string;
|
||||
containerName: string;
|
||||
user: string;
|
||||
url: string;
|
||||
|
||||
scriptName: string;
|
||||
apps: Array<TransHelper>;
|
||||
websites: Array<string>;
|
||||
dbType: string;
|
||||
dbNames: Array<TransHelper>;
|
||||
|
||||
exclusionRules: string;
|
||||
|
||||
isDir: boolean;
|
||||
sourceDir: string;
|
||||
|
||||
retainCopies: number;
|
||||
retryTimes: number;
|
||||
timeout: number;
|
||||
ignoreErr: boolean;
|
||||
snapshotRule: string;
|
||||
secret: string;
|
||||
|
||||
sourceAccounts: Array<string>;
|
||||
downloadAccount: string;
|
||||
|
||||
alertCount: number;
|
||||
}
|
||||
export interface TransHelper {
|
||||
name: string;
|
||||
detailName: string;
|
||||
}
|
||||
export interface snapshotTransHelper {
|
||||
withImage: boolean;
|
||||
ignoreApps: Array<TransHelper>;
|
||||
}
|
||||
export interface snapshotRule {
|
||||
withImage: boolean;
|
||||
ignoreAppIDs: Array<Number>;
|
||||
|
||||
@@ -11,6 +11,13 @@ export const loadNextHandle = (spec: string) => {
|
||||
return http.post<Array<String>>(`/cronjobs/next`, { spec: spec });
|
||||
};
|
||||
|
||||
export const importCronjob = (trans: Array<Cronjob.CronjobTrans>) => {
|
||||
return http.post('cronjobs/import', { cronjobs: trans }, TimeoutEnum.T_60S);
|
||||
};
|
||||
export const exportCronjob = (params: { ids: Array<number> }) => {
|
||||
return http.download<BlobPart>('cronjobs/export', params, { responseType: 'blob', timeout: TimeoutEnum.T_40S });
|
||||
};
|
||||
|
||||
export const loadCronjobInfo = (id: number) => {
|
||||
return http.post<Cronjob.CronjobOperate>(`/cronjobs/load/info`, { id: id });
|
||||
};
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, useSlots } from 'vue';
|
||||
import { computed, useSlots, ref } from 'vue';
|
||||
defineOptions({ name: 'DrawerPro' });
|
||||
import i18n from '@/lang';
|
||||
import { GlobalStore } from '@/store';
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
<template>
|
||||
<div v-loading="firstLoading">
|
||||
<div v-if="defaultButton">
|
||||
<el-checkbox border v-model="tailLog" class="float-left" @change="changeTail(false)" v-if="showTail">
|
||||
<el-checkbox
|
||||
border
|
||||
:disabled="isTailDisabled"
|
||||
v-model="tailLog"
|
||||
class="float-left"
|
||||
@change="changeTail(false)"
|
||||
v-if="showTail"
|
||||
>
|
||||
{{ $t('commons.button.watch') }}
|
||||
</el-checkbox>
|
||||
<el-button
|
||||
@@ -106,7 +113,7 @@ const stopSignals = [
|
||||
'image push successful!',
|
||||
'[TASK-END]',
|
||||
];
|
||||
const emit = defineEmits(['update:loading', 'update:hasContent', 'update:isReading']);
|
||||
const emit = defineEmits(['update:loading', 'update:hasContent', 'update:isReading', 'stop-reading']);
|
||||
const tailLog = ref(true);
|
||||
const loading = ref(props.loading);
|
||||
const readReq = reactive({
|
||||
@@ -129,6 +136,8 @@ const minPage = ref(0);
|
||||
let timer: NodeJS.Timer | null = null;
|
||||
const logPath = ref('');
|
||||
|
||||
const isTailDisabled = ref();
|
||||
|
||||
const firstLoading = ref(false);
|
||||
const logs = ref<string[]>([]);
|
||||
const logContainer = ref<HTMLElement | null>(null);
|
||||
@@ -213,6 +222,10 @@ const getContent = async (pre: boolean) => {
|
||||
firstLoading.value = false;
|
||||
}
|
||||
|
||||
if (res.data.taskStatus && res.data.taskStatus !== 'Executing') {
|
||||
isTailDisabled.value = true;
|
||||
}
|
||||
|
||||
logPath.value = res.data.path;
|
||||
firstLoading.value = false;
|
||||
|
||||
@@ -298,6 +311,7 @@ const onCloseLog = async () => {
|
||||
timer = null;
|
||||
}
|
||||
timer = null;
|
||||
emit('stop-reading');
|
||||
isLoading.value = false;
|
||||
emit('update:isReading', false);
|
||||
bus.emit('refreshTask', true);
|
||||
@@ -329,6 +343,7 @@ const containerStyle = computed(() => ({
|
||||
|
||||
onMounted(async () => {
|
||||
logs.value = [];
|
||||
isTailDisabled.value = false;
|
||||
firstLoading.value = true;
|
||||
await init();
|
||||
nextTick(() => {
|
||||
|
||||
@@ -188,6 +188,8 @@ const acceptParams = async (params: DialogProps): Promise<void> => {
|
||||
case 'mysql':
|
||||
case 'mariadb':
|
||||
case 'postgresql':
|
||||
case 'mysql-cluster':
|
||||
case 'postgresql-cluster':
|
||||
title.value = name.value + ' [ ' + detailName.value + ' ]';
|
||||
if (detailName.value) {
|
||||
baseDir.value = `${pathRes.data}/uploads/database/${type.value}/${name.value}/${detailName.value}/`;
|
||||
|
||||
@@ -12,6 +12,7 @@ export const useLogo = async () => {
|
||||
globalStore.themeConfig.loginImage = res.data?.loginImage;
|
||||
globalStore.themeConfig.loginBgType = res.data?.loginBgType;
|
||||
globalStore.themeConfig.loginBackground = res.data?.loginBackground;
|
||||
globalStore.themeConfig.loginBtnLinkColor = res.data?.loginBtnLinkColor;
|
||||
globalStore.themeConfig.favicon = res.data.favicon;
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ const message = {
|
||||
verify: 'Verify',
|
||||
saveAndEnable: 'Save and enable',
|
||||
import: 'Import',
|
||||
export: 'Export',
|
||||
power: 'Authorization',
|
||||
search: 'Search',
|
||||
refresh: 'Refresh',
|
||||
@@ -298,6 +299,7 @@ const message = {
|
||||
normal: 'Normal',
|
||||
building: 'Building',
|
||||
upgrading: 'Upgrading',
|
||||
pending: 'Pending Edit',
|
||||
rebuilding: 'Rebuilding',
|
||||
deny: 'Denied',
|
||||
accept: 'Accepted',
|
||||
@@ -988,6 +990,10 @@ const message = {
|
||||
cronjob: {
|
||||
create: 'Create cron job',
|
||||
edit: 'Edit cron job',
|
||||
errImport: 'File content exception:',
|
||||
errImportFormat: 'The scheduled task data or format is abnormal. Please check and try again!',
|
||||
importHelper:
|
||||
'Duplicate scheduled tasks will be automatically skipped during import. Tasks will be set to [Disabled] status by default, and set to [Pending Edit] status when data association is abnormal.',
|
||||
changeStatus: 'Change status',
|
||||
disableMsg: 'This will stop the scheduled task from automatically executing. Do you want to continue?',
|
||||
enableMsg: 'This will allow the scheduled task to automatically execute. Do you want to continue?',
|
||||
@@ -1607,7 +1613,7 @@ const message = {
|
||||
apiKeyHelper: 'API key is used for third-party applications to access the API.',
|
||||
ipWhiteList: 'IP allowlist',
|
||||
ipWhiteListEgs: 'One per line. For example,\n172.161.10.111\n172.161.10.0/24',
|
||||
ipWhiteListHelper: 'IPs within the allowlist can access the API.',
|
||||
ipWhiteListHelper: 'IPs within the allowlist can access the API,0.0.0.0/0 (all IPv4), ::/0 (all IPv6)',
|
||||
apiKeyValidityTime: 'Validity period of interface key',
|
||||
apiKeyValidityTimeEgs: 'Validity period of interface key (in minutes)',
|
||||
apiKeyValidityTimeHelper:
|
||||
@@ -1716,10 +1722,11 @@ const message = {
|
||||
typeStandard_IA: 'Standard_IA',
|
||||
typeArchive: 'Archive',
|
||||
typeDeep_Archive: 'Deep_Archive',
|
||||
scLighthouse: 'Default, Lightweight object storage only supports this storage type',
|
||||
scStandard:
|
||||
'Standard Storage is suitable for business scenarios with a large number of hot files that require real-time access, frequent data interaction, and so on.',
|
||||
'Standard storage, suitable for business scenarios with large amounts of frequently accessed hot files and frequent data interactions.',
|
||||
scStandard_IA:
|
||||
'Low-frequency storage is suitable for business scenarios with relatively low access frequency, and stores data for at least 30 days.',
|
||||
'Infrequent access storage, suitable for business scenarios with lower access frequency (e.g., average monthly access frequency of 1-2 times), minimum storage duration of 30 days.',
|
||||
scArchive: 'Archival storage is suitable for business scenarios with extremely low access frequency.',
|
||||
scDeep_Archive: 'Durable cold storage is suitable for business scenarios with extremely low access frequency.',
|
||||
archiveHelper:
|
||||
@@ -2863,7 +2870,7 @@ const message = {
|
||||
apiInterfaceHelper:
|
||||
'Supports panel API interface access (this feature needs to be enabled for the panel app)',
|
||||
apiInterfaceHelper1:
|
||||
"Panel app access requires adding the visitor to the whitelist; for non-fixed IPs, it's recommended to add 0.0.0.0/0",
|
||||
"Panel app access requires adding the visitor to the whitelist; for non-fixed IPs, it's recommended to add 0.0.0.0/0 (all IPv4), ::/0 (all IPv6)",
|
||||
qrCodeExpired: 'Refresh time',
|
||||
apiLeakageHelper: 'Do not disclose the QR code. Ensure it is used only in trusted environments.',
|
||||
},
|
||||
@@ -3282,6 +3289,9 @@ const message = {
|
||||
loginBgColorHelper: 'Displayed as background color on the login page',
|
||||
image: 'Image',
|
||||
bgColor: 'Background Color',
|
||||
loginGroup: 'Login Page',
|
||||
loginBtnLinkColor: 'Button/Link Color',
|
||||
loginBtnLinkColorHelper: 'Will be displayed as the button/link color on the login page',
|
||||
},
|
||||
helper: {
|
||||
wafTitle1: 'Interception Map',
|
||||
|
||||
@@ -48,6 +48,7 @@ const message = {
|
||||
verify: '確認する',
|
||||
saveAndEnable: '保存して有効にします',
|
||||
import: '輸入',
|
||||
export: 'エクスポート',
|
||||
power: '認可',
|
||||
search: '検索',
|
||||
refresh: 'リロード',
|
||||
@@ -287,6 +288,7 @@ const message = {
|
||||
normal: '普通',
|
||||
building: '建物',
|
||||
upgrading: 'アップグレード',
|
||||
pending: '編集待ち',
|
||||
rebuilding: '再構築',
|
||||
deny: '拒否されました',
|
||||
accept: '受け入れられました',
|
||||
@@ -958,6 +960,10 @@ const message = {
|
||||
cronjob: {
|
||||
create: 'Cronジョブを作成します',
|
||||
edit: 'Cronジョブを編集します',
|
||||
errImport: 'ファイル内容異常:',
|
||||
errImportFormat: 'インポートしたスケジュールタスクのデータまたは形式が異常です。確認して再試行してください!',
|
||||
importHelper:
|
||||
'インポート時に同名のスケジュールタスクは自動的にスキップされます。タスクはデフォルトで【無効】状態に設定され、データ関連付け異常時には【編集待ち】状態に設定されます。',
|
||||
changeStatus: 'ステータスを変更します',
|
||||
disableMsg: 'これにより、スケジュールされたタスクが自動的に実行されなくなります。続けたいですか?',
|
||||
enableMsg: 'これにより、スケジュールされたタスクが自動的に実行されます。続けたいですか?',
|
||||
@@ -1551,7 +1557,7 @@ const message = {
|
||||
apiKeyHelper: 'APIキーは、サードパーティアプリケーションに使用されてAPIにアクセスします。',
|
||||
ipWhiteList: 'IP AllowList',
|
||||
ipWhiteListEgs: '1行に1つ。たとえば、 n172.161.10.111 n172.161.10.0/24',
|
||||
ipWhiteListHelper: 'AllowList内のIPSはAPIにアクセスできます。',
|
||||
ipWhiteListHelper: 'AllowList内のIPSはAPIにアクセスできます、0.0.0.0/0(すべての IPv4)、::/0(すべての IPv6)',
|
||||
apiKeyReset: 'インターフェイスキーリセット',
|
||||
apiKeyResetHelper: '関連するキーサービスは無効になります。サービスに新しいキーを追加してください',
|
||||
confDockerProxy: 'Dockerプロキシを構成します',
|
||||
@@ -1637,6 +1643,7 @@ const message = {
|
||||
typeStandard_IA: '低頻度ストレージ',
|
||||
typeArchive: 'アーカイブストレージ',
|
||||
typeDeep_Archive: '深層アーカイブストレージ',
|
||||
scLighthouse: 'デフォルト:軽量オブジェクトストレージはこのストレージタイプのみサポートしています',
|
||||
scStandard:
|
||||
'標準ストレージは、リアルタイムアクセスが多いホットファイルや頻繁なデータ交換などの業務シナリオに適しています。',
|
||||
scStandard_IA:
|
||||
@@ -2741,7 +2748,7 @@ const message = {
|
||||
apiInterfaceHelper:
|
||||
'パネルAPIインターフェースアクセスをサポート(この機能はパネルアプリで有効にする必要があります)',
|
||||
apiInterfaceHelper1:
|
||||
'パネルアプリのアクセスには訪問者をホワイトリストに追加する必要があります。固定IPでない場合、0.0.0.0/0 を追加することをお勧めします',
|
||||
'パネルアプリのアクセスには訪問者をホワイトリストに追加する必要があります。固定IPでない場合、0.0.0.0/0(すべての IPv4)、::/0(すべての IPv6)を追加することをお勧めします',
|
||||
qrCodeExpired: 'リフレッシュ時間',
|
||||
apiLeakageHelper: 'QRコードを漏洩しないでください。信頼できる環境でのみ使用してください。',
|
||||
},
|
||||
@@ -3151,6 +3158,9 @@ const message = {
|
||||
loginBgColorHelper: 'ログインページの背景色として表示されます',
|
||||
image: '画像',
|
||||
bgColor: '背景色',
|
||||
loginGroup: 'ログインページ',
|
||||
loginBtnLinkColor: 'ボタン/リンクの色',
|
||||
loginBtnLinkColorHelper: 'ログインページに表示されるボタン/リンクの色になります',
|
||||
},
|
||||
helper: {
|
||||
wafTitle1: 'インターセプションマップ',
|
||||
|
||||
@@ -48,6 +48,7 @@ const message = {
|
||||
verify: '검증',
|
||||
saveAndEnable: '저장 및 활성화',
|
||||
import: '가져오기',
|
||||
export: '내보내기',
|
||||
power: '권한 부여',
|
||||
search: '검색',
|
||||
refresh: '새로고침',
|
||||
@@ -289,6 +290,7 @@ const message = {
|
||||
normal: '정상',
|
||||
building: '빌드 중',
|
||||
upgrading: '업그레이드 중',
|
||||
pending: '편집 대기',
|
||||
rebuilding: '재빌드 중',
|
||||
deny: '거부됨',
|
||||
accept: '수락됨',
|
||||
@@ -948,6 +950,10 @@ const message = {
|
||||
cronjob: {
|
||||
create: '크론 작업 생성',
|
||||
edit: '크론 작업 수정',
|
||||
errImport: '파일 내용 이상:',
|
||||
errImportFormat: '가져온 예약 작업 데이터 또는 형식이 이상합니다. 확인 후 다시 시도하십시오!',
|
||||
importHelper:
|
||||
'가져오기 시 동일한 이름의 예약 작업은 자동으로 건너뜁니다. 작업은 기본적으로 【비활성화】 상태로 설정되며, 데이터 연동 이상 시 【편집 대기】 상태로 설정됩니다.',
|
||||
changeStatus: '상태 변경',
|
||||
disableMsg: '이 작업은 예약된 작업이 자동으로 실행되지 않도록 멈춥니다. 계속하시겠습니까?',
|
||||
enableMsg: '이 작업은 예약된 작업이 자동으로 실행되도록 허용합니다. 계속하시겠습니까?',
|
||||
@@ -1532,7 +1538,7 @@ const message = {
|
||||
apiKeyHelper: 'API 키는 서드파티 애플리케이션이 API 에 접근하는 데 사용됩니다.',
|
||||
ipWhiteList: 'IP 허용 목록',
|
||||
ipWhiteListEgs: '한 줄에 하나씩 입력하십시오. 예:\n172.161.10.111\n172.161.10.0/24',
|
||||
ipWhiteListHelper: '허용 목록에 있는 IP만 API 에 접근할 수 있습니다.',
|
||||
ipWhiteListHelper: '허용 목록에 있는 IP만 API 에 접근할 수 있습니다. 0.0.0.0/0(모든 IPv4), ::/0(모든 IPv6)',
|
||||
apiKeyValidityTime: '인터페이스 키 유효 기간',
|
||||
apiKeyValidityTimeEgs: '인터페이스 키 유효 기간 (분 단위)',
|
||||
apiKeyValidityTimeHelper:
|
||||
@@ -1620,6 +1626,7 @@ const message = {
|
||||
typeStandard_IA: '저빈도 표준',
|
||||
typeArchive: '아카이브',
|
||||
typeDeep_Archive: '심층 아카이브',
|
||||
scLighthouse: '기본값: 경량 오브젝트 스토리지는 이 스토리지 유형만 지원합니다',
|
||||
scStandard:
|
||||
'표준 스토리지는 실시간 접근, 빈번한 데이터 상호작용이 필요한 핫 파일이 많은 비즈니스 시나리오에 적합합니다.',
|
||||
scStandard_IA:
|
||||
@@ -2694,7 +2701,7 @@ const message = {
|
||||
apiStatusHelper: '패널 APP 는 API 인터페이스 기능을 활성화해야 합니다',
|
||||
apiInterfaceHelper: '패널 API 인터페이스 액세스를 지원합니다(패널 앱에서 이 기능을 활성화해야 합니다)',
|
||||
apiInterfaceHelper1:
|
||||
'패널 앱의 액세스에는 방문자를 화이트리스트에 추가해야 하며, 고정 IP가 아닌 경우 0.0.0.0/0 을 추가하는 것이 좋습니다',
|
||||
'패널 앱의 액세스에는 방문자를 화이트리스트에 추가해야 하며, 고정 IP가 아닌 경우 0.0.0.0/0(모든 IPv4), ::/0(모든 IPv6) 을 추가하는 것이 좋습니다',
|
||||
qrCodeExpired: '새로 고침 시간',
|
||||
apiLeakageHelper: 'QR 코드를 누출하지 마십시오. 신뢰할 수 있는 환경에서만 사용하십시오.',
|
||||
},
|
||||
@@ -3099,6 +3106,9 @@ const message = {
|
||||
loginBgColorHelper: '로그인 페이지의 배경 색상으로 표시됩니다',
|
||||
image: '이미지',
|
||||
bgColor: '배경 색상',
|
||||
loginGroup: '로그인 페이지',
|
||||
loginBtnLinkColor: '버튼/링크 색상',
|
||||
loginBtnLinkColorHelper: '로그인 페이지의 버튼/링크 색상으로 표시됩니다',
|
||||
},
|
||||
helper: {
|
||||
wafTitle1: '차단 지도',
|
||||
|
||||
@@ -48,6 +48,7 @@ const message = {
|
||||
verify: 'Sahkan',
|
||||
saveAndEnable: 'Simpan dan aktifkan',
|
||||
import: 'Import',
|
||||
export: 'Eksport',
|
||||
power: 'Pemberian Kuasa',
|
||||
search: 'Cari',
|
||||
refresh: 'Segarkan',
|
||||
@@ -295,6 +296,7 @@ const message = {
|
||||
normal: 'Normal',
|
||||
building: 'Sedang Membina',
|
||||
upgrading: 'Sedang Meningkatkan',
|
||||
pending: 'Menunggu Edit',
|
||||
rebuilding: 'Sedang Membina Semula',
|
||||
deny: 'Ditolak',
|
||||
accept: 'Diterima',
|
||||
@@ -979,6 +981,10 @@ const message = {
|
||||
cronjob: {
|
||||
create: 'Cipta tugas cron',
|
||||
edit: 'Edit tugas cron',
|
||||
errImport: 'Kandungan fail tidak normal:',
|
||||
errImportFormat: 'Data atau format tugas terjadual yang diimport tidak normal. Sila semak dan cuba lagi!',
|
||||
importHelper:
|
||||
'Tugas terjadual dengan nama sama akan dilangkau secara automatik semasa import. Tugas akan ditetapkan ke status 【Lumpuh】 secara lalai, dan ditetapkan ke status 【Menunggu Edit】 apabila perkaitan data tidak normal.',
|
||||
changeStatus: 'Tukar status',
|
||||
disableMsg:
|
||||
'Ini akan menghentikan tugas berjadual daripada dilaksanakan secara automatik. Adakah anda mahu meneruskan?',
|
||||
@@ -1595,7 +1601,7 @@ const message = {
|
||||
apiKeyHelper: 'Kunci API digunakan untuk aplikasi pihak ketiga mengakses API.',
|
||||
ipWhiteList: 'Senarai putih IP',
|
||||
ipWhiteListEgs: 'Satu per baris. Contoh,\n172.161.10.111\n172.161.10.0/24',
|
||||
ipWhiteListHelper: 'IP dalam senarai putih boleh mengakses API.',
|
||||
ipWhiteListHelper: 'IP dalam senarai putih boleh mengakses API, 0.0.0.0/0 (semua IPv4), ::/0 (semua IPv6)',
|
||||
apiKeyValidityTime: 'Tempoh sah kunci antara muka',
|
||||
apiKeyValidityTimeEgs: 'Tempoh sah kunci antara muka (dalam minit)',
|
||||
apiKeyValidityTimeHelper:
|
||||
@@ -1689,6 +1695,7 @@ const message = {
|
||||
typeStandard_IA: 'Standard_IA',
|
||||
typeArchive: 'Archive',
|
||||
typeDeep_Archive: 'Deep_Archive',
|
||||
scLighthouse: 'Lalai, Penyimpanan objek ringan hanya menyokong jenis penyimpanan ini',
|
||||
scStandard:
|
||||
'Storan Standard sesuai untuk senario perniagaan dengan sejumlah besar fail panas yang memerlukan akses masa nyata, interaksi data yang kerap, dan sebagainya.',
|
||||
scStandard_IA:
|
||||
@@ -2802,7 +2809,7 @@ const message = {
|
||||
apiInterfaceHelper:
|
||||
'Menyokong akses antara muka API panel (fungsi ini perlu diaktifkan untuk aplikasi panel)',
|
||||
apiInterfaceHelper1:
|
||||
'Akses aplikasi panel memerlukan penambahan pelawat ke dalam senarai putih, untuk IP yang tidak tetap, disyorkan untuk menambah 0.0.0.0/0',
|
||||
'Akses aplikasi panel memerlukan penambahan pelawat ke dalam senarai putih, untuk IP yang tidak tetap, disyorkan untuk menambah 0.0.0.0/0(semua IPv4), ::/0 (semua IPv6)',
|
||||
qrCodeExpired: 'Masa penyegaran',
|
||||
apiLeakageHelper: 'Jangan dedahkan kod QR. Pastikan ia hanya digunakan di persekitaran yang dipercayai.',
|
||||
},
|
||||
@@ -3222,6 +3229,9 @@ const message = {
|
||||
loginBgColorHelper: 'Akan dipaparkan sebagai warna latar belakang halaman log masuk',
|
||||
image: 'Imej',
|
||||
bgColor: 'Warna Latar',
|
||||
loginGroup: 'Halaman Log Masuk',
|
||||
loginBtnLinkColor: 'Warna Butang/Pautan',
|
||||
loginBtnLinkColorHelper: 'Akan dipaparkan sebagai warna butang/pautan di halaman log masuk',
|
||||
},
|
||||
helper: {
|
||||
wafTitle1: 'Peta Pencegahan',
|
||||
|
||||
@@ -48,6 +48,7 @@ const message = {
|
||||
verify: 'Verificar',
|
||||
saveAndEnable: 'Salvar e ativar',
|
||||
import: 'Importar',
|
||||
export: 'Exportar',
|
||||
power: 'Autorização',
|
||||
search: 'Pesquisar',
|
||||
refresh: 'Atualizar',
|
||||
@@ -293,6 +294,7 @@ const message = {
|
||||
normal: 'Normal',
|
||||
building: 'Construindo',
|
||||
upgrading: 'Atualizando',
|
||||
pending: 'Aguardando Edição',
|
||||
rebuilding: 'Reconstruindo',
|
||||
deny: 'Negado',
|
||||
accept: 'Aceito',
|
||||
@@ -975,6 +977,11 @@ const message = {
|
||||
cronjob: {
|
||||
create: 'Criar tarefa cron',
|
||||
edit: 'Editar tarefa cron',
|
||||
errImport: 'Conteúdo do arquivo anormal:',
|
||||
errImportFormat:
|
||||
'Os dados ou formato da tarefa agendada estão anormais. Por favor, verifique e tente novamente!',
|
||||
importHelper:
|
||||
'Tarefas agendadas duplicadas serão automaticamente ignoradas durante a importação. As tarefas serão definidas como status 【Desativado】 por padrão, e como status 【Aguardando Edição】 quando a associação de dados for anormal.',
|
||||
changeStatus: 'Alterar status',
|
||||
disableMsg: 'Isso irá parar a execução automática da tarefa agendada. Você deseja continuar?',
|
||||
enableMsg: 'Isso permitirá que a tarefa agendada seja executada automaticamente. Você deseja continuar?',
|
||||
@@ -1583,7 +1590,8 @@ const message = {
|
||||
apiKeyHelper: 'A chave da API é usada para aplicativos de terceiros acessarem a API.',
|
||||
ipWhiteList: 'Lista de IPs permitidos',
|
||||
ipWhiteListEgs: 'Um por linha. Exemplo: \n172.161.10.111\n172.161.10.0/24',
|
||||
ipWhiteListHelper: 'IPs na lista de permitidos podem acessar a API.',
|
||||
ipWhiteListHelper:
|
||||
'IPs na lista de permitidos podem acessar a API, 0.0.0.0/0 (todos os IPv4), ::/0 (todos os IPv6)',
|
||||
apiKeyReset: 'Redefinir chave da interface',
|
||||
apiKeyResetHelper:
|
||||
'O serviço associado à chave se tornará inválido. Por favor, adicione uma nova chave ao serviço',
|
||||
@@ -1673,6 +1681,7 @@ const message = {
|
||||
typeStandard_IA: 'Padrão_IA',
|
||||
typeArchive: 'Arquivo',
|
||||
typeDeep_Archive: 'Arquivo Profundo',
|
||||
scLighthouse: 'Padrão, O armazenamento de objetos leve só suporta este tipo de armazenamento',
|
||||
scStandard:
|
||||
'O armazenamento padrão é adequado para cenários de negócios com grande volume de arquivos quentes que exigem acesso em tempo real, interação frequente de dados, etc.',
|
||||
scStandard_IA:
|
||||
@@ -2804,7 +2813,7 @@ const message = {
|
||||
apiInterfaceHelper:
|
||||
'Suporta acesso à interface de API do painel (essa funcionalidade precisa ser ativada no aplicativo do painel)',
|
||||
apiInterfaceHelper1:
|
||||
'O acesso ao aplicativo do painel requer que o visitante seja adicionado à lista de permissões; para IPs não fixos, recomenda-se adicionar 0.0.0.0/0',
|
||||
'O acesso ao aplicativo do painel requer que o visitante seja adicionado à lista de permissões; para IPs não fixos, recomenda-se adicionar 0.0.0.0/0(todos os IPv4), ::/0 (todos os IPv6)',
|
||||
qrCodeExpired: 'Tempo de atualização',
|
||||
apiLeakageHelper: 'Não revele o QR code. Garanta que ele seja usado apenas em ambientes confiáveis.',
|
||||
},
|
||||
@@ -3229,6 +3238,9 @@ const message = {
|
||||
loginBgColorHelper: 'Será exibida como cor de fundo na página de login',
|
||||
image: 'Imagem',
|
||||
bgColor: 'Cor de Fundo',
|
||||
loginGroup: 'Página de login',
|
||||
loginBtnLinkColor: 'Cor do botão/link',
|
||||
loginBtnLinkColorHelper: 'Será exibido como a cor do botão/link na página de login',
|
||||
},
|
||||
helper: {
|
||||
wafTitle1: 'Mapa de Interceptação',
|
||||
|
||||
@@ -48,6 +48,7 @@ const message = {
|
||||
verify: 'Проверить',
|
||||
saveAndEnable: 'Сохранить и включить',
|
||||
import: 'Импорт',
|
||||
export: 'Экспорт',
|
||||
power: 'Авторизация',
|
||||
search: 'Поиск',
|
||||
refresh: 'Обновить',
|
||||
@@ -290,6 +291,7 @@ const message = {
|
||||
normal: 'Нормально',
|
||||
building: 'Сборка',
|
||||
upgrading: 'Обновление',
|
||||
pending: 'Ожидает редактирования',
|
||||
rebuilding: 'Пересборка',
|
||||
deny: 'Отказано',
|
||||
accept: 'Принято',
|
||||
@@ -972,6 +974,11 @@ const message = {
|
||||
cronjob: {
|
||||
create: 'Создать задачу cron',
|
||||
edit: 'Редактировать задачу cron',
|
||||
errImport: 'Аномальное содержимое файла:',
|
||||
errImportFormat:
|
||||
'Данные или формат запланированной задачи ненормальны. Пожалуйста, проверьте и повторите попытку!',
|
||||
importHelper:
|
||||
'Повторяющиеся запланированные задачи будут автоматически пропущены при импорте. По умолчанию задачи устанавливаются в статус 【Отключено】, а при аномальной ассоциации данных - в статус 【Ожидает редактирования】.',
|
||||
changeStatus: 'Изменить статус',
|
||||
disableMsg: 'Это остановит автоматическое выполнение запланированной задачи. Хотите продолжить?',
|
||||
enableMsg: 'Это позволит запланированной задаче автоматически выполняться. Хотите продолжить?',
|
||||
@@ -1582,7 +1589,8 @@ const message = {
|
||||
apiKeyHelper: 'API ключ используется сторонними приложениями для доступа к API.',
|
||||
ipWhiteList: 'Белый список IP',
|
||||
ipWhiteListEgs: 'По одному в строке. Например,\n172.161.10.111\n172.161.10.0/24',
|
||||
ipWhiteListHelper: 'IP-адреса из белого списка могут получить доступ к API.',
|
||||
ipWhiteListHelper:
|
||||
'IP-адреса из белого списка могут получить доступ к API, 0.0.0.0/0 (все IPv4), ::/0 (все IPv6)',
|
||||
apiKeyValidityTime: 'Срок действия ключа интерфейса',
|
||||
apiKeyValidityTimeEgs: 'Срок действия ключа интерфейса (в единицах)',
|
||||
apiKeyValidityTimeHelper:
|
||||
@@ -1675,6 +1683,7 @@ const message = {
|
||||
typeStandard_IA: 'Стандартный_IA',
|
||||
typeArchive: 'Архивный',
|
||||
typeDeep_Archive: 'Глубокий_Архив',
|
||||
scLighthouse: 'По умолчанию, Легковесное объектное хранилище поддерживает только этот тип хранилища',
|
||||
scStandard:
|
||||
'Стандартное хранилище подходит для бизнес-сценариев с большим количеством горячих файлов, требующих мгновенного доступа, частого обмена данными и т.д.',
|
||||
scStandard_IA:
|
||||
@@ -2796,7 +2805,7 @@ const message = {
|
||||
apiInterfaceHelper:
|
||||
'Поддержка доступа к API-интерфейсу панели (эта функция должна быть включена для мобильного приложения панели)',
|
||||
apiInterfaceHelper1:
|
||||
'Для доступа к приложению панели необходимо добавить посетителя в белый список. Для нестабильных IP рекомендуется добавить 0.0.0.0/0',
|
||||
'Для доступа к приложению панели необходимо добавить посетителя в белый список. Для нестабильных IP рекомендуется добавить 0.0.0.0/0 (все IPv4), ::/0 (все IPv6)',
|
||||
qrCodeExpired: 'Время обновления',
|
||||
apiLeakageHelper: 'Не раскрывайте QR-код. Убедитесь, что он используется только в доверенных средах.',
|
||||
},
|
||||
@@ -3214,6 +3223,9 @@ const message = {
|
||||
loginBgColorHelper: 'Будет отображаться как цвет фона страницы входа',
|
||||
image: 'Изображение',
|
||||
bgColor: 'Цвет фона',
|
||||
loginGroup: 'Страница входа',
|
||||
loginBtnLinkColor: 'Цвет кнопки/ссылки',
|
||||
loginBtnLinkColorHelper: 'Будет отображаться как цвет кнопки/ссылки на странице входа',
|
||||
},
|
||||
helper: {
|
||||
wafTitle1: 'Карта Перехватов',
|
||||
|
||||
@@ -50,6 +50,7 @@ const message = {
|
||||
verify: 'Doğrula',
|
||||
saveAndEnable: 'Kaydet ve etkinleştir',
|
||||
import: 'İçe Aktar',
|
||||
export: 'Dışa Aktar',
|
||||
power: 'Yetkilendirme',
|
||||
search: 'Ara',
|
||||
refresh: 'Yenile',
|
||||
@@ -302,6 +303,7 @@ const message = {
|
||||
normal: 'Normal',
|
||||
building: 'İnşa Ediliyor',
|
||||
upgrading: 'Yükseltiliyor',
|
||||
pending: 'Düzenleme Bekliyor',
|
||||
rebuilding: 'Yeniden İnşa Ediliyor',
|
||||
deny: 'Reddedildi',
|
||||
accept: 'Kabul Edildi',
|
||||
@@ -1000,6 +1002,10 @@ const message = {
|
||||
cronjob: {
|
||||
create: 'Cron görevi oluştur',
|
||||
edit: 'Cron görevini düzenle',
|
||||
errImport: 'Dosya içeriği anormal:',
|
||||
errImportFormat: 'Zamanlanmış görev verileri veya biçimi anormal. Lütfen kontrol edip tekrar deneyin!',
|
||||
importHelper:
|
||||
'İçe aktarım sırasında aynı isimli zamanlanmış görevler otomatik olarak atlanacaktır. Görevler varsayılan olarak 【Devre Dışı】 durumuna ayarlanır ve veri ilişkilendirme anormalse 【Düzenleme Bekliyor】 durumuna ayarlanır.',
|
||||
changeStatus: 'Durumu değiştir',
|
||||
disableMsg: 'Bu, zamanlanmış görevin otomatik olarak yürütülmesini durduracaktır. Devam etmek istiyor musunuz?',
|
||||
enableMsg:
|
||||
@@ -1624,7 +1630,7 @@ const message = {
|
||||
apiKeyHelper: 'API anahtarı, üçüncü taraf uygulamaların API’ye erişmesi için kullanılır.',
|
||||
ipWhiteList: 'IP izin listesi',
|
||||
ipWhiteListEgs: 'Her satıra bir tane. Örneğin,\n172.161.10.111\n172.161.10.0/24',
|
||||
ipWhiteListHelper: 'İzin listesindeki IP’ler API’ye erişebilir.',
|
||||
ipWhiteListHelper: 'İzin listesindeki IP’ler API’ye erişebilir, 0.0.0.0/0 (tüm IPv4), ::/0 (tüm IPv6)',
|
||||
apiKeyValidityTime: 'Arayüz anahtarının geçerlilik süresi',
|
||||
apiKeyValidityTimeEgs: 'Arayüz anahtarının geçerlilik süresi (dakika cinsinden)',
|
||||
apiKeyValidityTimeHelper:
|
||||
@@ -1735,6 +1741,7 @@ const message = {
|
||||
typeStandard_IA: 'Standart_IA',
|
||||
typeArchive: 'Arşiv',
|
||||
typeDeep_Archive: 'Derin_Arşiv',
|
||||
scLighthouse: 'Varsayılan, Hafif nesne depolama yalnızca bu depolama türünü destekler',
|
||||
scStandard:
|
||||
'Standart Depolama, gerçek zamanlı erişim gerektiren çok sayıda sıcak dosya içeren iş senaryoları için uygundur, sık veri etkileşimi vb.',
|
||||
scStandard_IA:
|
||||
@@ -2900,7 +2907,7 @@ const message = {
|
||||
apiInterfaceHelper:
|
||||
'Panel API arayüz erişimini destekler (bu özellik, panel uygulaması için etkinleştirilmelidir)',
|
||||
apiInterfaceHelper1:
|
||||
'Panel uygulama erişimi, ziyaretçinin beyaz listeye eklenmesini gerektirir; sabit olmayan IP’ler için 0.0.0.0/0 eklenmesi önerilir',
|
||||
'Panel uygulama erişimi, ziyaretçinin beyaz listeye eklenmesini gerektirir; sabit olmayan IP’ler için 0.0.0.0/0(tüm IPv4), ::/0 (tüm IPv6) eklenmesi önerilir',
|
||||
qrCodeExpired: 'Yenileme zamanı',
|
||||
apiLeakageHelper: 'QR kodunu paylaşmayın. Yalnızca güvenilir ortamlarda kullanıldığından emin olun.',
|
||||
},
|
||||
@@ -3316,6 +3323,9 @@ const message = {
|
||||
loginBgColorHelper: 'Giriş sayfasında arka plan rengi olarak gösterilir',
|
||||
image: 'Görüntü',
|
||||
bgColor: 'Arka Plan Rengi',
|
||||
loginGroup: 'Giriş Sayfası',
|
||||
loginBtnLinkColor: 'Buton/Bağlantı Rengi',
|
||||
loginBtnLinkColorHelper: 'Giriş sayfasındaki buton/bağlantı rengi olarak gösterilecektir',
|
||||
},
|
||||
helper: {
|
||||
wafTitle1: 'Engelleme Haritası',
|
||||
|
||||
@@ -51,6 +51,7 @@ const message = {
|
||||
saveAndEnable: '保存並啟用',
|
||||
import: '導入',
|
||||
power: '授權',
|
||||
export: '導出',
|
||||
search: '搜索',
|
||||
refresh: '刷新',
|
||||
get: '獲取',
|
||||
@@ -289,6 +290,7 @@ const message = {
|
||||
normal: '正常',
|
||||
building: '製作鏡像中',
|
||||
upgrading: '升級中',
|
||||
pending: '待編輯',
|
||||
rebuilding: '重建中',
|
||||
deny: '已屏蔽',
|
||||
accept: '已放行',
|
||||
@@ -942,6 +944,10 @@ const message = {
|
||||
cronjob: {
|
||||
create: '創建計劃任務',
|
||||
edit: '編輯計劃任務',
|
||||
errImport: '文件內容異常:',
|
||||
errImportFormat: '導入的計劃任務數據或格式異常,請檢查後重試!',
|
||||
importHelper:
|
||||
'導入時將自動跳過重名計劃任務。任務默認設置為【停用】狀態,數據關聯異常時,設置為【待編輯】狀態。',
|
||||
changeStatus: '狀態修改',
|
||||
disableMsg: '停止計劃任務會導致該任務不再自動執行。是否繼續?',
|
||||
enableMsg: '啟用計劃任務會讓該任務定期自動執行。是否繼續?',
|
||||
@@ -1528,7 +1534,8 @@ const message = {
|
||||
apiKeyHelper: '接口密钥用於外部應用訪問 API 接口',
|
||||
ipWhiteList: 'IP白名單',
|
||||
ipWhiteListEgs: '當存在多個 IP 時,需要換行顯示,例:\n172.16.10.111 \n172.16.10.0/24',
|
||||
ipWhiteListHelper: '必需在 IP 白名單清單中的 IP 才能訪問面板 API 接口',
|
||||
ipWhiteListHelper:
|
||||
'必需在 IP 白名單清單中的 IP 才能訪問面板 API 接口,0.0.0.0/0(所有 IPv4),::/0(所有 IPv6)',
|
||||
apiKeyValidityTime: '介面金鑰有效期',
|
||||
apiKeyValidityTimeEgs: '介面金鑰有效期(組織分)',
|
||||
apiKeyValidityTimeHelper: '介面時間戳記到請求時的當前時間戳之間有效(組織分),設定為0時,不做時間戳記校驗',
|
||||
@@ -1620,6 +1627,7 @@ const message = {
|
||||
typeStandard_IA: '低頻存儲',
|
||||
typeArchive: '歸檔存儲',
|
||||
typeDeep_Archive: '深度歸檔存儲',
|
||||
scLighthouse: '預設,輕量物件儲存僅支援該儲存類型',
|
||||
scStandard: '標準存儲,適用於實時訪問的大量熱點文件、頻繁的數據交互等業務場景。',
|
||||
scStandard_IA: '低頻存儲,適用於較低訪問頻率(例如平均每月訪問頻率1到2次)的業務場景,最少存儲30天。',
|
||||
scArchive: '歸檔存儲,適用於極低訪問頻率(例如半年訪問1次)的業務場景。',
|
||||
@@ -2668,7 +2676,8 @@ const message = {
|
||||
qrCode: '二維碼',
|
||||
apiStatusHelper: '面板 APP 需要開啟 API 接口功能',
|
||||
apiInterfaceHelper: '支持面板 API 接口訪問功能(面板 APP 需要開啟該功能)',
|
||||
apiInterfaceHelper1: '面板 APP 訪問需將訪問者添加至白名單,非固定 IP 建議添加 0.0.0.0/0',
|
||||
apiInterfaceHelper1:
|
||||
'面板 APP 訪問需將訪問者添加至白名單,非固定 IP 建議添加 0.0.0.0/0(所有 IPv4),::/0(所有 IPv6)',
|
||||
qrCodeExpired: '刷新時間',
|
||||
apiLeakageHelper: '請勿洩露二維碼,確保僅在受信任的環境中使用',
|
||||
},
|
||||
@@ -3062,6 +3071,9 @@ const message = {
|
||||
loginBgColorHelper: '將會顯示在登入頁面背景顏色',
|
||||
image: '圖片',
|
||||
bgColor: '背景色',
|
||||
loginGroup: '登入頁面',
|
||||
loginBtnLinkColor: '按鈕顏色',
|
||||
loginBtnLinkColorHelper: '將顯示為登入頁面上的按鈕顏色',
|
||||
},
|
||||
helper: {
|
||||
wafTitle1: '攔截地圖',
|
||||
|
||||
@@ -50,6 +50,7 @@ const message = {
|
||||
verify: '验证',
|
||||
saveAndEnable: '保存并启用',
|
||||
import: '导入',
|
||||
export: '导出',
|
||||
power: '授权',
|
||||
search: '搜索',
|
||||
refresh: '刷新',
|
||||
@@ -287,6 +288,7 @@ const message = {
|
||||
normal: '正常',
|
||||
building: '制作镜像中',
|
||||
upgrading: '升级中',
|
||||
pending: '待编辑',
|
||||
rebuilding: '重建中',
|
||||
deny: '已屏蔽',
|
||||
accept: '已放行',
|
||||
@@ -940,6 +942,10 @@ const message = {
|
||||
cronjob: {
|
||||
create: '创建计划任务',
|
||||
edit: '编辑计划任务',
|
||||
errImport: '文件内容异常:',
|
||||
errImportFormat: '导入的计划任务数据或格式异常,请检查后重试!',
|
||||
importHelper:
|
||||
'导入时将自动跳过重名计划任务。任务默认设置为【停用】状态,数据关联异常时,设置为【待编辑】状态。',
|
||||
changeStatus: '状态修改',
|
||||
disableMsg: '停止计划任务会导致该任务不再自动执行。是否继续?',
|
||||
enableMsg: '启用计划任务会让该任务定期自动执行。是否继续?',
|
||||
@@ -1523,7 +1529,8 @@ const message = {
|
||||
apiKeyHelper: '接口密钥用于外部应用访问 API 接口',
|
||||
ipWhiteList: 'IP 白名单',
|
||||
ipWhiteListEgs: '当存在多个 IP 时,需要换行显示,例: \n172.16.10.111 \n172.16.10.0/24',
|
||||
ipWhiteListHelper: '必需在 IP 白名单列表中的 IP 才能访问面板 API 接口',
|
||||
ipWhiteListHelper:
|
||||
'必需在 IP 白名单列表中的 IP 才能访问面板 API 接口,0.0.0.0/0(所有 IPv4),::/0(所有 IPv6)',
|
||||
apiKeyValidityTime: '接口密钥有效期',
|
||||
apiKeyValidityTimeEgs: '接口密钥有效期(单位分)',
|
||||
apiKeyValidityTimeHelper: '接口时间戳到请求时的当前时间戳之间有效(单位分),设置为 0 时,不做时间戳校验',
|
||||
@@ -1615,6 +1622,7 @@ const message = {
|
||||
typeStandard_IA: '低频存储',
|
||||
typeArchive: '归档存储',
|
||||
typeDeep_Archive: '深度归档存储',
|
||||
scLighthouse: '默认,轻量对象存储仅支持该存储类型',
|
||||
scStandard: '标准存储,适用于实时访问的大量热点文件、频繁的数据交互等业务场景。',
|
||||
scStandard_IA: '低频存储,适用于较低访问频率(例如平均每月访问频率1到2次)的业务场景,最少存储30天。',
|
||||
scArchive: '归档存储,适用于极低访问频率(例如半年访问1次)的业务场景。',
|
||||
@@ -3032,6 +3040,9 @@ const message = {
|
||||
loginBgColorHelper: '将会显示在登录页面背景颜色',
|
||||
image: '图片',
|
||||
bgColor: '背景色',
|
||||
loginGroup: '登录页',
|
||||
loginBtnLinkColor: '按钮/链接颜色',
|
||||
loginBtnLinkColorHelper: '将会显示在登录页面按钮/链接颜色',
|
||||
},
|
||||
helper: {
|
||||
wafTitle1: '拦截地图',
|
||||
@@ -3399,7 +3410,8 @@ const message = {
|
||||
qrCode: '二维码',
|
||||
apiStatusHelper: '面板 APP 需要开启 API 接口功能',
|
||||
apiInterfaceHelper: '支持面板 API 接口访问功能(面板 APP 需要开启该功能)',
|
||||
apiInterfaceHelper1: '面板 APP 访问需将访问者添加至白名单,非固定 IP 建议添加 0.0.0.0/0',
|
||||
apiInterfaceHelper1:
|
||||
'面板 APP 访问需将访问者添加至白名单,非固定 IP 建议添加 0.0.0.0/0(所有 IPv4),::/0(所有 IPv6)',
|
||||
qrCodeExpired: '刷新时间',
|
||||
apiLeakageHelper: '请勿泄露二维码,确保仅在受信任的环境中使用',
|
||||
},
|
||||
|
||||
@@ -87,6 +87,7 @@ import { ref, watch } from 'vue';
|
||||
import bus from '@/global/bus';
|
||||
import { logOutApi } from '@/api/modules/auth';
|
||||
import router from '@/routers';
|
||||
import { loadProductProFromDB } from '@/utils/xpack';
|
||||
|
||||
const filter = ref();
|
||||
const globalStore = GlobalStore();
|
||||
@@ -176,6 +177,7 @@ const changeNode = (command: string) => {
|
||||
if (command == 'local') {
|
||||
globalStore.currentNode = command || 'local';
|
||||
globalStore.currentNodeAddr = '';
|
||||
loadProductProFromDB();
|
||||
router.push({ name: 'home' });
|
||||
return;
|
||||
}
|
||||
@@ -195,6 +197,7 @@ const changeNode = (command: string) => {
|
||||
}
|
||||
globalStore.currentNode = command || 'local';
|
||||
globalStore.currentNodeAddr = item.addr;
|
||||
loadProductProFromDB();
|
||||
router.push({ name: 'home' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ export interface ThemeConfigProp {
|
||||
loginImage: string;
|
||||
loginBgType: string;
|
||||
loginBackground: string;
|
||||
loginBtnLinkColor: string;
|
||||
themeColor: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ const GlobalStore = defineStore({
|
||||
loginImage: '',
|
||||
loginBackground: '',
|
||||
loginBgType: '',
|
||||
loginBtnLinkColor: '',
|
||||
},
|
||||
openMenuTabs: false,
|
||||
isFullScreen: false,
|
||||
|
||||
@@ -98,7 +98,7 @@ html.dark {
|
||||
--panel-logs-bg-color: var(--panel-main-bg-color-9);
|
||||
--panel-alert-bg-color: var(--panel-main-bg-color-10);
|
||||
|
||||
--el-menu-item-bg-color: var(--panel-main-bg-color-10);
|
||||
--el-menu-item-bg-color: var(--panel-main-bg-color-9);
|
||||
--el-menu-item-bg-color-active: var(--panel-main-bg-color-8);
|
||||
--el-menu-hover-bg-color: var(--panel-main-bg-color-8);
|
||||
--el-menu-text-color: var(--panel-main-bg-color-2);
|
||||
|
||||
@@ -114,6 +114,7 @@ export async function getXpackSettingForTheme() {
|
||||
globalStore.themeConfig.loginImage = res2.data?.loginImage;
|
||||
globalStore.themeConfig.loginBgType = res2.data?.loginBgType;
|
||||
globalStore.themeConfig.loginBackground = res2.data?.loginBackground;
|
||||
globalStore.themeConfig.loginBtnLinkColor = res2.data?.loginBtnLinkColor;
|
||||
globalStore.themeConfig.themeColor = res2.data?.themeColor;
|
||||
if (res2.data?.theme) {
|
||||
globalStore.themeConfig.theme = res2.data.theme;
|
||||
|
||||
@@ -78,7 +78,12 @@
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="allowPort" v-if="!paramModel.isHostMode">
|
||||
<el-checkbox v-model="paramModel.allowPort" :label="$t('app.allowPort')" size="large" />
|
||||
<el-checkbox
|
||||
v-model="paramModel.allowPort"
|
||||
:label="$t('app.allowPort')"
|
||||
size="large"
|
||||
@change="changeAllowPort"
|
||||
/>
|
||||
<span class="input-help">{{ $t('app.allowPortHelper') }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('app.specifyIP')" v-if="paramModel.allowPort" prop="specifyIP">
|
||||
@@ -223,6 +228,12 @@ const editParam = () => {
|
||||
edit.value = !edit.value;
|
||||
};
|
||||
|
||||
const changeAllowPort = () => {
|
||||
if (paramModel.value.allowPort) {
|
||||
paramModel.value.specifyIP = '';
|
||||
}
|
||||
};
|
||||
|
||||
const get = async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
|
||||
@@ -4,67 +4,68 @@
|
||||
:header="$t('commons.button.' + operateReq.operate)"
|
||||
:resource="resourceName"
|
||||
@close="handleClose"
|
||||
v-loading="loading"
|
||||
>
|
||||
<el-descriptions direction="vertical">
|
||||
<el-descriptions-item>
|
||||
<el-link @click="toLink(app.website)">
|
||||
<el-icon><OfficeBuilding /></el-icon>
|
||||
<span>{{ $t('app.appOfficeWebsite') }}</span>
|
||||
</el-link>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item>
|
||||
<el-link @click="toLink(app.document)">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>{{ $t('app.document') }}</span>
|
||||
</el-link>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item>
|
||||
<el-link @click="toLink(app.github)">
|
||||
<el-icon><Link /></el-icon>
|
||||
<span>{{ $t('app.github') }}</span>
|
||||
</el-link>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-form @submit.prevent ref="updateRef" :rules="rules" label-position="top" :model="operateReq">
|
||||
<el-form-item :label="$t('app.versionSelect')" prop="detailId">
|
||||
<el-select v-model="operateReq.version" @change="getVersions(operateReq.version)">
|
||||
<el-option
|
||||
v-if="operateReq.operate == 'ignore'"
|
||||
:value="'all'"
|
||||
:label="$t('commons.table.all') + $t('app.version')"
|
||||
></el-option>
|
||||
<el-option
|
||||
v-for="(version, index) in versions"
|
||||
:key="index"
|
||||
:value="version.version"
|
||||
:label="version.version"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item prop="backup" v-if="operateReq.operate === 'upgrade'">
|
||||
<el-checkbox v-model="operateReq.backup" :label="$t('app.backupApp')" />
|
||||
<span class="input-help">
|
||||
<el-text type="warning">{{ $t('app.backupAppHelper') }}</el-text>
|
||||
</span>
|
||||
</el-form-item>
|
||||
<el-form-item prop="pullImage" v-if="operateReq.operate === 'upgrade'">
|
||||
<el-checkbox v-model="operateReq.pullImage" :label="$t('app.pullImage')" size="large" />
|
||||
<span class="input-help">{{ $t('app.pullImageHelper') }}</span>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div v-loading="loading">
|
||||
<el-descriptions direction="vertical">
|
||||
<el-descriptions-item>
|
||||
<el-link @click="toLink(app.website)">
|
||||
<el-icon><OfficeBuilding /></el-icon>
|
||||
<span>{{ $t('app.appOfficeWebsite') }}</span>
|
||||
</el-link>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item>
|
||||
<el-link @click="toLink(app.document)">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>{{ $t('app.document') }}</span>
|
||||
</el-link>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item>
|
||||
<el-link @click="toLink(app.github)">
|
||||
<el-icon><Link /></el-icon>
|
||||
<span>{{ $t('app.github') }}</span>
|
||||
</el-link>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-form @submit.prevent ref="updateRef" :rules="rules" label-position="top" :model="operateReq">
|
||||
<el-form-item :label="$t('app.versionSelect')" prop="detailId">
|
||||
<el-select v-model="operateReq.version" @change="getVersions(operateReq.version)">
|
||||
<el-option
|
||||
v-if="operateReq.operate == 'ignore'"
|
||||
:value="'all'"
|
||||
:label="$t('commons.table.all') + $t('app.version')"
|
||||
></el-option>
|
||||
<el-option
|
||||
v-for="(version, index) in versions"
|
||||
:key="index"
|
||||
:value="version.version"
|
||||
:label="version.version"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item prop="backup" v-if="operateReq.operate === 'upgrade'">
|
||||
<el-checkbox v-model="operateReq.backup" :label="$t('app.backupApp')" />
|
||||
<span class="input-help">
|
||||
<el-text type="warning">{{ $t('app.backupAppHelper') }}</el-text>
|
||||
</span>
|
||||
</el-form-item>
|
||||
<el-form-item prop="pullImage" v-if="operateReq.operate === 'upgrade'">
|
||||
<el-checkbox v-model="operateReq.pullImage" :label="$t('app.pullImage')" size="large" />
|
||||
<span class="input-help">{{ $t('app.pullImageHelper') }}</span>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div v-if="operateReq.operate === 'upgrade'">
|
||||
<el-text type="warning">{{ $t('app.upgradeWarn') }}</el-text>
|
||||
<br />
|
||||
<el-button @click="openDiff()" type="primary">{{ $t('app.showDiff') }}</el-button>
|
||||
<div>
|
||||
<el-checkbox v-model="useNewCompose" :label="$t('app.useCustom')" size="large" />
|
||||
<div v-if="operateReq.operate === 'upgrade'">
|
||||
<el-text type="warning">{{ $t('app.upgradeWarn') }}</el-text>
|
||||
<br />
|
||||
<el-button @click="openDiff()" type="primary">{{ $t('app.showDiff') }}</el-button>
|
||||
<div>
|
||||
<el-checkbox v-model="useNewCompose" :label="$t('app.useCustom')" size="large" />
|
||||
</div>
|
||||
<div v-if="useNewCompose">
|
||||
<el-text type="danger">{{ $t('app.useCustomHelper') }}</el-text>
|
||||
</div>
|
||||
<CodemirrorPro v-if="useNewCompose" v-model="newCompose" mode="yaml"></CodemirrorPro>
|
||||
</div>
|
||||
<div v-if="useNewCompose">
|
||||
<el-text type="danger">{{ $t('app.useCustomHelper') }}</el-text>
|
||||
</div>
|
||||
<CodemirrorPro v-if="useNewCompose" v-model="newCompose" mode="yaml"></CodemirrorPro>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
<template>
|
||||
<DialogPro v-model="visible" :title="$t('commons.button.import')" size="large">
|
||||
<div>
|
||||
<el-alert :closable="false" show-icon type="info" :title="$t('cronjob.importHelper')" />
|
||||
<el-upload
|
||||
action="#"
|
||||
:auto-upload="false"
|
||||
ref="uploadRef"
|
||||
class="upload mt-2"
|
||||
:show-file-list="false"
|
||||
:limit="1"
|
||||
:accept="'.json'"
|
||||
:on-change="fileOnChange"
|
||||
:on-exceed="handleExceed"
|
||||
v-model:file-list="uploaderFiles"
|
||||
>
|
||||
<el-button class="float-left" type="primary">{{ $t('commons.button.upload') }}</el-button>
|
||||
</el-upload>
|
||||
|
||||
<el-button :disabled="selects.length === 0" @click="onImport" class="ml-2 mt-2">
|
||||
{{ $t('commons.button.import') }}
|
||||
</el-button>
|
||||
|
||||
<el-card class="mt-2 w-full" v-loading="loading">
|
||||
<el-table :data="data" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" fix />
|
||||
<el-table-column
|
||||
:label="$t('cronjob.taskName')"
|
||||
:min-width="120"
|
||||
prop="name"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
:label="$t('commons.table.type')"
|
||||
:min-width="120"
|
||||
prop="type"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag>{{ $t('cronjob.' + row.type) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('cronjob.cronSpec')" show-overflow-tooltip :min-width="120">
|
||||
<template #default="{ row }">
|
||||
<div v-for="(item, index) of row.spec.split(',')" :key="index">
|
||||
<div v-if="row.expand || (!row.expand && index < 3)">
|
||||
<span>
|
||||
{{ row.specCustom ? item : transSpecToStr(item) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!row.expand && row.spec.split(',').length > 3">
|
||||
<el-button type="primary" link @click="row.expand = true">
|
||||
{{ $t('commons.button.expand') }}...
|
||||
</el-button>
|
||||
</div>
|
||||
<div v-if="row.expand && row.spec.split(',').length > 3">
|
||||
<el-button type="primary" link @click="row.expand = false">
|
||||
{{ $t('commons.button.collapse') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('cronjob.retainCopies')" :min-width="120" prop="retainCopies" />
|
||||
</el-table>
|
||||
</el-card>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="visible = false">
|
||||
{{ $t('commons.button.cancel') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</DialogPro>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { transSpecToStr } from './../helper';
|
||||
import { genFileId, UploadFile, UploadFiles, UploadProps, UploadRawFile } from 'element-plus';
|
||||
import { MsgError, MsgSuccess } from '@/utils/message';
|
||||
import i18n from '@/lang';
|
||||
import { importCronjob } from '@/api/modules/cronjob';
|
||||
import { Cronjob } from '@/api/interface/cronjob';
|
||||
|
||||
const emit = defineEmits<{ (e: 'search'): void }>();
|
||||
|
||||
const visible = ref(false);
|
||||
const loading = ref();
|
||||
const selects = ref<any>([]);
|
||||
const data = ref();
|
||||
|
||||
const uploadRef = ref();
|
||||
const uploaderFiles = ref();
|
||||
|
||||
const acceptParams = (): void => {
|
||||
visible.value = true;
|
||||
data.value = [];
|
||||
};
|
||||
|
||||
const handleSelectionChange = (val: any) => {
|
||||
selects.value = val;
|
||||
};
|
||||
|
||||
const fileOnChange = (_uploadFile: UploadFile, uploadFiles: UploadFiles) => {
|
||||
loading.value = true;
|
||||
data.value = [];
|
||||
uploaderFiles.value = uploadFiles;
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
try {
|
||||
const content = e.target.result as string;
|
||||
const parsed = JSON.parse(content) as Cronjob.CronjobTrans;
|
||||
if (!Array.isArray(parsed)) {
|
||||
MsgError(i18n.global.t('cronjob.errImportFormat'));
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
for (const item of parsed) {
|
||||
if (!checkDataFormat(item)) {
|
||||
MsgError(i18n.global.t('cronjob.errImportFormat'));
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
data.value = parsed;
|
||||
loading.value = false;
|
||||
} catch (error) {
|
||||
MsgError(i18n.global.t('cronjob.errImport') + error.message);
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
reader.readAsText(_uploadFile.raw);
|
||||
};
|
||||
|
||||
const handleExceed: UploadProps['onExceed'] = (files) => {
|
||||
uploadRef.value!.clearFiles();
|
||||
const file = files[0] as UploadRawFile;
|
||||
file.uid = genFileId();
|
||||
uploadRef.value!.handleStart(file);
|
||||
};
|
||||
|
||||
const checkDataFormat = (item: any) => {
|
||||
if (!item.name) {
|
||||
return false;
|
||||
}
|
||||
const cronjobTypes = [
|
||||
'shell',
|
||||
'app',
|
||||
'website',
|
||||
'database',
|
||||
'directory',
|
||||
'log',
|
||||
'curl',
|
||||
'cutWebsiteLog',
|
||||
'clean',
|
||||
'snapshot',
|
||||
'ntp',
|
||||
];
|
||||
if (!item.type || cronjobTypes.indexOf(item.type) === -1) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const onImport = async () => {
|
||||
await importCronjob(selects.value).then(() => {
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
visible.value = false;
|
||||
emit('search');
|
||||
});
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
acceptParams,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.upload {
|
||||
width: 60px;
|
||||
float: left;
|
||||
}
|
||||
</style>
|
||||
@@ -16,6 +16,15 @@
|
||||
{{ $t('commons.button.delete') }}
|
||||
</el-button>
|
||||
</el-button-group>
|
||||
|
||||
<el-button-group class="ml-4">
|
||||
<el-button @click="onImport">
|
||||
{{ $t('commons.button.import') }}
|
||||
</el-button>
|
||||
<el-button :disabled="selects.length === 0" @click="onExport">
|
||||
{{ $t('commons.button.export') }}
|
||||
</el-button>
|
||||
</el-button-group>
|
||||
</template>
|
||||
<template #rightToolBar>
|
||||
<TableSearch @search="search()" v-model:searchName="searchName" />
|
||||
@@ -54,11 +63,12 @@
|
||||
:operate="true"
|
||||
/>
|
||||
<Status
|
||||
v-else
|
||||
v-if="row.status === 'Disable'"
|
||||
@click="onChangeStatus(row.id, 'enable')"
|
||||
:status="row.status"
|
||||
:operate="true"
|
||||
/>
|
||||
<Status v-if="row.status === 'Pending'" :status="row.status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('cronjob.cronSpec')" show-overflow-tooltip :min-width="120">
|
||||
@@ -166,7 +176,9 @@
|
||||
</el-form>
|
||||
</template>
|
||||
</OpDialog>
|
||||
<OpDialog ref="opExportRef" @search="search" @submit="onSubmitExport()" />
|
||||
<Records @search="search" ref="dialogRecordRef" />
|
||||
<Import @search="search" ref="dialogImportRef" />
|
||||
<Backups @search="search" ref="dialogBackupRef" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -174,8 +186,9 @@
|
||||
<script lang="ts" setup>
|
||||
import Records from '@/views/cronjob/cronjob/record/index.vue';
|
||||
import Backups from '@/views/cronjob/cronjob/backup/index.vue';
|
||||
import Import from '@/views/cronjob/cronjob/import/index.vue';
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
import { deleteCronjob, getCronjobPage, handleOnce, updateStatus } from '@/api/modules/cronjob';
|
||||
import { deleteCronjob, exportCronjob, getCronjobPage, handleOnce, updateStatus } from '@/api/modules/cronjob';
|
||||
import i18n from '@/lang';
|
||||
import { Cronjob } from '@/api/interface/cronjob';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
@@ -183,6 +196,7 @@ import { MsgSuccess } from '@/utils/message';
|
||||
import { hasBackup, transSpecToStr } from './helper';
|
||||
import { GlobalStore } from '@/store';
|
||||
import router from '@/routers';
|
||||
import { getCurrentDateFormatted } from '@/utils/util';
|
||||
|
||||
const globalStore = GlobalStore();
|
||||
const mobile = computed(() => {
|
||||
@@ -198,6 +212,8 @@ const opRef = ref();
|
||||
const showClean = ref();
|
||||
const cleanData = ref();
|
||||
const cleanRemoteData = ref();
|
||||
const opExportRef = ref();
|
||||
const dialogImportRef = ref();
|
||||
|
||||
const data = ref();
|
||||
const paginationConfig = reactive({
|
||||
@@ -286,6 +302,47 @@ const onSubmitDelete = async () => {
|
||||
});
|
||||
};
|
||||
|
||||
const onImport = () => {
|
||||
dialogImportRef.value.acceptParams();
|
||||
};
|
||||
|
||||
const onExport = async () => {
|
||||
let names = [];
|
||||
let ids = [];
|
||||
for (const item of selects.value) {
|
||||
names.push(item.name);
|
||||
ids.push(item.id);
|
||||
}
|
||||
operateIDs.value = ids;
|
||||
opExportRef.value.acceptParams({
|
||||
title: i18n.global.t('commons.button.export'),
|
||||
names: names,
|
||||
msg: i18n.global.t('commons.msg.operatorHelper', [
|
||||
i18n.global.t('menu.cronjob'),
|
||||
i18n.global.t('commons.button.export'),
|
||||
]),
|
||||
api: null,
|
||||
params: null,
|
||||
});
|
||||
};
|
||||
|
||||
const onSubmitExport = async () => {
|
||||
loading.value = true;
|
||||
await exportCronjob({ ids: operateIDs.value })
|
||||
.then((res) => {
|
||||
const downloadUrl = window.URL.createObjectURL(new Blob([res]));
|
||||
const a = document.createElement('a');
|
||||
a.style.display = 'none';
|
||||
a.href = downloadUrl;
|
||||
a.download = '1panel-cronjob-' + getCurrentDateFormatted() + '.json';
|
||||
const event = new MouseEvent('click');
|
||||
a.dispatchEvent(event);
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
const onChangeStatus = async (id: number, status: string) => {
|
||||
ElMessageBox.confirm(i18n.global.t('cronjob.' + status + 'Msg'), i18n.global.t('cronjob.changeStatus'), {
|
||||
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
||||
@@ -343,6 +400,9 @@ const buttons = [
|
||||
click: (row: Cronjob.CronjobInfo) => {
|
||||
onHandle(row);
|
||||
},
|
||||
disabled: (row: any) => {
|
||||
return row.status === 'Pending';
|
||||
},
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('commons.button.edit'),
|
||||
|
||||
@@ -834,7 +834,7 @@ const search = async () => {
|
||||
form.type = res.data.type;
|
||||
form.specCustom = res.data.specCustom;
|
||||
form.spec = res.data.spec;
|
||||
form.specs = res.data.specs;
|
||||
form.specs = res.data.specs || [];
|
||||
if (!form.specCustom && form.spec) {
|
||||
let objs = [];
|
||||
for (const item of res.data.spec.split(',')) {
|
||||
@@ -864,16 +864,16 @@ const search = async () => {
|
||||
form.executor !== 'python3';
|
||||
}
|
||||
|
||||
form.scriptID = res.data.scriptID;
|
||||
form.scriptID = res.data.scriptID || null;
|
||||
form.appID = res.data.appID;
|
||||
form.appIdList = res.data.appID.split(',') || [];
|
||||
form.appIdList = res.data.appID ? res.data.appID.split(',') : [];
|
||||
form.website = res.data.website;
|
||||
form.websiteList = res.data.website.split(',') || [];
|
||||
form.websiteList = res.data.website ? res.data.website.split(',') : [];
|
||||
form.exclusionRules = res.data.exclusionRules;
|
||||
form.ignoreFiles = res.data.exclusionRules.split(',');
|
||||
form.ignoreFiles = res.data.exclusionRules ? res.data.exclusionRules.split(',') : [];
|
||||
form.dbType = res.data.dbType;
|
||||
form.dbName = res.data.dbName;
|
||||
form.dbNameList = res.data.dbName.split(',') || [];
|
||||
form.dbNameList = res.data.dbName ? res.data.dbName.split(',') : [];
|
||||
form.url = res.data.url;
|
||||
form.withImage = res.data.snapshotRule.withImage;
|
||||
form.ignoreAppIDs = res.data.snapshotRule.ignoreAppIDs;
|
||||
|
||||
@@ -30,10 +30,15 @@
|
||||
<Status class="mt-0.5" :status="dialogData.rowData.status" />
|
||||
</div>
|
||||
<div class="mt-0.5">
|
||||
<el-button type="primary" @click="onHandle(dialogData.rowData)" link>
|
||||
<el-button
|
||||
type="primary"
|
||||
:disabled="dialogData.rowData.status === 'Pending'"
|
||||
@click="onHandle(dialogData.rowData)"
|
||||
link
|
||||
>
|
||||
{{ $t('commons.button.handle') }}
|
||||
</el-button>
|
||||
<el-divider direction="vertical" />
|
||||
<el-divider direction="vertical" v-if="dialogData.rowData.status !== 'Pending'" />
|
||||
<el-button
|
||||
type="primary"
|
||||
v-if="dialogData.rowData.status === 'Enable'"
|
||||
@@ -63,7 +68,7 @@
|
||||
<template #rightToolBar>
|
||||
<el-date-picker
|
||||
class="mr-2.5"
|
||||
@change="search()"
|
||||
@change="search(true)"
|
||||
v-model="timeRangeLoad"
|
||||
type="datetimerange"
|
||||
range-separator="-"
|
||||
@@ -71,7 +76,7 @@
|
||||
:end-placeholder="$t('commons.search.timeEnd')"
|
||||
:shortcuts="shortcuts"
|
||||
></el-date-picker>
|
||||
<el-select @change="search()" v-model="searchInfo.status" class="p-w-200">
|
||||
<el-select @change="search(true)" v-model="searchInfo.status" class="p-w-200">
|
||||
<template #prefix>{{ $t('commons.table.status') }}</template>
|
||||
<el-option :label="$t('commons.table.all')" value="" />
|
||||
<el-option :label="$t('commons.status.success')" value="Success" />
|
||||
@@ -119,7 +124,7 @@
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="17">
|
||||
<el-form label-position="top" :v-key="refresh">
|
||||
<el-form label-position="top">
|
||||
<el-row type="flex" justify="center">
|
||||
<el-form-item class="descriptionWide">
|
||||
<template #label>
|
||||
@@ -158,22 +163,12 @@
|
||||
{{ currentRecord?.message }}
|
||||
</el-form-item>
|
||||
</el-row>
|
||||
<el-row v-if="currentRecord?.records">
|
||||
<span>{{ $t('commons.table.records') }}</span>
|
||||
<div class="editor-main">
|
||||
<highlightjs
|
||||
ref="mymirror"
|
||||
language="JavaScript"
|
||||
:autodetect="false"
|
||||
:code="currentRecordDetail"
|
||||
></highlightjs>
|
||||
</div>
|
||||
</el-row>
|
||||
<el-row v-if="currentRecord?.taskID && currentRecord?.taskID != ''">
|
||||
<LogFile
|
||||
:defaultButton="false"
|
||||
:defaultButton="true"
|
||||
class="w-full"
|
||||
:key="currentRecord?.taskID"
|
||||
@stop-reading="search(false)"
|
||||
:heightDiff="410"
|
||||
:config="{ type: 'task', taskID: currentRecord?.taskID, tail: true }"
|
||||
/>
|
||||
@@ -218,9 +213,9 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { nextTick, onBeforeUnmount, reactive, ref } from 'vue';
|
||||
import { reactive, ref } from 'vue';
|
||||
import { Cronjob } from '@/api/interface/cronjob';
|
||||
import { searchRecords, handleOnce, updateStatus, cleanRecords, getRecordLog } from '@/api/modules/cronjob';
|
||||
import { searchRecords, handleOnce, updateStatus, cleanRecords } from '@/api/modules/cronjob';
|
||||
import { dateFormat } from '@/utils/util';
|
||||
import LogFile from '@/components/log/file/index.vue';
|
||||
import i18n from '@/lang';
|
||||
@@ -232,14 +227,8 @@ import { shortcuts } from '@/utils/shortcuts';
|
||||
import { hasBackup } from '../helper';
|
||||
|
||||
const loading = ref();
|
||||
const refresh = ref(false);
|
||||
const hasRecords = ref();
|
||||
|
||||
let timer: NodeJS.Timer | null = null;
|
||||
|
||||
const mymirror = ref();
|
||||
const scrollerElement = ref<HTMLElement | null>(null);
|
||||
|
||||
interface DialogProps {
|
||||
rowData: Cronjob.CronjobInfo;
|
||||
}
|
||||
@@ -247,7 +236,6 @@ const recordShow = ref(false);
|
||||
const dialogData = ref();
|
||||
const records = ref<Array<Cronjob.Record>>([]);
|
||||
const currentRecord = ref<Cronjob.Record>();
|
||||
const currentRecordDetail = ref<string>('');
|
||||
|
||||
const open = ref();
|
||||
const delLoading = ref();
|
||||
@@ -282,20 +270,17 @@ const acceptParams = async (params: DialogProps): Promise<void> => {
|
||||
}
|
||||
}
|
||||
}
|
||||
search();
|
||||
timer = setInterval(() => {
|
||||
search();
|
||||
}, 1000 * 5);
|
||||
search(true);
|
||||
};
|
||||
|
||||
const handleSizeChange = (val: number) => {
|
||||
searchInfo.pageSize = val;
|
||||
localStorage.setItem(searchInfo.cacheSizeKey, val + '');
|
||||
search();
|
||||
search(true);
|
||||
};
|
||||
const handleCurrentChange = (val: number) => {
|
||||
searchInfo.page = val;
|
||||
search();
|
||||
search(false);
|
||||
};
|
||||
|
||||
const timeRangeLoad = ref<[Date, Date]>([
|
||||
@@ -319,7 +304,7 @@ const onHandle = async (row: Cronjob.CronjobInfo) => {
|
||||
.then(() => {
|
||||
loading.value = false;
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
search();
|
||||
search(true);
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
@@ -338,7 +323,7 @@ const onChangeStatus = async (id: number, status: string) => {
|
||||
});
|
||||
};
|
||||
|
||||
const search = async () => {
|
||||
const search = async (changeToLatest: boolean) => {
|
||||
if (timeRangeLoad.value && timeRangeLoad.value.length === 2) {
|
||||
searchInfo.startTime = timeRangeLoad.value[0];
|
||||
searchInfo.endTime = timeRangeLoad.value[1];
|
||||
@@ -361,44 +346,20 @@ const search = async () => {
|
||||
if (!hasRecords.value) {
|
||||
return;
|
||||
}
|
||||
if (!currentRecord.value) {
|
||||
if (changeToLatest) {
|
||||
currentRecord.value = records.value[0];
|
||||
} else {
|
||||
let beDelete = true;
|
||||
for (const item of records.value) {
|
||||
if (item.id === currentRecord.value.id) {
|
||||
beDelete = false;
|
||||
currentRecord.value = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (beDelete) {
|
||||
currentRecord.value = records.value[0];
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (currentRecord.value?.records) {
|
||||
loadRecord(currentRecord.value);
|
||||
for (const item of records.value) {
|
||||
if (item.id === currentRecord.value.id) {
|
||||
currentRecord.value = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const forDetail = async (row: Cronjob.Record) => {
|
||||
currentRecord.value = row;
|
||||
loadRecord(row);
|
||||
};
|
||||
const loadRecord = async (row: Cronjob.Record) => {
|
||||
currentRecord.value = row;
|
||||
if (row.records) {
|
||||
const res = await getRecordLog(row.id);
|
||||
let log = res.data.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '');
|
||||
if (currentRecordDetail.value === log) {
|
||||
return;
|
||||
}
|
||||
currentRecordDetail.value = log;
|
||||
nextTick(() => {
|
||||
initLog();
|
||||
scrollerElement.value.scrollTop = scrollerElement.value.scrollHeight;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onClean = async () => {
|
||||
@@ -412,7 +373,7 @@ const onClean = async () => {
|
||||
.then(() => {
|
||||
delLoading.value = false;
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
search();
|
||||
search(true);
|
||||
})
|
||||
.catch(() => {
|
||||
delLoading.value = false;
|
||||
@@ -430,26 +391,13 @@ const cleanRecord = async () => {
|
||||
delLoading.value = false;
|
||||
open.value = false;
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
search();
|
||||
search(true);
|
||||
})
|
||||
.catch(() => {
|
||||
delLoading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
const initLog = () => {
|
||||
if (mymirror.value && scrollerElement.value == undefined) {
|
||||
scrollerElement.value = mymirror.value.$el as HTMLElement;
|
||||
let hljsDom = scrollerElement.value.querySelector('.hljs') as HTMLElement;
|
||||
hljsDom.style['min-height'] = '500px';
|
||||
}
|
||||
};
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearInterval(Number(timer));
|
||||
timer = null;
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
acceptParams,
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@ import i18n from '@/lang';
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
label: i18n.global.t('menu.settings', 2),
|
||||
label: i18n.global.t('menu.config', 2),
|
||||
path: '/hosts/ssh/ssh',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -200,6 +200,7 @@ const errMfaInfo = ref(false);
|
||||
const isDemo = ref(false);
|
||||
const isIntl = ref(true);
|
||||
const open = ref(false);
|
||||
const loginBtnLinkColor = ref<string | null>(null);
|
||||
|
||||
type FormInstance = InstanceType<typeof ElForm>;
|
||||
const _isMobile = () => {
|
||||
@@ -420,6 +421,47 @@ const getSetting = async () => {
|
||||
} catch (error) {}
|
||||
};
|
||||
|
||||
function adjustColorToRGBA(color: string, percent: number, opacity: number): string {
|
||||
let r = 0,
|
||||
g = 0,
|
||||
b = 0,
|
||||
a = opacity;
|
||||
|
||||
color = color.trim();
|
||||
|
||||
if (color.startsWith('#')) {
|
||||
if (color.length === 4) {
|
||||
r = parseInt(color[1] + color[1], 16);
|
||||
g = parseInt(color[2] + color[2], 16);
|
||||
b = parseInt(color[3] + color[3], 16);
|
||||
} else if (color.length === 7) {
|
||||
r = parseInt(color.slice(1, 3), 16);
|
||||
g = parseInt(color.slice(3, 5), 16);
|
||||
b = parseInt(color.slice(5, 7), 16);
|
||||
} else {
|
||||
return color;
|
||||
}
|
||||
} else if (color.startsWith('rgb')) {
|
||||
const result = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([0-9.]+))?\)/);
|
||||
if (!result) return color;
|
||||
r = parseInt(result[1], 10);
|
||||
g = parseInt(result[2], 10);
|
||||
b = parseInt(result[3], 10);
|
||||
if (result[4] !== undefined) {
|
||||
a = parseFloat(result[4]);
|
||||
}
|
||||
} else {
|
||||
return color;
|
||||
}
|
||||
|
||||
r = Math.min(255, Math.max(0, Math.round(r * (1 + percent / 100))));
|
||||
g = Math.min(255, Math.max(0, Math.round(g * (1 + percent / 100))));
|
||||
b = Math.min(255, Math.max(0, Math.round(b * (1 + percent / 100))));
|
||||
a = Math.min(1, Math.max(0, opacity / 100));
|
||||
|
||||
return `rgba(${r}, ${g}, ${b}, ${a})`;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
globalStore.isOnRestart = false;
|
||||
getSetting();
|
||||
@@ -428,6 +470,17 @@ onMounted(() => {
|
||||
loginVerify();
|
||||
}
|
||||
document.title = globalStore.themeConfig.panelName;
|
||||
loginBtnLinkColor.value = globalStore.themeConfig.loginBtnLinkColor || '#005eeb';
|
||||
document.documentElement.style.setProperty('--login-btn-link-color', loginBtnLinkColor.value);
|
||||
document.documentElement.style.setProperty(
|
||||
'--login-btn-link-hover-color',
|
||||
adjustColorToRGBA(loginBtnLinkColor.value, -10, 80),
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
'--login-loading-mask-color',
|
||||
adjustColorToRGBA(loginBtnLinkColor.value, 30, 15),
|
||||
);
|
||||
|
||||
loginForm.agreeLicense = globalStore.agreeLicense;
|
||||
document.onkeydown = (e: any) => {
|
||||
e = window.event || e;
|
||||
@@ -467,12 +520,12 @@ onMounted(() => {
|
||||
|
||||
.login-form {
|
||||
.login-button {
|
||||
background-color: #005eeb;
|
||||
border-color: #005eeb;
|
||||
background-color: var(--login-btn-link-color);
|
||||
border-color: var(--login-btn-link-color);
|
||||
color: #ffffff;
|
||||
&:hover {
|
||||
background-color: #196eed !important;
|
||||
border-color: #196eed !important;
|
||||
background-color: var(--login-btn-link-hover-color) !important;
|
||||
border-color: var(--login-btn-link-hover-color) !important;
|
||||
outline: none !important;
|
||||
}
|
||||
}
|
||||
@@ -486,7 +539,7 @@ onMounted(() => {
|
||||
background: none !important;
|
||||
}
|
||||
:deep(.el-input__wrapper.is-focus) {
|
||||
box-shadow: 0 0 0 1px #005eeb inset !important;
|
||||
box-shadow: 0 0 0 1px var(--login-btn-link-color) inset !important;
|
||||
}
|
||||
|
||||
.demo {
|
||||
@@ -497,16 +550,16 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.agree-title {
|
||||
color: #005eeb;
|
||||
color: var(--login-btn-link-color);
|
||||
}
|
||||
|
||||
.agree {
|
||||
white-space: pre-wrap;
|
||||
color: #005eeb;
|
||||
color: var(--login-btn-link-color);
|
||||
}
|
||||
|
||||
:deep(a) {
|
||||
color: #005eeb;
|
||||
color: var(--login-btn-link-color);
|
||||
&:hover {
|
||||
opacity: 75%;
|
||||
}
|
||||
@@ -514,16 +567,16 @@ onMounted(() => {
|
||||
|
||||
:deep(.el-checkbox__input .el-checkbox__inner) {
|
||||
background-color: #fff !important;
|
||||
border-color: #e5eefd !important;
|
||||
border-color: var(--login-btn-link-color) !important;
|
||||
}
|
||||
|
||||
:deep(.el-checkbox__input.is-checked .el-checkbox__inner) {
|
||||
background-color: #005eeb !important;
|
||||
border-color: #005eeb !important;
|
||||
background-color: var(--login-btn-link-color) !important;
|
||||
border-color: var(--login-btn-link-color) !important;
|
||||
}
|
||||
|
||||
:deep(.el-checkbox__input.is-checked .el-checkbox__inner::after) {
|
||||
border-color: #e5eefd !important;
|
||||
border-color: #ffffff !important;
|
||||
}
|
||||
|
||||
:deep(.el-input__inner) {
|
||||
@@ -543,22 +596,25 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
:deep(.el-dropdown-menu__item:not(.is-disabled):hover) {
|
||||
color: #005eeb !important;
|
||||
background-color: #e5eefd !important;
|
||||
background-color: var(--login-btn-link-color) !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
:deep(.el-dropdown-menu__item:not(.is-disabled):focus) {
|
||||
color: #005eeb !important;
|
||||
background-color: #e5eefd !important;
|
||||
background-color: var(--login-btn-link-color) !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
:deep(.el-loading-mask) {
|
||||
background-color: rgba(229, 238, 253, 0.8) !important;
|
||||
background-color: var(--login-loading-mask-color) !important;
|
||||
.el-loading-spinner .path {
|
||||
stroke: var(--login-btn-link-color);
|
||||
}
|
||||
}
|
||||
|
||||
.login-footer-btn {
|
||||
.el-button--primary {
|
||||
border-color: #005eeb !important;
|
||||
background-color: #005eeb !important;
|
||||
border-color: var(--login-btn-link-color) !important;
|
||||
background-color: var(--login-btn-link-color) !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -200,6 +200,7 @@
|
||||
:rules="[Rules.requiredSelect]"
|
||||
>
|
||||
<el-select v-model="dialogData.rowData!.varsJson['scType']">
|
||||
<el-option value="DEFAULT" :label="$t('setting.scLighthouse')" />
|
||||
<el-option value="Standard" :label="$t('setting.scStandard')" />
|
||||
<el-option value="Standard_IA" :label="$t('setting.scStandard_IA')" />
|
||||
<el-option value="Archive" :label="$t('setting.scArchive')" />
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { checkAppInstalled } from '@/api/modules/app';
|
||||
import { Rules, checkNumberRange } from '@/global/form-rules';
|
||||
import i18n from '@/lang';
|
||||
import { MsgError } from '@/utils/message';
|
||||
@@ -89,9 +90,10 @@ const rules = ref({
|
||||
type: Array,
|
||||
},
|
||||
});
|
||||
const defaultPort = ref(80);
|
||||
const initDomain = () => ({
|
||||
domain: '',
|
||||
port: 80,
|
||||
port: defaultPort.value,
|
||||
ssl: false,
|
||||
});
|
||||
const create = ref({
|
||||
@@ -128,16 +130,16 @@ const gengerateDomains = () => {
|
||||
const exists = (domain: string, port: number): boolean => {
|
||||
return create.value.domains.some((info) => info.domain === domain && info.port === port);
|
||||
};
|
||||
if (exists(domain, port ? Number(port) : 80)) {
|
||||
if (exists(domain, port ? Number(port) : defaultPort.value)) {
|
||||
return;
|
||||
}
|
||||
if (create.value.domains[0].domain == '') {
|
||||
create.value.domains[0].domain = domain;
|
||||
create.value.domains[0].port = port ? Number(port) : 80;
|
||||
create.value.domains[0].port = port ? Number(port) : defaultPort.value;
|
||||
} else {
|
||||
create.value.domains.push({
|
||||
domain,
|
||||
port: port ? Number(port) : 80,
|
||||
port: port ? Number(port) : defaultPort.value,
|
||||
ssl: false,
|
||||
});
|
||||
}
|
||||
@@ -149,7 +151,19 @@ const handleParams = () => {
|
||||
props.form.domains = create.value.domains;
|
||||
};
|
||||
|
||||
const getOprensty = async () => {
|
||||
try {
|
||||
await checkAppInstalled('openresty', '')
|
||||
.then((res) => {
|
||||
defaultPort.value = res.data.httpPort || 80;
|
||||
create.value.domains[0].port = defaultPort.value;
|
||||
})
|
||||
.catch(() => {});
|
||||
} catch (error) {}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getOprensty();
|
||||
handleParams();
|
||||
});
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user