diff --git a/backend/app.yaml b/backend/app.yaml index 20c856a77..26fb94315 100644 --- a/backend/app.yaml +++ b/backend/app.yaml @@ -1,21 +1,11 @@ -base_dir: /opt - system: port: 9999 - db_type: sqlite - data_dir: ${base_dir}/1Panel/data - cache: ${base_dir}/1Panel/data/cache - backup: ${base_dir}/1Panel/data/backup - app_oss: "https://1panel.oss-cn-hangzhou.aliyuncs.com/apps/list.json" - -sqlite: - path: ${base_dir}/1Panel/data/db db_file: 1Panel.db + app_oss: "https://1panel.oss-cn-hangzhou.aliyuncs.com/apps/list.json" log: level: debug time_zone: Asia/Shanghai - path: ${base_dir}/1Panel/log log_name: 1Panel log_suffix: .log log_backup: 10 diff --git a/backend/app/api/v1/backup.go b/backend/app/api/v1/backup.go index 6bf428fec..818e50fc2 100644 --- a/backend/app/api/v1/backup.go +++ b/backend/app/api/v1/backup.go @@ -188,12 +188,7 @@ func (b *BaseApi) UpdateBackup(c *gin.Context) { helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) return } - - upMap := make(map[string]interface{}) - upMap["bucket"] = req.Bucket - upMap["credential"] = req.Credential - upMap["vars"] = req.Vars - if err := backupService.Update(req.ID, upMap); err != nil { + if err := backupService.Update(req); err != nil { helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) return } diff --git a/backend/app/api/v1/cronjob.go b/backend/app/api/v1/cronjob.go index 5bb33cc7e..16896b1b6 100644 --- a/backend/app/api/v1/cronjob.go +++ b/backend/app/api/v1/cronjob.go @@ -77,10 +77,8 @@ func (b *BaseApi) SearchJobRecords(c *gin.Context) { helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) return } - if global.CONF.System.DbType == "sqlite" { - req.StartTime = req.StartTime.Add(8 * time.Hour) - req.EndTime = req.EndTime.Add(8 * time.Hour) - } + req.StartTime = req.StartTime.Add(8 * time.Hour) + req.EndTime = req.EndTime.Add(8 * time.Hour) total, list, err := cronjobService.SearchRecords(req) if err != nil { diff --git a/backend/app/api/v1/monitor.go b/backend/app/api/v1/monitor.go index cb3ad7263..37759da1c 100644 --- a/backend/app/api/v1/monitor.go +++ b/backend/app/api/v1/monitor.go @@ -23,10 +23,8 @@ func (b *BaseApi) LoadMonitor(c *gin.Context) { helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) return } - if global.CONF.System.DbType == "sqlite" { - req.StartTime = req.StartTime.Add(8 * time.Hour) - req.EndTime = req.EndTime.Add(8 * time.Hour) - } + req.StartTime = req.StartTime.Add(8 * time.Hour) + req.EndTime = req.EndTime.Add(8 * time.Hour) var backdatas []dto.MonitorData if req.Param == "all" || req.Param == "cpu" || req.Param == "memory" || req.Param == "load" { diff --git a/backend/app/api/v1/setting.go b/backend/app/api/v1/setting.go index 5f1f7f5d2..7bc5ff8c9 100644 --- a/backend/app/api/v1/setting.go +++ b/backend/app/api/v1/setting.go @@ -82,6 +82,33 @@ func (b *BaseApi) UpdatePassword(c *gin.Context) { helper.SuccessWithData(c, nil) } +// @Tags System Setting +// @Summary Update system port +// @Description 更新系统端口 +// @Accept json +// @Param request body dto.PortUpdate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /settings/port/update [post] +// @x-panel-log {"bodyKeys":["serverPort"],"paramKeys":[],"BeforeFuntions":[],"formatZH":"修改系统端口 => [serverPort]","formatEN":"update system port => [serverPort]"} +func (b *BaseApi) UpdatePort(c *gin.Context) { + var req dto.PortUpdate + if err := c.ShouldBindJSON(&req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + if err := global.VALID.Struct(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + + if err := settingService.UpdatePort(req.ServerPort); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + // @Tags System Setting // @Summary Reset system password expired // @Description 重置过期系统登录密码 @@ -132,6 +159,16 @@ func (b *BaseApi) SyncTime(c *gin.Context) { helper.SuccessWithData(c, ntime.Format("2006-01-02 15:04:05 MST -0700")) } +// @Tags System Setting +// @Summary Load local backup dir +// @Description 获取安装根目录 +// @Success 200 {string} path +// @Security ApiKeyAuth +// @Router /settings/basedir [get] +func (b *BaseApi) LoadBaseDir(c *gin.Context) { + helper.SuccessWithData(c, global.CONF.System.DataDir) +} + // @Tags System Setting // @Summary Clean monitor datas // @Description 清空监控数据 diff --git a/backend/app/dto/setting.go b/backend/app/dto/setting.go index 47ad3da92..3dc6f0862 100644 --- a/backend/app/dto/setting.go +++ b/backend/app/dto/setting.go @@ -10,6 +10,7 @@ type SettingInfo struct { SessionTimeout string `json:"sessionTimeout"` LocalTime string `json:"localTime"` + Port string `json:"port"` PanelName string `json:"panelName"` Theme string `json:"theme"` Language string `json:"language"` @@ -41,6 +42,10 @@ type PasswordUpdate struct { NewPassword string `json:"newPassword" validate:"required"` } +type PortUpdate struct { + ServerPort uint `json:"serverPort" validate:"required,number,max=65535,min=1"` +} + type SnapshotCreate struct { From string `json:"from" validate:"required,oneof=OSS S3 SFTP MINIO"` Description string `json:"description"` diff --git a/backend/app/service/backup.go b/backend/app/service/backup.go index cbb0ae719..1c4bc198e 100644 --- a/backend/app/service/backup.go +++ b/backend/app/service/backup.go @@ -5,12 +5,14 @@ import ( "encoding/json" "fmt" "os" + "path" "github.com/1Panel-dev/1Panel/backend/app/dto" "github.com/1Panel-dev/1Panel/backend/app/model" "github.com/1Panel-dev/1Panel/backend/constant" "github.com/1Panel-dev/1Panel/backend/global" "github.com/1Panel-dev/1Panel/backend/utils/cloud_storage" + "github.com/1Panel-dev/1Panel/backend/utils/files" "github.com/jinzhu/copier" "github.com/pkg/errors" ) @@ -23,7 +25,7 @@ type IBackupService interface { DownloadRecord(info dto.DownloadRecord) (string, error) Create(backupDto dto.BackupOperate) error GetBuckets(backupDto dto.ForBuckets) ([]interface{}, error) - Update(id uint, upMap map[string]interface{}) error + Update(ireq dto.BackupOperate) error BatchDelete(ids []uint) error BatchDeleteRecord(ids []uint) error NewClient(backup *model.BackupAccount) (cloud_storage.CloudStorageClient, error) @@ -114,7 +116,7 @@ func (u *BackupService) DownloadRecord(info dto.DownloadRecord) (string, error) if err != nil { return "", fmt.Errorf("new cloud storage client failed, err: %v", err) } - tempPath := fmt.Sprintf("%s%s", constant.DownloadDir, info.FileDir) + tempPath := fmt.Sprintf("%sdownload%s", constant.DataDir, info.FileDir) if _, err := os.Stat(tempPath); err != nil && os.IsNotExist(err) { if err = os.MkdirAll(tempPath, os.ModePerm); err != nil { fmt.Println(err) @@ -141,10 +143,6 @@ func (u *BackupService) Create(backupDto dto.BackupOperate) error { if err := backupRepo.Create(&backup); err != nil { return err } - var backupinfo dto.BackupInfo - if err := copier.Copy(&backupinfo, &backup); err != nil { - return errors.WithMessage(constant.ErrStructTransform, err.Error()) - } return nil } @@ -200,8 +198,32 @@ func (u *BackupService) BatchDeleteRecord(ids []uint) error { return backupRepo.DeleteRecord(context.Background(), commonRepo.WithIdsIn(ids)) } -func (u *BackupService) Update(id uint, upMap map[string]interface{}) error { - return backupRepo.Update(id, upMap) +func (u *BackupService) Update(req dto.BackupOperate) error { + backup, err := backupRepo.Get(commonRepo.WithByID(req.ID)) + if err != nil { + return constant.ErrRecordNotFound + } + varMap := make(map[string]string) + if err := json.Unmarshal([]byte(req.Vars), &varMap); err != nil { + return err + } + upMap := make(map[string]interface{}) + upMap["bucket"] = req.Bucket + upMap["credential"] = req.Credential + upMap["vars"] = req.Vars + if err := backupRepo.Update(req.ID, upMap); err != nil { + return err + } + if backup.Type == "LOCAL" { + if dir, ok := varMap["dir"]; ok { + if err := updateBackupDir(dir); err != nil { + upMap["vars"] = backup.Vars + _ = backupRepo.Update(req.ID, upMap) + return err + } + } + } + return nil } func (u *BackupService) NewClient(backup *model.BackupAccount) (cloud_storage.CloudStorageClient, error) { @@ -256,3 +278,23 @@ func loadLocalDir() (string, error) { } return "", fmt.Errorf("error type dir: %T", varMap["dir"]) } + +func updateBackupDir(dir string) error { + oldDir := global.CONF.System.Backup + fileOp := files.NewFileOp() + if _, err := os.Stat(dir); err != nil && os.IsNotExist(err) { + if err = os.MkdirAll(dir, os.ModePerm); err != nil { + return err + } + } + global.Viper.Set("system.backup", path.Join(dir, "backup")) + if err := global.Viper.WriteConfig(); err != nil { + return err + } + if err := fileOp.CopyDir(oldDir, dir); err != nil { + global.Viper.Set("system.backup", oldDir) + _ = global.Viper.WriteConfig() + return err + } + return nil +} diff --git a/backend/app/service/container_compose.go b/backend/app/service/container_compose.go index 30535488e..0bc3f3973 100644 --- a/backend/app/service/container_compose.go +++ b/backend/app/service/container_compose.go @@ -122,7 +122,7 @@ func (u *ContainerService) CreateCompose(req dto.ComposeCreate) error { req.File = template.Content } if req.From == "edit" { - dir := fmt.Sprintf("%s/%s", constant.TmpComposeBuildDir, req.Name) + dir := fmt.Sprintf("%s/docker/compose/%s", constant.DataDir, req.Name) if _, err := os.Stat(dir); err != nil && os.IsNotExist(err) { if err = os.MkdirAll(dir, os.ModePerm); err != nil { return err diff --git a/backend/app/service/cornjob.go b/backend/app/service/cornjob.go index 694fa6a5d..fc9d2d7ee 100644 --- a/backend/app/service/cornjob.go +++ b/backend/app/service/cornjob.go @@ -119,7 +119,7 @@ func (u *CronjobService) Download(down dto.CronjobDownload) (string, error) { if cronjob.Type == "database" { name = fmt.Sprintf("%s%s.gz", commonDir, record.StartTime.Format("20060102150405")) } - tempPath := fmt.Sprintf("%s/%s", constant.DownloadDir, commonDir) + tempPath := fmt.Sprintf("%s/download/%s", constant.DataDir, commonDir) if _, err := os.Stat(tempPath); err != nil && os.IsNotExist(err) { if err = os.MkdirAll(tempPath, os.ModePerm); err != nil { fmt.Println(err) @@ -274,7 +274,7 @@ func (u *CronjobService) AddCronJob(cronjob *model.Cronjob) (int, error) { } func mkdirAndWriteFile(cronjob *model.Cronjob, startTime time.Time, msg []byte) (string, error) { - dir := fmt.Sprintf("%s/%s/%s", constant.TaskDir, cronjob.Type, cronjob.Name) + dir := fmt.Sprintf("%s/task/%s/%s", constant.DataDir, cronjob.Type, cronjob.Name) if _, err := os.Stat(dir); err != nil && os.IsNotExist(err) { if err = os.MkdirAll(dir, os.ModePerm); err != nil { return "", err diff --git a/backend/app/service/cronjob_helper.go b/backend/app/service/cronjob_helper.go index 2a1954d73..6170b2ecd 100644 --- a/backend/app/service/cronjob_helper.go +++ b/backend/app/service/cronjob_helper.go @@ -151,10 +151,10 @@ func (u *CronjobService) HandleDelete(id uint) error { global.LOG.Infof("stop cronjob entryID: %d", cronjob.EntryID) _ = cronjobRepo.DeleteRecord(cronjobRepo.WithByJobID(int(id))) - dir := fmt.Sprintf("%s/%s/%s", constant.TaskDir, cronjob.Type, cronjob.Name) + dir := fmt.Sprintf("%s/task/%s/%s", constant.DataDir, cronjob.Type, cronjob.Name) if _, err := os.Stat(dir); err == nil { if err := os.RemoveAll(dir); err != nil { - global.LOG.Errorf("rm file %s/%s failed, err: %v", constant.TaskDir, commonDir, err) + global.LOG.Errorf("rm file %s/task/%s failed, err: %v", constant.DataDir, commonDir, err) } } return nil diff --git a/backend/app/service/database_mysql.go b/backend/app/service/database_mysql.go index 75b6d53d9..513ec5cb7 100644 --- a/backend/app/service/database_mysql.go +++ b/backend/app/service/database_mysql.go @@ -286,7 +286,7 @@ func (u *MysqlService) Delete(ctx context.Context, req dto.MysqlDBDelete) error } global.LOG.Info("execute delete database sql successful, now start to drop uploads and records") - uploadDir := fmt.Sprintf("%s/uploads/database/mysql/%s/%s", constant.DefaultDataDir, app.Name, db.Name) + uploadDir := fmt.Sprintf("%s/uploads/database/mysql/%s/%s", constant.DataDir, app.Name, db.Name) if _, err := os.Stat(uploadDir); err == nil { _ = os.RemoveAll(uploadDir) } diff --git a/backend/app/service/image.go b/backend/app/service/image.go index 750f0cbe5..0dfc087a7 100644 --- a/backend/app/service/image.go +++ b/backend/app/service/image.go @@ -20,7 +20,7 @@ import ( "github.com/docker/docker/pkg/archive" ) -const dockerLogDir = constant.TmpDir + "/docker_logs" +var dockerLogDir = constant.TmpDir + "/docker_logs" type ImageService struct{} @@ -104,7 +104,7 @@ func (u *ImageService) ImageBuild(req dto.ImageBuild) (string, error) { return "", err } if req.From == "edit" { - dir := fmt.Sprintf("%s/%s", constant.TmpDockerBuildDir, req.Name) + dir := fmt.Sprintf("%s/docker/build/%s", constant.DataDir, req.Name) if _, err := os.Stat(dir); err != nil && os.IsNotExist(err) { if err = os.MkdirAll(dir, os.ModePerm); err != nil { return "", err diff --git a/backend/app/service/setting.go b/backend/app/service/setting.go index 6033087d3..ee0a42a3c 100644 --- a/backend/app/service/setting.go +++ b/backend/app/service/setting.go @@ -2,12 +2,14 @@ package service import ( "encoding/json" + "errors" "strconv" "time" "github.com/1Panel-dev/1Panel/backend/app/dto" "github.com/1Panel-dev/1Panel/backend/constant" "github.com/1Panel-dev/1Panel/backend/global" + "github.com/1Panel-dev/1Panel/backend/utils/cmd" "github.com/1Panel-dev/1Panel/backend/utils/encrypt" "github.com/gin-gonic/gin" ) @@ -18,6 +20,7 @@ type ISettingService interface { GetSettingInfo() (*dto.SettingInfo, error) Update(c *gin.Context, key, value string) error UpdatePassword(c *gin.Context, old, new string) error + UpdatePort(port uint) error HandlePasswordExpired(c *gin.Context, old, new string) error } @@ -59,6 +62,19 @@ func (u *SettingService) Update(c *gin.Context, key, value string) error { return nil } +func (u *SettingService) UpdatePort(port uint) error { + global.Viper.Set("system.port", port) + if err := global.Viper.WriteConfig(); err != nil { + return err + } + _ = settingRepo.Update("ServerPort", strconv.Itoa(int(port))) + stdout, err := cmd.Exec("systemctl restart 1panel.service") + if err != nil { + return errors.New(stdout) + } + return nil +} + func (u *SettingService) HandlePasswordExpired(c *gin.Context, old, new string) error { setting, err := settingRepo.Get(settingRepo.WithByKey("Password")) if err != nil { diff --git a/backend/app/service/snapshot.go b/backend/app/service/snapshot.go index ecd6bfbb5..d9005d773 100644 --- a/backend/app/service/snapshot.go +++ b/backend/app/service/snapshot.go @@ -779,7 +779,7 @@ func (u *SnapshotService) updateLiveRestore(enabled bool) error { if err != nil { return errors.New(stdout) } - time.Sleep(10 * time.Second) + time.Sleep(5 * time.Second) return nil } diff --git a/backend/app/service/snapshot_test.go b/backend/app/service/snapshot_test.go index 0d8a61d24..0fd8365fc 100644 --- a/backend/app/service/snapshot_test.go +++ b/backend/app/service/snapshot_test.go @@ -7,26 +7,9 @@ import ( "strings" "testing" - "github.com/1Panel-dev/1Panel/backend/init/db" - "github.com/1Panel-dev/1Panel/backend/init/viper" "github.com/google/go-github/github" ) -func TestDw(t *testing.T) { - viper.Init() - db.Init() - - backup, err := backupRepo.Get(commonRepo.WithByType("OSS")) - if err != nil { - fmt.Println(err) - } - client, err := NewIBackupService().NewClient(&backup) - if err != nil { - fmt.Println(err) - } - fmt.Println(client.Download("system_snapshot/1panel_snapshot_20230112135640.tar.gz", "/opt/1Panel/data/backup/system/test.tar.gz")) -} - func TestDi(t *testing.T) { docker := "var/lib/docker" fmt.Println(docker[strings.LastIndex(docker, "/"):]) diff --git a/backend/configs/config.go b/backend/configs/config.go index e76d38297..35910ef40 100644 --- a/backend/configs/config.go +++ b/backend/configs/config.go @@ -3,7 +3,6 @@ package configs type ServerConfig struct { BaseDir string `mapstructure:"base_dir"` System System `mapstructure:"system"` - Sqlite Sqlite `mapstructure:"sqlite"` LogConfig LogConfig `mapstructure:"log"` CORS CORS `mapstructure:"cors"` Encrypt Encrypt `mapstructure:"encrypt"` diff --git a/backend/configs/log.go b/backend/configs/log.go index 37040ed07..cbbbc47c0 100644 --- a/backend/configs/log.go +++ b/backend/configs/log.go @@ -3,7 +3,6 @@ package configs type LogConfig struct { Level string `mapstructure:"level"` TimeZone string `mapstructure:"timeZone"` - Path string `mapstructure:"path"` LogName string `mapstructure:"log_name"` LogSuffix string `mapstructure:"log_suffix"` LogBackup int `mapstructure:"log_backup"` diff --git a/backend/configs/sqlite.go b/backend/configs/sqlite.go deleted file mode 100644 index bb74000df..000000000 --- a/backend/configs/sqlite.go +++ /dev/null @@ -1,25 +0,0 @@ -package configs - -import ( - "fmt" - "os" -) - -type Sqlite struct { - Path string `mapstructure:"path"` - DbFile string `mapstructure:"db_file"` -} - -func (s *Sqlite) Dsn() string { - if _, err := os.Stat(s.Path); err != nil { - if err := os.MkdirAll(s.Path, os.ModePerm); err != nil { - panic(fmt.Errorf("init db dir falied, err: %v", err)) - } - } - if _, err := os.Stat(s.Path + "/" + s.DbFile); err != nil { - if _, err := os.Create(s.Path + "/" + s.DbFile); err != nil { - panic(fmt.Errorf("init db file falied, err: %v", err)) - } - } - return s.Path + "/" + s.DbFile -} diff --git a/backend/configs/system.go b/backend/configs/system.go index 2a4b655d5..406b41a19 100644 --- a/backend/configs/system.go +++ b/backend/configs/system.go @@ -2,7 +2,9 @@ package configs type System struct { Port int `mapstructure:"port"` - DbType string `mapstructure:"db_type"` + DbFile string `mapstructure:"db_file"` + DbPath string `mapstructure:"db_path"` + LogPath string `mapstructure:"log_path"` DataDir string `mapstructure:"data_dir"` Cache string `mapstructure:"cache"` Backup string `mapstructure:"backup"` diff --git a/backend/constant/backup.go b/backend/constant/backup.go index 566b8f2fd..d2a688620 100644 --- a/backend/constant/backup.go +++ b/backend/constant/backup.go @@ -8,7 +8,4 @@ const ( OSS = "OSS" Sftp = "SFTP" MinIo = "MINIO" - - DatabaseBackupDir = "/opt/1Panel/data/backup/database" - WebsiteBackupDir = "/opt/1Panel/data/backup/website" ) diff --git a/backend/constant/container.go b/backend/constant/container.go index a55074f02..98853664c 100644 --- a/backend/constant/container.go +++ b/backend/constant/container.go @@ -14,7 +14,5 @@ const ( ComposeOpRestart = "restart" ComposeOpRemove = "remove" - TmpDockerBuildDir = "/opt/1Panel/data/docker/build" - TmpComposeBuildDir = "/opt/1Panel/data/docker/compose" - DaemonJsonPath = "/etc/docker/daemon.json" + DaemonJsonPath = "/etc/docker/daemon.json" ) diff --git a/backend/constant/dir.go b/backend/constant/dir.go index 4adcf7c7e..dda5939d6 100644 --- a/backend/constant/dir.go +++ b/backend/constant/dir.go @@ -2,18 +2,14 @@ package constant import ( "path" + + "github.com/1Panel-dev/1Panel/backend/global" ) var ( - DefaultDataDir = "/opt/1Panel/data" - ResourceDir = path.Join(DefaultDataDir, "resource") + DataDir = global.CONF.System.DataDir + ResourceDir = path.Join(DataDir, "resource") AppResourceDir = path.Join(ResourceDir, "apps") - AppInstallDir = path.Join(DefaultDataDir, "apps") -) - -const ( - TmpDir = "/opt/1Panel/data/tmp" - TaskDir = "/opt/1Panel/data/task" - DownloadDir = "/opt/1Panel/data/download" - UploadDir = "/opt/1Panel/data/uploads" + AppInstallDir = path.Join(DataDir, "apps") + TmpDir = path.Join(DataDir, "tmp") ) diff --git a/backend/global/global.go b/backend/global/global.go index 8dab4c697..6091693c5 100644 --- a/backend/global/global.go +++ b/backend/global/global.go @@ -7,6 +7,7 @@ import ( "github.com/go-playground/validator/v10" "github.com/robfig/cron/v3" "github.com/sirupsen/logrus" + "github.com/spf13/viper" "gorm.io/gorm" ) @@ -17,6 +18,7 @@ var ( VALID *validator.Validate SESSION *psession.PSession CACHE *badger_db.Cache + Viper *viper.Viper Cron *cron.Cron ) diff --git a/backend/init/app/app.go b/backend/init/app/app.go index b44a5e492..7e5a8f99d 100644 --- a/backend/init/app/app.go +++ b/backend/init/app/app.go @@ -2,19 +2,22 @@ package app import ( "fmt" + "path" + "github.com/1Panel-dev/1Panel/backend/constant" + "github.com/1Panel-dev/1Panel/backend/global" "github.com/1Panel-dev/1Panel/backend/utils/docker" "github.com/1Panel-dev/1Panel/backend/utils/files" - "path" ) func Init() { - constant.DefaultDataDir = "/opt/1Panel/data" - constant.ResourceDir = path.Join(constant.DefaultDataDir, "resource") + constant.DataDir = global.CONF.System.DataDir + constant.ResourceDir = path.Join(constant.DataDir, "resource") constant.AppResourceDir = path.Join(constant.ResourceDir, "apps") - constant.AppInstallDir = path.Join(constant.DefaultDataDir, "apps") + constant.AppInstallDir = path.Join(constant.DataDir, "apps") + constant.TmpDir = path.Join(constant.DataDir, "tmp") - dirs := []string{constant.DefaultDataDir, constant.ResourceDir, constant.AppResourceDir, constant.AppInstallDir} + dirs := []string{constant.DataDir, constant.ResourceDir, constant.AppResourceDir, constant.AppInstallDir} fileOp := files.NewFileOp() for _, dir := range dirs { diff --git a/backend/init/db/db.go b/backend/init/db/db.go index 36c2792b5..d5ced3c33 100644 --- a/backend/init/db/db.go +++ b/backend/init/db/db.go @@ -1,14 +1,27 @@ package db import ( + "fmt" + "os" + "github.com/1Panel-dev/1Panel/backend/global" "gorm.io/driver/sqlite" "gorm.io/gorm" ) func Init() { - s := global.CONF.Sqlite - db, err := gorm.Open(sqlite.Open(s.Dsn()), &gorm.Config{}) + if _, err := os.Stat(global.CONF.System.DbPath); err != nil { + if err := os.MkdirAll(global.CONF.System.DbPath, os.ModePerm); err != nil { + panic(fmt.Errorf("init db dir falied, err: %v", err)) + } + } + fullPath := global.CONF.System.DbPath + "/" + global.CONF.System.DbFile + if _, err := os.Stat(fullPath); err != nil { + if _, err := os.Create(fullPath); err != nil { + panic(fmt.Errorf("init db file falied, err: %v", err)) + } + } + db, err := gorm.Open(sqlite.Open(fullPath), &gorm.Config{}) if err != nil { panic(err) } diff --git a/backend/init/log/log.go b/backend/init/log/log.go index 08c0a5588..496925361 100644 --- a/backend/init/log/log.go +++ b/backend/init/log/log.go @@ -2,12 +2,13 @@ package log import ( "fmt" - "github.com/1Panel-dev/1Panel/backend/log" "io" "os" "strings" "time" + "github.com/1Panel-dev/1Panel/backend/log" + "github.com/1Panel-dev/1Panel/backend/configs" "github.com/1Panel-dev/1Panel/backend/global" @@ -29,7 +30,7 @@ func Init() { func setOutput(logger *logrus.Logger, config configs.LogConfig) { writer, err := log.NewWriterFromConfig(&log.Config{ - LogPath: config.Path, + LogPath: global.CONF.System.LogPath, FileName: config.LogName, TimeTagFormat: FileTImeFormat, MaxRemain: config.LogBackup, diff --git a/backend/init/migration/migrations/init.go b/backend/init/migration/migrations/init.go index 412c575a5..7ee701cbc 100644 --- a/backend/init/migration/migrations/init.go +++ b/backend/init/migration/migrations/init.go @@ -1,10 +1,12 @@ package migrations import ( + "fmt" "time" "github.com/1Panel-dev/1Panel/backend/app/model" "github.com/1Panel-dev/1Panel/backend/constant" + "github.com/1Panel-dev/1Panel/backend/global" "github.com/go-gormigrate/gormigrate/v2" "gorm.io/gorm" @@ -85,7 +87,7 @@ var AddTableSetting = &gormigrate.Migration{ return err } - if err := tx.Create(&model.Setting{Key: "ServerPort", Value: "4004"}).Error; err != nil { + if err := tx.Create(&model.Setting{Key: "ServerPort", Value: "9999"}).Error; err != nil { return err } if err := tx.Create(&model.Setting{Key: "SecurityEntrance", Value: "onepanel"}).Error; err != nil { @@ -139,9 +141,10 @@ var AddTableBackupAccount = &gormigrate.Migration{ if err := tx.AutoMigrate(&model.BackupAccount{}, &model.BackupRecord{}); err != nil { return err } + item := &model.BackupAccount{ Type: "LOCAL", - Vars: "{\"dir\":\"/opt/1Panel/data/backup\"}", + Vars: fmt.Sprintf("{\"dir\":\"%s\"}", global.CONF.System.Backup), } if err := tx.Create(item).Error; err != nil { return err diff --git a/backend/init/viper/viper.go b/backend/init/viper/viper.go index 6c9a05c6b..47518ba2d 100644 --- a/backend/init/viper/viper.go +++ b/backend/init/viper/viper.go @@ -2,7 +2,7 @@ package viper import ( "fmt" - "strings" + "path" "github.com/1Panel-dev/1Panel/backend/configs" "github.com/1Panel-dev/1Panel/backend/global" @@ -11,10 +11,11 @@ import ( ) func Init() { + baseDir := "/opt" v := viper.NewWithOptions() v.SetConfigName("app") - v.SetConfigType("yml") - v.AddConfigPath("/opt/1Panel/conf") + v.SetConfigType("yaml") + v.AddConfigPath(path.Dir(baseDir + "/1Panel/conf/app.yaml")) if err := v.ReadInConfig(); err != nil { panic(fmt.Errorf("Fatal error config file: %s \n", err)) } @@ -25,16 +26,16 @@ func Init() { panic(err) } }) - for _, k := range v.AllKeys() { - value := v.GetString(k) - if strings.HasPrefix(value, "${") && strings.Contains(value, "}") { - itemKey := strings.ReplaceAll(value[strings.Index(value, "${"):strings.Index(value, "}")], "${", "") - v.Set(k, strings.ReplaceAll(value, fmt.Sprintf("${%s}", itemKey), v.GetString(itemKey))) - } - } serverConfig := configs.ServerConfig{} if err := v.Unmarshal(&serverConfig); err != nil { panic(err) } global.CONF = serverConfig + global.CONF.BaseDir = baseDir + global.CONF.System.DataDir = global.CONF.BaseDir + "/1Panel/data" + global.CONF.System.Cache = global.CONF.BaseDir + "/1Panel/data/cache" + global.CONF.System.Backup = global.CONF.BaseDir + "/1Panel/data/backup" + global.CONF.System.DbPath = global.CONF.BaseDir + "/1Panel/data/db" + global.CONF.System.LogPath = global.CONF.BaseDir + "/1Panel/log" + global.Viper = v } diff --git a/backend/router/ro_setting.go b/backend/router/ro_setting.go index dcd5d91b7..598c774e6 100644 --- a/backend/router/ro_setting.go +++ b/backend/router/ro_setting.go @@ -18,6 +18,7 @@ func (s *SettingRouter) InitSettingRouter(Router *gin.RouterGroup) { settingRouter.POST("/search", baseApi.GetSettingInfo) settingRouter.POST("/expired/handle", baseApi.HandlePasswordExpired) settingRouter.POST("/update", baseApi.UpdateSetting) + settingRouter.POST("/port/update", baseApi.UpdatePort) settingRouter.POST("/password/update", baseApi.UpdatePassword) settingRouter.POST("/time/sync", baseApi.SyncTime) settingRouter.POST("/monitor/clean", baseApi.CleanMonitor) @@ -29,5 +30,6 @@ func (s *SettingRouter) InitSettingRouter(Router *gin.RouterGroup) { settingRouter.POST("/snapshot/recover", baseApi.RecoverSnapshot) settingRouter.POST("/snapshot/rollback", baseApi.RollbackSnapshot) settingRouter.GET("/upgrade", baseApi.GetUpgradeInfo) + settingRouter.GET("/basedir", baseApi.LoadBaseDir) } } diff --git a/backend/utils/cloud_storage/client/oss_test.go b/backend/utils/cloud_storage/client/oss_test.go index 4f07e876e..bda34ca89 100644 --- a/backend/utils/cloud_storage/client/oss_test.go +++ b/backend/utils/cloud_storage/client/oss_test.go @@ -3,7 +3,6 @@ package client import ( "encoding/json" "fmt" - "io/ioutil" "os" "testing" @@ -56,7 +55,7 @@ func TestCron(t *testing.T) { fmt.Println("my objects:", getObjectsFormResponse(lor)) name := "directory/directory-test1/20220928104331.tar.gz" - targetPath := constant.DownloadDir + "directory/directory-test1/" + targetPath := constant.DataDir + "/download/directory/directory-test1/" if _, err := os.Stat(targetPath); err != nil && os.IsNotExist(err) { if err = os.MkdirAll(targetPath, os.ModePerm); err != nil { fmt.Println(err) @@ -67,17 +66,6 @@ func TestCron(t *testing.T) { } } -func TestDir(t *testing.T) { - files, err := ioutil.ReadDir("/opt/1Panel/task/directory/directory-test1-3") - if len(files) <= 10 { - return - } - for i := 0; i < len(files)-10; i++ { - os.Remove("/opt/1Panel/task/directory/directory-test1-3/" + files[i].Name()) - } - fmt.Println(files, err) -} - func getObjectsFormResponse(lor oss.ListObjectsResult) string { var output string for _, object := range lor.Objects { diff --git a/cmd/server/docs/docs.go b/cmd/server/docs/docs.go index e135c85e4..8ac4b1c64 100644 --- a/cmd/server/docs/docs.go +++ b/cmd/server/docs/docs.go @@ -6144,6 +6144,48 @@ var doc = `{ } } }, + "/settings/port/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新系统端口", + "consumes": [ + "application/json" + ], + "tags": [ + "System Setting" + ], + "summary": "Update system port", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.PortUpdate" + } + } + ], + "responses": { + "200": { + "description": "" + } + }, + "x-panel-log": { + "BeforeFuntions": [], + "bodyKeys": [ + "serverPort" + ], + "formatEN": "update system port =\u003e [serverPort]", + "formatZH": "修改系统端口 =\u003e [serverPort]", + "paramKeys": [] + } + } + }, "/settings/search": { "post": { "security": [ @@ -6200,11 +6242,11 @@ var doc = `{ "x-panel-log": { "BeforeFuntions": [], "bodyKeys": [ - "name", + "from", "description" ], - "formatEN": "Create system backup [name][description]", - "formatZH": "创建系统快照 [name][description]", + "formatEN": "Create system backup [description] to [from]", + "formatZH": "创建系统快照 [description] 到 [from]", "paramKeys": [] } } @@ -9909,6 +9951,19 @@ var doc = `{ } } }, + "dto.PortUpdate": { + "type": "object", + "required": [ + "serverPort" + ], + "properties": { + "serverPort": { + "type": "integer", + "maximum": 65535, + "minimum": 1 + } + } + }, "dto.RecordSearch": { "type": "object", "required": [ @@ -10186,6 +10241,9 @@ var doc = `{ "panelName": { "type": "string" }, + "port": { + "type": "string" + }, "securityEntrance": { "type": "string" }, diff --git a/cmd/server/docs/swagger.json b/cmd/server/docs/swagger.json index 168203e6d..e61c1f090 100644 --- a/cmd/server/docs/swagger.json +++ b/cmd/server/docs/swagger.json @@ -6130,6 +6130,48 @@ } } }, + "/settings/port/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新系统端口", + "consumes": [ + "application/json" + ], + "tags": [ + "System Setting" + ], + "summary": "Update system port", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.PortUpdate" + } + } + ], + "responses": { + "200": { + "description": "" + } + }, + "x-panel-log": { + "BeforeFuntions": [], + "bodyKeys": [ + "serverPort" + ], + "formatEN": "update system port =\u003e [serverPort]", + "formatZH": "修改系统端口 =\u003e [serverPort]", + "paramKeys": [] + } + } + }, "/settings/search": { "post": { "security": [ @@ -6186,11 +6228,11 @@ "x-panel-log": { "BeforeFuntions": [], "bodyKeys": [ - "name", + "from", "description" ], - "formatEN": "Create system backup [name][description]", - "formatZH": "创建系统快照 [name][description]", + "formatEN": "Create system backup [description] to [from]", + "formatZH": "创建系统快照 [description] 到 [from]", "paramKeys": [] } } @@ -9895,6 +9937,19 @@ } } }, + "dto.PortUpdate": { + "type": "object", + "required": [ + "serverPort" + ], + "properties": { + "serverPort": { + "type": "integer", + "maximum": 65535, + "minimum": 1 + } + } + }, "dto.RecordSearch": { "type": "object", "required": [ @@ -10172,6 +10227,9 @@ "panelName": { "type": "string" }, + "port": { + "type": "string" + }, "securityEntrance": { "type": "string" }, diff --git a/cmd/server/docs/swagger.yaml b/cmd/server/docs/swagger.yaml index 9c37f2845..ec80b8275 100644 --- a/cmd/server/docs/swagger.yaml +++ b/cmd/server/docs/swagger.yaml @@ -1090,6 +1090,15 @@ definitions: hostPort: type: integer type: object + dto.PortUpdate: + properties: + serverPort: + maximum: 65535 + minimum: 1 + type: integer + required: + - serverPort + type: object dto.RecordSearch: properties: detailName: @@ -1274,6 +1283,8 @@ definitions: type: string panelName: type: string + port: + type: string securityEntrance: type: string serverPort: @@ -6464,6 +6475,33 @@ paths: formatEN: update system password formatZH: 修改系统密码 paramKeys: [] + /settings/port/update: + post: + consumes: + - application/json + description: 更新系统端口 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.PortUpdate' + responses: + "200": + description: "" + security: + - ApiKeyAuth: [] + summary: Update system port + tags: + - System Setting + x-panel-log: + BeforeFuntions: [] + bodyKeys: + - serverPort + formatEN: update system port => [serverPort] + formatZH: 修改系统端口 => [serverPort] + paramKeys: [] /settings/search: post: description: 加载系统配置信息 @@ -6500,10 +6538,10 @@ paths: x-panel-log: BeforeFuntions: [] bodyKeys: - - name + - from - description - formatEN: Create system backup [name][description] - formatZH: 创建系统快照 [name][description] + formatEN: Create system backup [description] to [from] + formatZH: 创建系统快照 [description] 到 [from] paramKeys: [] /settings/snapshot/del: post: diff --git a/frontend/src/api/interface/setting.ts b/frontend/src/api/interface/setting.ts index 21ba5b17e..fbf02336c 100644 --- a/frontend/src/api/interface/setting.ts +++ b/frontend/src/api/interface/setting.ts @@ -38,6 +38,9 @@ export namespace Setting { oldPassword: string; newPassword: string; } + export interface PortUpdate { + serverPort: number; + } export interface MFAInfo { secret: string; qrImage: string; diff --git a/frontend/src/api/modules/setting.ts b/frontend/src/api/modules/setting.ts index ee4a07fb7..c6abab7d5 100644 --- a/frontend/src/api/modules/setting.ts +++ b/frontend/src/api/modules/setting.ts @@ -14,6 +14,10 @@ export const updatePassword = (param: Setting.PasswordUpdate) => { return http.post(`/settings/password/update`, param); }; +export const updatePort = (param: Setting.PortUpdate) => { + return http.post(`/settings/port/update`, param); +}; + export const handleExpired = (param: Setting.PasswordUpdate) => { return http.post(`/settings/expired/handle`, param); }; @@ -38,6 +42,10 @@ export const bindMFA = (param: Setting.MFABind) => { return http.post(`/settings/mfa/bind`, param); }; +export const loadBaseDir = () => { + return http.get(`/settings/basedir`); +}; + // snapshot export const snapshotCreate = (param: Setting.SnapshotCreate) => { return http.post(`/settings/snapshot`, param); diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index 0056577bb..29e9f3f25 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -679,8 +679,15 @@ export default { setting: { all: 'All', panel: 'Panel', + user: 'UserName', + passwd: 'Password', emailHelper: 'For password retrieval', title: 'Panel alias', + panelPort: 'Panel port', + portHelper: + 'The recommended port range is 8888 to 65535. Note: If the server has a security group, permit the new port from the security group in advance', + portChange: 'Port change', + portChangeHelper: 'Modify the service port and restart the service. Do you want to continue?', theme: 'Theme', dark: 'Dark', light: 'Light', @@ -699,6 +706,7 @@ export default { duplicatePassword: 'The new password cannot be the same as the original password, please re-enter!', backup: 'Backup', + thirdParty: 'Third-party', createBackupAccount: 'Create {0} backup account', noTypeForCreate: 'No backup type is currently created', serverDisk: 'Server disks', @@ -716,9 +724,6 @@ export default { path: 'Path', safe: 'Safe', - panelPort: 'Panel port', - portHelper: - 'The recommended port range is 8888 to 65535. Note: If the server has a security group, permit the new port from the security group in advance', safeEntrance: 'Security entrance', safeEntranceHelper: 'Panel management portal. You can log in to the panel only through a specified security portal, for example: onepanel', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 6e1bcba6b..c526748f1 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -694,12 +694,18 @@ export default { setting: { all: '全部', panel: '面板', + user: '用户名称', + passwd: '帐户密码', emailHelper: '用于密码找回', title: '面板别名', - theme: '主题色', + panelPort: '面板端口', + portHelper: '建议端口范围8888 - 65535,注意:有安全组的服务器请提前在安全组放行新端口', + portChange: '端口修改', + portChangeHelper: '服务端口修改需要重启服务,是否继续?', + theme: '主题颜色', componentSize: '组件大小', - dark: '黑金', - light: '白金', + dark: '暗色', + light: '亮色', language: '系统语言', languageHelper: '默认跟随浏览器语言,设置后只对当前浏览器生效,更换浏览器后需要重新设置', sessionTimeout: '超时时间', @@ -713,6 +719,7 @@ export default { duplicatePassword: '新密码不能与原始密码一致,请重新输入!', backup: '备份', + thirdParty: '第三方', createBackupAccount: '添加 {0} 备份账号', noTypeForCreate: '当前无可创建备份类型', serverDisk: '服务器磁盘', @@ -756,8 +763,6 @@ export default { upgradeNow: '立即更新', safe: '安全', - panelPort: '面板端口', - portHelper: '建议端口范围8888 - 65535,注意:有安全组的服务器请提前在安全组放行新端口', safeEntrance: '安全入口', safeEntranceHelper: '面板管理入口,设置后只能通过指定安全入口登录面板,如: onepanel', expirationTime: '密码过期时间', diff --git a/frontend/src/views/database/mysql/setting/index.vue b/frontend/src/views/database/mysql/setting/index.vue index 85fa509ee..8e47f8ee8 100644 --- a/frontend/src/views/database/mysql/setting/index.vue +++ b/frontend/src/views/database/mysql/setting/index.vue @@ -101,6 +101,7 @@ import { loadMysqlBaseInfo, loadMysqlVariables, updateMysqlConfByFile } from '@/ import { ChangePort, GetAppDefaultConfig } from '@/api/modules/app'; import { Rules } from '@/global/form-rules'; import i18n from '@/lang'; +import { loadBaseDir } from '@/api/modules/setting'; const loading = ref(false); @@ -241,7 +242,8 @@ const loadBaseInfo = async () => { mysqlName.value = res.data?.name; baseInfo.port = res.data?.port; baseInfo.containerID = res.data?.containerName; - loadMysqlConf(`/opt/1Panel/data/apps/mysql/${mysqlName.value}/conf/my.cnf`); + const pathRes = await loadBaseDir(); + loadMysqlConf(`${pathRes.data}/apps/mysql/${mysqlName.value}/conf/my.cnf`); loadContainerLog(baseInfo.containerID); }; diff --git a/frontend/src/views/database/mysql/setting/slow-log/index.vue b/frontend/src/views/database/mysql/setting/slow-log/index.vue index 30594a2f2..1f1a7762a 100644 --- a/frontend/src/views/database/mysql/setting/slow-log/index.vue +++ b/frontend/src/views/database/mysql/setting/slow-log/index.vue @@ -59,6 +59,7 @@ import { updateMysqlVariables } from '@/api/modules/database'; import { ElMessage } from 'element-plus'; import { dateFromatForName } from '@/utils/util'; import i18n from '@/lang'; +import { loadBaseDir } from '@/api/modules/setting'; const loading = ref(); const extensions = [javascript(), oneDark]; @@ -84,12 +85,13 @@ interface DialogProps { mysqlName: string; variables: Database.MysqlVariables; } -const acceptParams = (params: DialogProps): void => { +const acceptParams = async (params: DialogProps): Promise => { mysqlName.value = params.mysqlName; variables.slow_query_log = params.variables.slow_query_log; variables.long_query_time = Number(params.variables.long_query_time); - let path = `/opt/1Panel/data/apps/mysql/${mysqlName.value}/data/1Panel-slow.log`; + const pathRes = await loadBaseDir(); + let path = `${pathRes.data}/apps/mysql/${mysqlName.value}/data/1Panel-slow.log`; if (variables.slow_query_log === 'ON') { loadMysqlSlowlogs(path); } diff --git a/frontend/src/views/database/mysql/upload/index.vue b/frontend/src/views/database/mysql/upload/index.vue index ddb6202d7..82fe48ecb 100644 --- a/frontend/src/views/database/mysql/upload/index.vue +++ b/frontend/src/views/database/mysql/upload/index.vue @@ -73,6 +73,7 @@ import i18n from '@/lang'; import { ElMessage, UploadFile, UploadFiles, UploadInstance, UploadProps } from 'element-plus'; import { File } from '@/api/interface/file'; import { BatchDeleteFile, GetFilesList, UploadFileData } from '@/api/modules/files'; +import { loadBaseDir } from '@/api/modules/setting'; const selects = ref([]); const baseDir = ref(); @@ -91,10 +92,12 @@ interface DialogProps { mysqlName: string; dbName: string; } -const acceptParams = (params: DialogProps): void => { +const acceptParams = async (params: DialogProps): Promise => { mysqlName.value = params.mysqlName; dbName.value = params.dbName; - baseDir.value = '/opt/1Panel/data/uploads/database/mysql/' + mysqlName.value + '/' + dbName.value + '/'; + + const pathRes = await loadBaseDir(); + baseDir.value = `${pathRes.data}/uploads/database/mysql/${mysqlName.value}/${dbName.value}/`; upVisiable.value = true; search(); }; diff --git a/frontend/src/views/database/redis/setting/index.vue b/frontend/src/views/database/redis/setting/index.vue index d7d42a338..31257f7bf 100644 --- a/frontend/src/views/database/redis/setting/index.vue +++ b/frontend/src/views/database/redis/setting/index.vue @@ -120,6 +120,7 @@ import { loadRedisConf, updateRedisConf, updateRedisConfByFile } from '@/api/mod import i18n from '@/lang'; import { Rules } from '@/global/form-rules'; import { ChangePort, GetAppDefaultConfig } from '@/api/modules/app'; +import { loadBaseDir } from '@/api/modules/setting'; const extensions = [javascript(), oneDark]; @@ -292,7 +293,8 @@ const loadform = async () => { }; const loadConfFile = async () => { - let path = `/opt/1Panel/data/apps/redis/${redisName.value}/conf/redis.conf`; + const pathRes = await loadBaseDir(); + let path = `${pathRes.data}/apps/redis/${redisName.value}/conf/redis.conf`; const res = await LoadFile({ path: path }); redisConf.value = res.data; }; diff --git a/frontend/src/views/log/system/index.vue b/frontend/src/views/log/system/index.vue index 8e30eda89..310060578 100644 --- a/frontend/src/views/log/system/index.vue +++ b/frontend/src/views/log/system/index.vue @@ -28,6 +28,7 @@ import { oneDark } from '@codemirror/theme-one-dark'; import { nextTick, onMounted, ref, shallowRef } from 'vue'; import Submenu from '@/views/log/index.vue'; import { LoadFile } from '@/api/modules/files'; +import { loadBaseDir } from '@/api/modules/setting'; const extensions = [javascript(), oneDark]; const logs = ref(); @@ -37,7 +38,9 @@ const handleReady = (payload) => { }; const loadSystemlogs = async () => { - const res = await LoadFile({ path: '/opt/1Panel/log/1Panel.log' }); + const pathRes = await loadBaseDir(); + let logPath = pathRes.data.replaceAll('/data', '/log'); + const res = await LoadFile({ path: `${logPath}/1Panel.log` }); logs.value = res.data; nextTick(() => { const state = view.value.state; diff --git a/frontend/src/views/setting/backup-account/index.vue b/frontend/src/views/setting/backup-account/index.vue index 0e61b514e..bb70f7b82 100644 --- a/frontend/src/views/setting/backup-account/index.vue +++ b/frontend/src/views/setting/backup-account/index.vue @@ -2,7 +2,8 @@
- +
Local
+