mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2024-12-18 11:17:49 +08:00
feat: 实现系统快照功能
This commit is contained in:
parent
183555e4c6
commit
e3a15e88d7
@ -43,5 +43,6 @@ var (
|
|||||||
|
|
||||||
nginxService = service.ServiceGroupApp.NginxService
|
nginxService = service.ServiceGroupApp.NginxService
|
||||||
|
|
||||||
logService = service.ServiceGroupApp.LogService
|
logService = service.ServiceGroupApp.LogService
|
||||||
|
snapshotService = service.ServiceGroupApp.SnapshotService
|
||||||
)
|
)
|
||||||
|
60
backend/app/api/v1/snapshot.go
Normal file
60
backend/app/api/v1/snapshot.go
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
package v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/1Panel-dev/1Panel/backend/app/api/v1/helper"
|
||||||
|
"github.com/1Panel-dev/1Panel/backend/app/dto"
|
||||||
|
"github.com/1Panel-dev/1Panel/backend/constant"
|
||||||
|
"github.com/1Panel-dev/1Panel/backend/global"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// @Tags System Setting
|
||||||
|
// @Summary Create snapshot
|
||||||
|
// @Description 创建系统快照
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.SnapshotCreate true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Router /settings/snapshot [post]
|
||||||
|
// @x-panel-log {"bodyKeys":["name", "description"],"paramKeys":[],"BeforeFuntions":[],"formatZH":"创建系统快照 [name][description]","formatEN":"Create system snapshot [name][description]"}
|
||||||
|
func (b *BaseApi) CreateSnapshot(c *gin.Context) {
|
||||||
|
var req dto.SnapshotCreate
|
||||||
|
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 := snapshotService.Create(req); err != nil {
|
||||||
|
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags System Setting
|
||||||
|
// @Summary Page system snapshot
|
||||||
|
// @Description 获取系统快照列表分页
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.PageInfo true "request"
|
||||||
|
// @Success 200 {object} dto.PageResult
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Router /websites/acme/search [post]
|
||||||
|
func (b *BaseApi) SearchSnapshot(c *gin.Context) {
|
||||||
|
var req dto.PageInfo
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
total, accounts, err := snapshotService.SearchWithPage(req)
|
||||||
|
if err != nil {
|
||||||
|
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, dto.PageResult{
|
||||||
|
Total: total,
|
||||||
|
Items: accounts,
|
||||||
|
})
|
||||||
|
}
|
@ -1,5 +1,7 @@
|
|||||||
package dto
|
package dto
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
type SettingInfo struct {
|
type SettingInfo struct {
|
||||||
UserName string `json:"userName"`
|
UserName string `json:"userName"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
@ -37,3 +39,17 @@ type PasswordUpdate struct {
|
|||||||
OldPassword string `json:"oldPassword" validate:"required"`
|
OldPassword string `json:"oldPassword" validate:"required"`
|
||||||
NewPassword string `json:"newPassword" validate:"required"`
|
NewPassword string `json:"newPassword" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SnapshotCreate struct {
|
||||||
|
BackupType string `json:"backupType" validate:"required,oneof=OSS S3 SFTP MINIO"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
}
|
||||||
|
type SnapshotInfo struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
BackupType string `json:"backupType"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
}
|
||||||
|
11
backend/app/model/snapshot.go
Normal file
11
backend/app/model/snapshot.go
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
type Snapshot struct {
|
||||||
|
BaseModel
|
||||||
|
Name string `json:"name" gorm:"type:varchar(64);not null;unique"`
|
||||||
|
Description string `json:"description" gorm:"type:varchar(256)"`
|
||||||
|
BackupType string `json:"backupType" gorm:"type:varchar(64)"`
|
||||||
|
Status string `json:"status" gorm:"type:varchar(64)"`
|
||||||
|
Message string `json:"message" gorm:"type:varchar(256)"`
|
||||||
|
Version string `json:"version" gorm:"type:varchar(256)"`
|
||||||
|
}
|
@ -2,6 +2,7 @@ package repo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/backend/constant"
|
"github.com/1Panel-dev/1Panel/backend/constant"
|
||||||
"github.com/1Panel-dev/1Panel/backend/global"
|
"github.com/1Panel-dev/1Panel/backend/global"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@ -12,6 +13,7 @@ type DBOption func(*gorm.DB) *gorm.DB
|
|||||||
type ICommonRepo interface {
|
type ICommonRepo interface {
|
||||||
WithByID(id uint) DBOption
|
WithByID(id uint) DBOption
|
||||||
WithByName(name string) DBOption
|
WithByName(name string) DBOption
|
||||||
|
WithByType(tp string) DBOption
|
||||||
WithOrderBy(orderStr string) DBOption
|
WithOrderBy(orderStr string) DBOption
|
||||||
WithLikeName(name string) DBOption
|
WithLikeName(name string) DBOption
|
||||||
WithIdsIn(ids []uint) DBOption
|
WithIdsIn(ids []uint) DBOption
|
||||||
@ -31,9 +33,9 @@ func (c *CommonRepo) WithByName(name string) DBOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CommonRepo) WithByType(name string) DBOption {
|
func (c *CommonRepo) WithByType(tp string) DBOption {
|
||||||
return func(g *gorm.DB) *gorm.DB {
|
return func(g *gorm.DB) *gorm.DB {
|
||||||
return g.Where("type = ?", name)
|
return g.Where("type = ?", tp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,6 +25,7 @@ type RepoGroup struct {
|
|||||||
WebsiteSSLRepo
|
WebsiteSSLRepo
|
||||||
WebsiteAcmeAccountRepo
|
WebsiteAcmeAccountRepo
|
||||||
LogRepo
|
LogRepo
|
||||||
|
SnapshotRepo
|
||||||
}
|
}
|
||||||
|
|
||||||
var RepoGroupApp = new(RepoGroup)
|
var RepoGroupApp = new(RepoGroup)
|
||||||
|
58
backend/app/repo/snapshot.go
Normal file
58
backend/app/repo/snapshot.go
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/1Panel-dev/1Panel/backend/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/backend/global"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ISnapshotRepo interface {
|
||||||
|
Get(opts ...DBOption) (model.Snapshot, error)
|
||||||
|
Page(limit, offset int, opts ...DBOption) (int64, []model.Snapshot, error)
|
||||||
|
Create(snapshot *model.Snapshot) error
|
||||||
|
Update(id uint, vars map[string]interface{}) error
|
||||||
|
Delete(opts ...DBOption) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewISnapshotRepo() ISnapshotRepo {
|
||||||
|
return &SnapshotRepo{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type SnapshotRepo struct{}
|
||||||
|
|
||||||
|
func (u *SnapshotRepo) Get(opts ...DBOption) (model.Snapshot, error) {
|
||||||
|
var snapshot model.Snapshot
|
||||||
|
db := global.DB
|
||||||
|
for _, opt := range opts {
|
||||||
|
db = opt(db)
|
||||||
|
}
|
||||||
|
err := db.First(&snapshot).Error
|
||||||
|
return snapshot, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *SnapshotRepo) Page(page, size int, opts ...DBOption) (int64, []model.Snapshot, error) {
|
||||||
|
var users []model.Snapshot
|
||||||
|
db := global.DB.Model(&model.Snapshot{})
|
||||||
|
for _, opt := range opts {
|
||||||
|
db = opt(db)
|
||||||
|
}
|
||||||
|
count := int64(0)
|
||||||
|
db = db.Count(&count)
|
||||||
|
err := db.Limit(size).Offset(size * (page - 1)).Find(&users).Error
|
||||||
|
return count, users, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *SnapshotRepo) Create(snapshot *model.Snapshot) error {
|
||||||
|
return global.DB.Create(snapshot).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *SnapshotRepo) Update(id uint, vars map[string]interface{}) error {
|
||||||
|
return global.DB.Model(&model.Snapshot{}).Where("id = ?", id).Updates(vars).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *SnapshotRepo) Delete(opts ...DBOption) error {
|
||||||
|
db := global.DB
|
||||||
|
for _, opt := range opts {
|
||||||
|
db = opt(db)
|
||||||
|
}
|
||||||
|
return db.Delete(&model.Snapshot{}).Error
|
||||||
|
}
|
@ -167,7 +167,7 @@ func (u *ContainerService) ContainerCreate(req dto.ContainerCreate) error {
|
|||||||
_ = client.ContainerRemove(context.Background(), req.Name, types.ContainerRemoveOptions{RemoveVolumes: true, Force: true})
|
_ = client.ContainerRemove(context.Background(), req.Name, types.ContainerRemoveOptions{RemoveVolumes: true, Force: true})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
global.LOG.Infof("create container successful! now check if the container is started and delete the container information if it is not.", req.Name)
|
global.LOG.Infof("create container %s successful! now check if the container is started and delete the container information if it is not.", req.Name)
|
||||||
if err := client.ContainerStart(context.TODO(), container.ID, types.ContainerStartOptions{}); err != nil {
|
if err := client.ContainerStart(context.TODO(), container.ID, types.ContainerStartOptions{}); err != nil {
|
||||||
_ = client.ContainerRemove(context.Background(), req.Name, types.ContainerRemoveOptions{RemoveVolumes: true, Force: true})
|
_ = client.ContainerRemove(context.Background(), req.Name, types.ContainerRemoveOptions{RemoveVolumes: true, Force: true})
|
||||||
return fmt.Errorf("create successful but start failed, err: %v", err)
|
return fmt.Errorf("create successful but start failed, err: %v", err)
|
||||||
|
@ -641,7 +641,7 @@ func backupMysql(backupType, baseDir, backupDir, mysqlName, dbName, fileName str
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
outfile, _ := os.OpenFile(fullDir+"/"+fileName, os.O_RDWR|os.O_CREATE, 0755)
|
outfile, _ := os.OpenFile(fullDir+"/"+fileName, os.O_RDWR|os.O_CREATE, 0755)
|
||||||
global.LOG.Infof("start to mysqldump | gzip > %s.gzip", outfile)
|
global.LOG.Infof("start to mysqldump | gzip > %s.gzip", fullDir+"/"+fileName)
|
||||||
cmd := exec.Command("docker", "exec", app.ContainerName, "mysqldump", "-uroot", "-p"+app.Password, dbName)
|
cmd := exec.Command("docker", "exec", app.ContainerName, "mysqldump", "-uroot", "-p"+app.Password, dbName)
|
||||||
gzipCmd := exec.Command("gzip", "-cf")
|
gzipCmd := exec.Command("gzip", "-cf")
|
||||||
gzipCmd.Stdin, _ = cmd.StdoutPipe()
|
gzipCmd.Stdin, _ = cmd.StdoutPipe()
|
||||||
|
@ -37,6 +37,7 @@ type ServiceGroup struct {
|
|||||||
NginxService
|
NginxService
|
||||||
|
|
||||||
LogService
|
LogService
|
||||||
|
SnapshotService
|
||||||
}
|
}
|
||||||
|
|
||||||
var ServiceGroupApp = new(ServiceGroup)
|
var ServiceGroupApp = new(ServiceGroup)
|
||||||
@ -73,5 +74,6 @@ var (
|
|||||||
websiteSSLRepo = repo.NewISSLRepo()
|
websiteSSLRepo = repo.NewISSLRepo()
|
||||||
websiteAcmeRepo = repo.NewIAcmeAccountRepo()
|
websiteAcmeRepo = repo.NewIAcmeAccountRepo()
|
||||||
|
|
||||||
logRepo = repo.RepoGroupApp.LogRepo
|
logRepo = repo.RepoGroupApp.LogRepo
|
||||||
|
snapshotRepo = repo.NewISnapshotRepo()
|
||||||
)
|
)
|
||||||
|
150
backend/app/service/snapshot.go
Normal file
150
backend/app/service/snapshot.go
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"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/cmd"
|
||||||
|
"github.com/1Panel-dev/1Panel/backend/utils/docker"
|
||||||
|
"github.com/1Panel-dev/1Panel/backend/utils/files"
|
||||||
|
"github.com/jinzhu/copier"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SnapshotService struct{}
|
||||||
|
|
||||||
|
type ISnapshotService interface {
|
||||||
|
SearchWithPage(req dto.PageInfo) (int64, interface{}, error)
|
||||||
|
Create(req dto.SnapshotCreate) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewISnapshotService() ISnapshotService {
|
||||||
|
return &SnapshotService{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *SnapshotService) SearchWithPage(req dto.PageInfo) (int64, interface{}, error) {
|
||||||
|
total, snapshots, err := snapshotRepo.Page(req.Page, req.PageSize)
|
||||||
|
var dtoSnap []dto.SnapshotInfo
|
||||||
|
for _, snapshot := range snapshots {
|
||||||
|
var item dto.SnapshotInfo
|
||||||
|
if err := copier.Copy(&item, &snapshot); err != nil {
|
||||||
|
return 0, nil, errors.WithMessage(constant.ErrStructTransform, err.Error())
|
||||||
|
}
|
||||||
|
dtoSnap = append(dtoSnap, item)
|
||||||
|
}
|
||||||
|
return total, dtoSnap, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *SnapshotService) Create(req dto.SnapshotCreate) error {
|
||||||
|
localDir, err := loadLocalDir()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
backup, err := backupRepo.Get(commonRepo.WithByType(req.BackupType))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
backupAccont, err := NewIBackupService().NewClient(&backup)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
timeNow := time.Now().Format("20060102150405")
|
||||||
|
rootDir := fmt.Sprintf("/tmp/songliu/1panel_backup_%s", timeNow)
|
||||||
|
backupPanelDir := fmt.Sprintf("%s/1panel", rootDir)
|
||||||
|
_ = os.MkdirAll(backupPanelDir, os.ModePerm)
|
||||||
|
backupDockerDir := fmt.Sprintf("%s/docker", rootDir)
|
||||||
|
_ = os.MkdirAll(backupDockerDir, os.ModePerm)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
_, _ = cmd.Exec("systemctl start docker")
|
||||||
|
_ = os.RemoveAll(rootDir)
|
||||||
|
}()
|
||||||
|
|
||||||
|
fileOp := files.NewFileOp()
|
||||||
|
if err := fileOp.Compress([]string{localDir}, backupPanelDir, "1panel_backup.tar.gz", files.TarGz); err != nil {
|
||||||
|
global.LOG.Errorf("snapshot backup 1panel backup datas %s failed, err: %v", localDir, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
client, err := docker.NewDockerClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
info, err := client.Info(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dataDir := info.DockerRootDir
|
||||||
|
stdout, err := cmd.Exec("systemctl stop docker")
|
||||||
|
if err != nil {
|
||||||
|
return errors.New(stdout)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat("/etc/systemd/system/1panel.service"); err == nil {
|
||||||
|
if err := fileOp.Compress([]string{dataDir}, backupDockerDir, "docker_data.tar.gz", files.TarGz); err != nil {
|
||||||
|
global.LOG.Errorf("snapshot backup docker data dir %s failed, err: %v", dataDir, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(constant.DaemonJsonPath); err == nil {
|
||||||
|
if err := fileOp.CopyFile(constant.DaemonJsonPath, backupDockerDir); err != nil {
|
||||||
|
global.LOG.Errorf("snapshot backup daemon.json failed, err: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat("/Users/slooop/go/bin/swag"); err == nil {
|
||||||
|
if err := fileOp.CopyFile("/Users/slooop/go/bin/swag", backupPanelDir); err != nil {
|
||||||
|
global.LOG.Errorf("snapshot backup 1panel failed, err: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err := os.Stat("/etc/systemd/system/1panel.service"); err == nil {
|
||||||
|
if err := fileOp.CopyFile("/etc/systemd/system/1panel.service", backupPanelDir); err != nil {
|
||||||
|
global.LOG.Errorf("snapshot backup 1panel.service failed, err: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err := os.Stat("/usr/local/bin/1panelctl"); err == nil {
|
||||||
|
if err := fileOp.CopyFile("/usr/local/bin/1panelctl", backupPanelDir); err != nil {
|
||||||
|
global.LOG.Errorf("snapshot backup 1panelctl failed, err: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(global.CONF.System.DataDir); err == nil {
|
||||||
|
if err := fileOp.Compress([]string{global.CONF.System.DataDir}, backupPanelDir, "1panel_data.tar.gz", files.TarGz); err != nil {
|
||||||
|
global.LOG.Errorf("snapshot backup 1panel data %s failed, err: %v", global.CONF.System.DataDir, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := fileOp.Compress([]string{rootDir}, fmt.Sprintf("%s/system", localDir), fmt.Sprintf("1panel_backup_%s.tar.gz", timeNow), files.TarGz); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
snap := model.Snapshot{
|
||||||
|
Name: "1panel_backup_" + timeNow,
|
||||||
|
Description: req.Description,
|
||||||
|
BackupType: req.BackupType,
|
||||||
|
Status: constant.StatusWaiting,
|
||||||
|
}
|
||||||
|
_ = snapshotRepo.Create(&snap)
|
||||||
|
go func() {
|
||||||
|
localPath := fmt.Sprintf("%s/system/1panel_backup_%s.tar.gz", localDir, timeNow)
|
||||||
|
if ok, err := backupAccont.Upload(localPath, fmt.Sprintf("system_snapshot/1panel_backup_%s.tar.gz", timeNow)); err != nil || !ok {
|
||||||
|
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
|
||||||
|
global.LOG.Errorf("upload snapshot to %s failed, err: %v", backup.Type, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
snap.Status = constant.StatusSuccess
|
||||||
|
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusSuccess})
|
||||||
|
global.LOG.Infof("upload snapshot to %s success", backup.Type)
|
||||||
|
}()
|
||||||
|
return nil
|
||||||
|
}
|
34
backend/app/service/snapshot_test.go
Normal file
34
backend/app/service/snapshot_test.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/backend/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/backend/global"
|
||||||
|
"github.com/1Panel-dev/1Panel/backend/init/db"
|
||||||
|
"github.com/1Panel-dev/1Panel/backend/init/viper"
|
||||||
|
"github.com/1Panel-dev/1Panel/backend/utils/files"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSnaa(t *testing.T) {
|
||||||
|
fileOp := files.NewFileOp()
|
||||||
|
|
||||||
|
fmt.Println(fileOp.CopyFile("/Users/slooop/Documents/编码规范.pdf", "/Users/slooop/Downloads"))
|
||||||
|
// fmt.Println(fileOp.Compress([]string{"/Users/slooop/Documents/编码规范.pdf", "/Users/slooop/Downloads/1Panel.db"}, "/Users/slooop/Downloads/", "test.tar.gz", files.TarGz))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOss(t *testing.T) {
|
||||||
|
viper.Init()
|
||||||
|
db.Init()
|
||||||
|
|
||||||
|
var backup model.BackupAccount
|
||||||
|
if err := global.DB.Where("id = ?", 6).First(&backup).Error; err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
backupAccont, err := NewIBackupService().NewClient(&backup)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
fmt.Println(backupAccont.Upload("/Users/slooop/Downloads/1Panel.db", "database/1Panel.db"))
|
||||||
|
}
|
@ -19,6 +19,7 @@ func Init() {
|
|||||||
migrations.AddTableImageRepo,
|
migrations.AddTableImageRepo,
|
||||||
migrations.AddTableWebsite,
|
migrations.AddTableWebsite,
|
||||||
migrations.AddTableDatabaseMysql,
|
migrations.AddTableDatabaseMysql,
|
||||||
|
migrations.AddTableSnapshot,
|
||||||
})
|
})
|
||||||
if err := m.Migrate(); err != nil {
|
if err := m.Migrate(); err != nil {
|
||||||
global.LOG.Error(err)
|
global.LOG.Error(err)
|
||||||
|
@ -202,3 +202,13 @@ var AddTableWebsite = &gormigrate.Migration{
|
|||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var AddTableSnapshot = &gormigrate.Migration{
|
||||||
|
ID: "20230106-add-table-snapshot",
|
||||||
|
Migrate: func(tx *gorm.DB) error {
|
||||||
|
if err := tx.AutoMigrate(&model.Snapshot{}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
@ -2,6 +2,8 @@ package viper
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/backend/configs"
|
"github.com/1Panel-dev/1Panel/backend/configs"
|
||||||
"github.com/1Panel-dev/1Panel/backend/global"
|
"github.com/1Panel-dev/1Panel/backend/global"
|
||||||
"github.com/fsnotify/fsnotify"
|
"github.com/fsnotify/fsnotify"
|
||||||
@ -23,6 +25,13 @@ func Init() {
|
|||||||
panic(err)
|
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{}
|
serverConfig := configs.ServerConfig{}
|
||||||
if err := v.Unmarshal(&serverConfig); err != nil {
|
if err := v.Unmarshal(&serverConfig); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
@ -24,5 +24,7 @@ func (s *SettingRouter) InitSettingRouter(Router *gin.RouterGroup) {
|
|||||||
settingRouter.POST("/monitor/clean", baseApi.CleanMonitor)
|
settingRouter.POST("/monitor/clean", baseApi.CleanMonitor)
|
||||||
settingRouter.GET("/mfa", baseApi.GetMFA)
|
settingRouter.GET("/mfa", baseApi.GetMFA)
|
||||||
settingRouter.POST("/mfa/bind", baseApi.MFABind)
|
settingRouter.POST("/mfa/bind", baseApi.MFABind)
|
||||||
|
settingRouter.POST("/snapshot", baseApi.CreateSnapshot)
|
||||||
|
settingRouter.POST("/snapshot/search", baseApi.SearchSnapshot)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { DateTimeFormats } from '@intlify/core-base';
|
||||||
|
|
||||||
export namespace Setting {
|
export namespace Setting {
|
||||||
export interface SettingInfo {
|
export interface SettingInfo {
|
||||||
userName: string;
|
userName: string;
|
||||||
@ -43,4 +45,17 @@ export namespace Setting {
|
|||||||
secret: string;
|
secret: string;
|
||||||
code: string;
|
code: string;
|
||||||
}
|
}
|
||||||
|
export interface SnapshotCreate {
|
||||||
|
description: string;
|
||||||
|
backupType: string;
|
||||||
|
}
|
||||||
|
export interface SnapshotInfo {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
backupType: string;
|
||||||
|
status: string;
|
||||||
|
message: string;
|
||||||
|
createdAt: DateTimeFormats;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import http from '@/api';
|
import http from '@/api';
|
||||||
|
import { ReqPage, ResPage } from '../interface';
|
||||||
import { Setting } from '../interface/setting';
|
import { Setting } from '../interface/setting';
|
||||||
|
|
||||||
export const getSettingInfo = () => {
|
export const getSettingInfo = () => {
|
||||||
@ -36,3 +37,11 @@ export const loadDaemonJsonPath = () => {
|
|||||||
export const bindMFA = (param: Setting.MFABind) => {
|
export const bindMFA = (param: Setting.MFABind) => {
|
||||||
return http.post(`/settings/mfa/bind`, param);
|
return http.post(`/settings/mfa/bind`, param);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// snapshot
|
||||||
|
export const snapshotCreate = (param: Setting.SnapshotCreate) => {
|
||||||
|
return http.post(`/settings/snapshot`, param);
|
||||||
|
};
|
||||||
|
export const searchSnapshotPage = (param: ReqPage) => {
|
||||||
|
return http.post<ResPage<Setting.SnapshotInfo>>(`/settings/snapshot/search`, param);
|
||||||
|
};
|
||||||
|
@ -603,6 +603,7 @@ export default {
|
|||||||
containers: 'Container',
|
containers: 'Container',
|
||||||
commands: 'Command',
|
commands: 'Command',
|
||||||
groups: 'System Group',
|
groups: 'System Group',
|
||||||
|
files: 'File Manage',
|
||||||
backups: 'Backup Account',
|
backups: 'Backup Account',
|
||||||
logs: 'Panel Logs',
|
logs: 'Panel Logs',
|
||||||
settings: 'Panel Setting',
|
settings: 'Panel Setting',
|
||||||
@ -735,6 +736,8 @@ export default {
|
|||||||
mfaHelper2: 'Scan the following QR code using the mobile app to obtain the 6-digit verification code',
|
mfaHelper2: 'Scan the following QR code using the mobile app to obtain the 6-digit verification code',
|
||||||
mfaHelper3: 'Enter six digits from the app',
|
mfaHelper3: 'Enter six digits from the app',
|
||||||
|
|
||||||
|
snapshot: 'Snapshot',
|
||||||
|
|
||||||
enableMonitor: 'Enable',
|
enableMonitor: 'Enable',
|
||||||
storeDays: 'Expiration time (day)',
|
storeDays: 'Expiration time (day)',
|
||||||
cleanMonitor: 'Clearing monitoring records',
|
cleanMonitor: 'Clearing monitoring records',
|
||||||
|
@ -617,6 +617,7 @@ export default {
|
|||||||
containers: '容器',
|
containers: '容器',
|
||||||
groups: '系统组',
|
groups: '系统组',
|
||||||
commands: '快捷命令',
|
commands: '快捷命令',
|
||||||
|
files: '文件管理',
|
||||||
backups: '备份账号',
|
backups: '备份账号',
|
||||||
logs: '面板日志',
|
logs: '面板日志',
|
||||||
settings: '面板设置',
|
settings: '面板设置',
|
||||||
@ -724,6 +725,8 @@ export default {
|
|||||||
password: '密码',
|
password: '密码',
|
||||||
path: '路径',
|
path: '路径',
|
||||||
|
|
||||||
|
snapshot: '快照',
|
||||||
|
|
||||||
safe: '安全',
|
safe: '安全',
|
||||||
panelPort: '面板端口',
|
panelPort: '面板端口',
|
||||||
portHelper: '建议端口范围8888 - 65535,注意:有安全组的服务器请提前在安全组放行新端口',
|
portHelper: '建议端口范围8888 - 65535,注意:有安全组的服务器请提前在安全组放行新端口',
|
||||||
|
@ -16,6 +16,7 @@ const settingRouter = {
|
|||||||
component: () => import('@/views/setting/panel/index.vue'),
|
component: () => import('@/views/setting/panel/index.vue'),
|
||||||
hidden: true,
|
hidden: true,
|
||||||
meta: {
|
meta: {
|
||||||
|
requiresAuth: true,
|
||||||
key: 'Setting',
|
key: 'Setting',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -25,6 +26,7 @@ const settingRouter = {
|
|||||||
component: () => import('@/views/setting/backup-account/index.vue'),
|
component: () => import('@/views/setting/backup-account/index.vue'),
|
||||||
hidden: true,
|
hidden: true,
|
||||||
meta: {
|
meta: {
|
||||||
|
requiresAuth: true,
|
||||||
key: 'Setting',
|
key: 'Setting',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -34,6 +36,7 @@ const settingRouter = {
|
|||||||
component: () => import('@/views/setting/about/index.vue'),
|
component: () => import('@/views/setting/about/index.vue'),
|
||||||
hidden: true,
|
hidden: true,
|
||||||
meta: {
|
meta: {
|
||||||
|
requiresAuth: true,
|
||||||
key: 'Setting',
|
key: 'Setting',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -43,6 +46,7 @@ const settingRouter = {
|
|||||||
component: () => import('@/views/setting/monitor/index.vue'),
|
component: () => import('@/views/setting/monitor/index.vue'),
|
||||||
hidden: true,
|
hidden: true,
|
||||||
meta: {
|
meta: {
|
||||||
|
requiresAuth: true,
|
||||||
key: 'Setting',
|
key: 'Setting',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -52,6 +56,17 @@ const settingRouter = {
|
|||||||
component: () => import('@/views/setting/safe/index.vue'),
|
component: () => import('@/views/setting/safe/index.vue'),
|
||||||
hidden: true,
|
hidden: true,
|
||||||
meta: {
|
meta: {
|
||||||
|
requiresAuth: true,
|
||||||
|
key: 'Setting',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/setting/snapshot',
|
||||||
|
name: 'Snapshot',
|
||||||
|
hidden: true,
|
||||||
|
component: () => import('@/views/setting/snapshot/index.vue'),
|
||||||
|
meta: {
|
||||||
|
requiresAuth: true,
|
||||||
key: 'Setting',
|
key: 'Setting',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -11,6 +11,9 @@
|
|||||||
<el-radio-button class="topRouterButton" size="default" label="backupaccount">
|
<el-radio-button class="topRouterButton" size="default" label="backupaccount">
|
||||||
{{ $t('setting.backupAccount') }}
|
{{ $t('setting.backupAccount') }}
|
||||||
</el-radio-button>
|
</el-radio-button>
|
||||||
|
<el-radio-button class="topRouterButton" size="default" label="snapshot">
|
||||||
|
{{ $t('setting.snapshot') }}
|
||||||
|
</el-radio-button>
|
||||||
<el-radio-button class="topRouterButton" size="default" label="monitor">
|
<el-radio-button class="topRouterButton" size="default" label="monitor">
|
||||||
{{ $t('menu.monitor') }}
|
{{ $t('menu.monitor') }}
|
||||||
</el-radio-button>
|
</el-radio-button>
|
||||||
@ -53,6 +56,8 @@ const handleChange = (val: string) => {
|
|||||||
case 'about':
|
case 'about':
|
||||||
routerTo('/setting/about');
|
routerTo('/setting/about');
|
||||||
break;
|
break;
|
||||||
|
case 'snapshot':
|
||||||
|
routerTo('/setting/snapshot');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
167
frontend/src/views/setting/snapshot/index.vue
Normal file
167
frontend/src/views/setting/snapshot/index.vue
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<Submenu activeName="snapshot" />
|
||||||
|
<el-card style="margin-top: 20px">
|
||||||
|
<ComplexTable :pagination-config="paginationConfig" v-model:selects="selects" :data="data" @search="search">
|
||||||
|
<template #toolbar>
|
||||||
|
<el-button type="primary" icon="Plus" @click="onCreate()">
|
||||||
|
{{ $t('commons.button.create') }}
|
||||||
|
</el-button>
|
||||||
|
<!-- <el-button type="danger" plain :disabled="selects.length === 0" @click="batchDelete(null)">
|
||||||
|
{{ $t('commons.button.delete') }}
|
||||||
|
</el-button> -->
|
||||||
|
</template>
|
||||||
|
<el-table-column type="selection" fix />
|
||||||
|
<el-table-column :label="$t('commons.table.name')" min-width="100" prop="name" fix />
|
||||||
|
<el-table-column
|
||||||
|
:label="$t('commons.table.description')"
|
||||||
|
min-width="150"
|
||||||
|
show-overflow-tooltip
|
||||||
|
prop="description"
|
||||||
|
/>
|
||||||
|
<el-table-column :label="$t('setting.backupAccount')" min-width="150" prop="backupAccount" />
|
||||||
|
<el-table-column :label="$t('commons.table.status')" min-width="80" prop="status">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag v-if="row.status === 'Success'" type="success">
|
||||||
|
{{ $t('commons.table.statusSuccess') }}
|
||||||
|
</el-tag>
|
||||||
|
<el-tooltip v-else class="box-item" effect="dark" :content="row.message" placement="top-start">
|
||||||
|
<el-tag type="danger">{{ $t('commons.table.statusFailed') }}</el-tag>
|
||||||
|
</el-tooltip>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<fu-table-operations :buttons="buttons" :label="$t('commons.table.operate')" fix />
|
||||||
|
</ComplexTable>
|
||||||
|
</el-card>
|
||||||
|
<el-dialog v-model="dialogVisiable" :title="$t('commons.button.create')" width="30%">
|
||||||
|
<el-form ref="snapRef" label-width="100px" :model="snapInfo" :rules="rules">
|
||||||
|
<el-form-item :label="$t('cronjob.target')" prop="backupType">
|
||||||
|
<el-select v-model="snapInfo.backupType" clearable>
|
||||||
|
<el-option
|
||||||
|
v-for="item in backupOptions"
|
||||||
|
:key="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item :label="$t('commons.table.description')" prop="description">
|
||||||
|
<el-input type="textarea" clearable v-model="snapInfo.description" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="dialogVisiable = false">{{ $t('commons.button.cancel') }}</el-button>
|
||||||
|
<el-button type="primary" @click="submitAddSnapshot(snapRef)">
|
||||||
|
{{ $t('commons.button.confirm') }}
|
||||||
|
</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import ComplexTable from '@/components/complex-table/index.vue';
|
||||||
|
import { snapshotCreate, searchSnapshotPage } from '@/api/modules/setting';
|
||||||
|
import { onMounted, reactive, ref } from 'vue';
|
||||||
|
// import { useDeleteData } from '@/hooks/use-delete-data';
|
||||||
|
import type { ElForm } from 'element-plus';
|
||||||
|
import { Rules } from '@/global/form-rules';
|
||||||
|
import i18n from '@/lang';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import { Setting } from '@/api/interface/setting';
|
||||||
|
import Submenu from '@/views/setting/index.vue';
|
||||||
|
import { getBackupList } from '@/api/modules/backup';
|
||||||
|
import { loadBackupName } from '../helper';
|
||||||
|
|
||||||
|
const data = ref();
|
||||||
|
const selects = ref<any>([]);
|
||||||
|
const paginationConfig = reactive({
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const backupOptions = ref();
|
||||||
|
|
||||||
|
type FormInstance = InstanceType<typeof ElForm>;
|
||||||
|
const snapRef = ref<FormInstance>();
|
||||||
|
const rules = reactive({
|
||||||
|
backupType: [Rules.requiredSelect],
|
||||||
|
});
|
||||||
|
|
||||||
|
let snapInfo = reactive<Setting.SnapshotCreate>({
|
||||||
|
description: '',
|
||||||
|
backupType: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const dialogVisiable = ref<boolean>(false);
|
||||||
|
|
||||||
|
const onCreate = async () => {
|
||||||
|
restForm();
|
||||||
|
dialogVisiable.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitAddSnapshot = (formEl: FormInstance | undefined) => {
|
||||||
|
if (!formEl) return;
|
||||||
|
formEl.validate(async (valid) => {
|
||||||
|
if (!valid) return;
|
||||||
|
await snapshotCreate(snapInfo);
|
||||||
|
dialogVisiable.value = false;
|
||||||
|
search();
|
||||||
|
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadBackups = async () => {
|
||||||
|
const res = await getBackupList();
|
||||||
|
backupOptions.value = [];
|
||||||
|
for (const item of res.data) {
|
||||||
|
if (item.type !== 'LOCAL') {
|
||||||
|
backupOptions.value.push({ label: loadBackupName(item.type), value: item.type });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// const batchDelete = async (row: Snapshot.SnapshotInfo | null) => {
|
||||||
|
// let ids: Array<number> = [];
|
||||||
|
// if (row === null) {
|
||||||
|
// selects.value.forEach((item: Snapshot.SnapshotInfo) => {
|
||||||
|
// ids.push(item.id);
|
||||||
|
// });
|
||||||
|
// } else {
|
||||||
|
// ids.push(row.id);
|
||||||
|
// }
|
||||||
|
// await useDeleteData(deleteSnapshot, { ids: ids }, 'commons.msg.delete');
|
||||||
|
// search();
|
||||||
|
// };
|
||||||
|
|
||||||
|
function restForm() {
|
||||||
|
if (snapRef.value) {
|
||||||
|
snapRef.value.resetFields();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const buttons = [
|
||||||
|
// {
|
||||||
|
// label: i18n.global.t('commons.button.delete'),
|
||||||
|
// icon: 'Delete',
|
||||||
|
// click: batchDelete,
|
||||||
|
// },
|
||||||
|
];
|
||||||
|
|
||||||
|
const search = async () => {
|
||||||
|
let params = {
|
||||||
|
page: paginationConfig.currentPage,
|
||||||
|
pageSize: paginationConfig.pageSize,
|
||||||
|
};
|
||||||
|
const res = await searchSnapshotPage(params);
|
||||||
|
data.value = res.data.items || [];
|
||||||
|
paginationConfig.total = res.data.total;
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
search();
|
||||||
|
loadBackups();
|
||||||
|
});
|
||||||
|
</script>
|
Loading…
Reference in New Issue
Block a user