mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-01-19 08:19:15 +08:00
feat: 增加应用备份功能
This commit is contained in:
parent
7f4e8a25ea
commit
9f55523972
@ -5,7 +5,6 @@ system:
|
|||||||
data_dir: /opt/1Panel/data
|
data_dir: /opt/1Panel/data
|
||||||
resource_dir: /opt/1Panel/data/resource
|
resource_dir: /opt/1Panel/data/resource
|
||||||
app_dir: /opt/1Panel/data/apps
|
app_dir: /opt/1Panel/data/apps
|
||||||
app_oss:
|
|
||||||
|
|
||||||
mysql:
|
mysql:
|
||||||
path: localhost
|
path: localhost
|
||||||
|
@ -75,6 +75,21 @@ func (b *BaseApi) InstallApp(c *gin.Context) {
|
|||||||
|
|
||||||
helper.SuccessWithData(c, nil)
|
helper.SuccessWithData(c, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *BaseApi) DeleteAppBackup(c *gin.Context) {
|
||||||
|
var req dto.AppBackupDeleteRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := appService.DeleteBackup(req); err != nil {
|
||||||
|
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
helper.SuccessWithData(c, nil)
|
||||||
|
}
|
||||||
|
|
||||||
func (b *BaseApi) SearchInstalled(c *gin.Context) {
|
func (b *BaseApi) SearchInstalled(c *gin.Context) {
|
||||||
var req dto.AppInstalledRequest
|
var req dto.AppInstalledRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
@ -93,6 +108,24 @@ func (b *BaseApi) SearchInstalled(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *BaseApi) SearchInstalledBackup(c *gin.Context) {
|
||||||
|
var req dto.AppBackupRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
total, list, err := appService.PageInstallBackups(req)
|
||||||
|
if err != nil {
|
||||||
|
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
helper.SuccessWithData(c, dto.PageResult{
|
||||||
|
Items: list,
|
||||||
|
Total: total,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (b *BaseApi) OperateInstalled(c *gin.Context) {
|
func (b *BaseApi) OperateInstalled(c *gin.Context) {
|
||||||
var req dto.AppInstallOperate
|
var req dto.AppInstallOperate
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
@ -92,6 +92,15 @@ type AppInstalledRequest struct {
|
|||||||
PageInfo
|
PageInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AppBackupRequest struct {
|
||||||
|
PageInfo
|
||||||
|
AppInstallID uint `json:"appInstallID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AppBackupDeleteRequest struct {
|
||||||
|
Ids []uint `json:"ids"`
|
||||||
|
}
|
||||||
|
|
||||||
type AppOperate string
|
type AppOperate string
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -100,6 +109,7 @@ var (
|
|||||||
Restart AppOperate = "restart"
|
Restart AppOperate = "restart"
|
||||||
Delete AppOperate = "delete"
|
Delete AppOperate = "delete"
|
||||||
Sync AppOperate = "sync"
|
Sync AppOperate = "sync"
|
||||||
|
Backup AppOperate = "backup"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AppInstallOperate struct {
|
type AppInstallOperate struct {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/1Panel-dev/1Panel/global"
|
"github.com/1Panel-dev/1Panel/constant"
|
||||||
"path"
|
"path"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -19,16 +19,17 @@ type AppInstall struct {
|
|||||||
Message string `json:"message" gorm:"type:longtext;"`
|
Message string `json:"message" gorm:"type:longtext;"`
|
||||||
CanUpdate bool `json:"canUpdate"`
|
CanUpdate bool `json:"canUpdate"`
|
||||||
ContainerName string `json:"containerName" gorm:"type:varchar(256);not null"`
|
ContainerName string `json:"containerName" gorm:"type:varchar(256);not null"`
|
||||||
ServiceName string `json:"ServiceName" gorm:"type:varchar(256);not null"`
|
ServiceName string `json:"serviceName" gorm:"type:varchar(256);not null"`
|
||||||
HttpPort int `json:"httpPort" gorm:"type:integer;not null"`
|
HttpPort int `json:"httpPort" gorm:"type:integer;not null"`
|
||||||
HttpsPort int `json:"httpsPort" gorm:"type:integer;not null"`
|
HttpsPort int `json:"httpsPort" gorm:"type:integer;not null"`
|
||||||
App App `json:"-"`
|
App App `json:"app"`
|
||||||
|
Backups []AppInstallBackup `json:"backups"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *AppInstall) GetPath() string {
|
func (i *AppInstall) GetPath() string {
|
||||||
return path.Join(global.CONF.System.AppDir, i.App.Key, i.Name)
|
return path.Join(constant.AppInstallDir, i.App.Key, i.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *AppInstall) GetComposePath() string {
|
func (i *AppInstall) GetComposePath() string {
|
||||||
return path.Join(global.CONF.System.AppDir, i.App.Key, i.Name, "docker-compose.yml")
|
return path.Join(constant.AppInstallDir, i.App.Key, i.Name, "docker-compose.yml")
|
||||||
}
|
}
|
||||||
|
9
backend/app/model/app_install_backup.go
Normal file
9
backend/app/model/app_install_backup.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
type AppInstallBackup struct {
|
||||||
|
BaseModel
|
||||||
|
Name string `gorm:"type:varchar(64);not null" json:"name"`
|
||||||
|
Path string `gorm:"type:varchar(64);not null" json:"path"`
|
||||||
|
AppDetailId uint `gorm:"type:varchar(64);not null" json:"app_detail_id"`
|
||||||
|
AppInstallId uint `gorm:"type:integer;not null" json:"app_install_id"`
|
||||||
|
}
|
@ -40,14 +40,14 @@ func (a AppInstallRepo) WithServiceName(serviceName string) DBOption {
|
|||||||
func (a AppInstallRepo) GetBy(opts ...DBOption) ([]model.AppInstall, error) {
|
func (a AppInstallRepo) GetBy(opts ...DBOption) ([]model.AppInstall, error) {
|
||||||
var install []model.AppInstall
|
var install []model.AppInstall
|
||||||
db := getDb(opts...).Model(&model.AppInstall{})
|
db := getDb(opts...).Model(&model.AppInstall{})
|
||||||
err := db.Preload("App").Find(&install).Error
|
err := db.Preload("App").Preload("Backups").Find(&install).Error
|
||||||
return install, err
|
return install, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a AppInstallRepo) GetFirst(opts ...DBOption) (model.AppInstall, error) {
|
func (a AppInstallRepo) GetFirst(opts ...DBOption) (model.AppInstall, error) {
|
||||||
var install model.AppInstall
|
var install model.AppInstall
|
||||||
db := getDb(opts...).Model(&model.AppInstall{})
|
db := getDb(opts...).Model(&model.AppInstall{})
|
||||||
err := db.Preload("App").First(&install).Error
|
err := db.Preload("App").Preload("Backups").First(&install).Error
|
||||||
return install, err
|
return install, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,7 +74,7 @@ func (a AppInstallRepo) Page(page, size int, opts ...DBOption) (int64, []model.A
|
|||||||
db := getDb(opts...).Model(&model.AppInstall{})
|
db := getDb(opts...).Model(&model.AppInstall{})
|
||||||
count := int64(0)
|
count := int64(0)
|
||||||
db = db.Count(&count)
|
db = db.Count(&count)
|
||||||
err := db.Debug().Limit(size).Offset(size * (page - 1)).Preload("App").Find(&apps).Error
|
err := db.Debug().Limit(size).Offset(size * (page - 1)).Preload("App").Preload("Backups").Find(&apps).Error
|
||||||
return count, apps, err
|
return count, apps, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
41
backend/app/repo/app_install_backup.go
Normal file
41
backend/app/repo/app_install_backup.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/1Panel-dev/1Panel/app/model"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AppInstallBackupRepo struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a AppInstallBackupRepo) WithAppInstallID(appInstallID uint) DBOption {
|
||||||
|
return func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Where("app_install_id = ?", appInstallID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a AppInstallBackupRepo) Create(ctx context.Context, backup model.AppInstallBackup) error {
|
||||||
|
return getTx(ctx).Omit(clause.Associations).Create(&backup).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a AppInstallBackupRepo) Delete(opts ...DBOption) error {
|
||||||
|
return getDb(opts...).Omit(clause.Associations).Delete(&model.AppInstallBackup{}).Error
|
||||||
|
}
|
||||||
|
func (a AppInstallBackupRepo) GetBy(opts ...DBOption) ([]model.AppInstallBackup, error) {
|
||||||
|
var backups []model.AppInstallBackup
|
||||||
|
if err := getDb(opts...).Find(&backups); err != nil {
|
||||||
|
return backups, nil
|
||||||
|
}
|
||||||
|
return backups, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a AppInstallBackupRepo) Page(page, size int, opts ...DBOption) (int64, []model.AppInstallBackup, error) {
|
||||||
|
var backups []model.AppInstallBackup
|
||||||
|
db := getDb(opts...).Model(&model.AppInstallBackup{})
|
||||||
|
count := int64(0)
|
||||||
|
db = db.Count(&count)
|
||||||
|
err := db.Debug().Limit(size).Offset(size * (page - 1)).Find(&backups).Error
|
||||||
|
return count, backups, err
|
||||||
|
}
|
@ -16,6 +16,7 @@ type RepoGroup struct {
|
|||||||
AppInstallRepo
|
AppInstallRepo
|
||||||
AppInstallResourceRpo
|
AppInstallResourceRpo
|
||||||
DatabaseRepo
|
DatabaseRepo
|
||||||
|
AppInstallBackupRepo
|
||||||
}
|
}
|
||||||
|
|
||||||
var RepoGroupApp = new(RepoGroup)
|
var RepoGroupApp = new(RepoGroup)
|
||||||
|
@ -117,8 +117,6 @@ func (a AppService) PageInstalled(req dto.AppInstalledRequest) (int64, []dto.App
|
|||||||
for _, in := range installed {
|
for _, in := range installed {
|
||||||
installDto := dto.AppInstalled{
|
installDto := dto.AppInstalled{
|
||||||
AppInstall: in,
|
AppInstall: in,
|
||||||
AppName: in.App.Name,
|
|
||||||
Icon: in.App.Icon,
|
|
||||||
}
|
}
|
||||||
installDTOs = append(installDTOs, installDto)
|
installDTOs = append(installDTOs, installDto)
|
||||||
}
|
}
|
||||||
@ -145,6 +143,10 @@ func (a AppService) GetAppDetail(appId uint, version string) (dto.AppDetailDTO,
|
|||||||
return appDetailDTO, nil
|
return appDetailDTO, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a AppService) PageInstallBackups(req dto.AppBackupRequest) (int64, []model.AppInstallBackup, error) {
|
||||||
|
return appInstallBackupRepo.Page(req.Page, req.PageSize, appInstallBackupRepo.WithAppInstallID(req.AppInstallID))
|
||||||
|
}
|
||||||
|
|
||||||
func (a AppService) OperateInstall(req dto.AppInstallOperate) error {
|
func (a AppService) OperateInstall(req dto.AppInstallOperate) error {
|
||||||
install, err := appInstallRepo.GetFirst(commonRepo.WithByID(req.InstallId))
|
install, err := appInstallRepo.GetFirst(commonRepo.WithByID(req.InstallId))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -186,9 +188,7 @@ func (a AppService) OperateInstall(req dto.AppInstallOperate) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tx := global.DB.Begin()
|
tx, ctx := getTxAndContext()
|
||||||
ctx := context.WithValue(context.Background(), "db", tx)
|
|
||||||
|
|
||||||
if err := appInstallRepo.Delete(ctx, install); err != nil {
|
if err := appInstallRepo.Delete(ctx, install); err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
return err
|
return err
|
||||||
@ -204,6 +204,13 @@ func (a AppService) OperateInstall(req dto.AppInstallOperate) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
case dto.Backup:
|
||||||
|
tx, ctx := getTxAndContext()
|
||||||
|
if err := backupInstall(ctx, install); err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
}
|
||||||
|
tx.Commit()
|
||||||
|
return nil
|
||||||
default:
|
default:
|
||||||
return errors.New("operate not support")
|
return errors.New("operate not support")
|
||||||
}
|
}
|
||||||
@ -284,8 +291,7 @@ func (a AppService) Install(name string, appDetailId uint, params map[string]int
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
tx := global.DB.Begin()
|
tx, ctx := getTxAndContext()
|
||||||
ctx := context.WithValue(context.Background(), "db", tx)
|
|
||||||
if err := appInstallRepo.Create(ctx, &appInstall); err != nil {
|
if err := appInstallRepo.Create(ctx, &appInstall); err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
return err
|
return err
|
||||||
@ -314,6 +320,24 @@ func (a AppService) SyncAllInstalled() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a AppService) DeleteBackup(req dto.AppBackupDeleteRequest) error {
|
||||||
|
|
||||||
|
backups, err := appInstallBackupRepo.GetBy(commonRepo.WithIdsIn(req.Ids))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fileOp := files.NewFileOp()
|
||||||
|
|
||||||
|
for _, backup := range backups {
|
||||||
|
dst := path.Join(backup.Path, backup.Name)
|
||||||
|
if err := fileOp.DeleteFile(dst); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
appInstallBackupRepo.Delete(commonRepo.WithIdsIn(req.Ids))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (a AppService) GetServices(key string) ([]dto.AppService, error) {
|
func (a AppService) GetServices(key string) ([]dto.AppService, error) {
|
||||||
app, err := appRepo.GetFirst(appRepo.WithKey(key))
|
app, err := appRepo.GetFirst(appRepo.WithKey(key))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -425,7 +449,7 @@ func (a AppService) SyncInstalled(installId uint) error {
|
|||||||
func (a AppService) SyncAppList() error {
|
func (a AppService) SyncAppList() error {
|
||||||
//TODO 从 oss 拉取最新列表
|
//TODO 从 oss 拉取最新列表
|
||||||
|
|
||||||
appDir := path.Join(global.CONF.System.ResourceDir, "apps")
|
appDir := constant.AppResourceDir
|
||||||
iconDir := path.Join(appDir, "icons")
|
iconDir := path.Join(appDir, "icons")
|
||||||
listFile := path.Join(appDir, "list.json")
|
listFile := path.Join(appDir, "list.json")
|
||||||
|
|
||||||
|
@ -7,7 +7,6 @@ import (
|
|||||||
"github.com/1Panel-dev/1Panel/app/dto"
|
"github.com/1Panel-dev/1Panel/app/dto"
|
||||||
"github.com/1Panel-dev/1Panel/app/model"
|
"github.com/1Panel-dev/1Panel/app/model"
|
||||||
"github.com/1Panel-dev/1Panel/constant"
|
"github.com/1Panel-dev/1Panel/constant"
|
||||||
"github.com/1Panel-dev/1Panel/global"
|
|
||||||
"github.com/1Panel-dev/1Panel/utils/cmd"
|
"github.com/1Panel-dev/1Panel/utils/cmd"
|
||||||
"github.com/1Panel-dev/1Panel/utils/common"
|
"github.com/1Panel-dev/1Panel/utils/common"
|
||||||
"github.com/1Panel-dev/1Panel/utils/compose"
|
"github.com/1Panel-dev/1Panel/utils/compose"
|
||||||
@ -18,6 +17,7 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type DatabaseOp string
|
type DatabaseOp string
|
||||||
@ -52,8 +52,8 @@ func getSqlStr(key string, operate DatabaseOp, exec dto.ContainerExec) string {
|
|||||||
switch key {
|
switch key {
|
||||||
case "mysql":
|
case "mysql":
|
||||||
if operate == Add {
|
if operate == Add {
|
||||||
str = fmt.Sprintf("docker exec -i %s mysql -uroot -p%s -e \"CREATE USER '%s'@'%%' IDENTIFIED BY '%s';\" -e \"create database %s;\" -e \"GRANT ALL ON %s.* TO '%s'@'%%';\"",
|
str = fmt.Sprintf("docker exec -i %s mysql -uroot -p%s -e \"CREATE USER '%s'@'%%' IDENTIFIED BY '%s';\" -e \"create database %s;\" -e \"GRANT ALL ON %s.* TO '%s'@'%%' IDENTIFIED BY '%s';\"",
|
||||||
exec.ContainerName, exec.Auth.RootPassword, param.DbUser, param.Password, param.DbName, param.DbName, param.DbUser)
|
exec.ContainerName, exec.Auth.RootPassword, param.DbUser, param.Password, param.DbName, param.DbName, param.DbUser, param.Password)
|
||||||
}
|
}
|
||||||
if operate == Delete {
|
if operate == Delete {
|
||||||
str = fmt.Sprintf("docker exec -i %s mysql -uroot -p%s -e \"drop database %s;\" -e \"drop user %s;\" ",
|
str = fmt.Sprintf("docker exec -i %s mysql -uroot -p%s -e \"drop database %s;\" -e \"drop user %s;\" ",
|
||||||
@ -160,6 +160,28 @@ func deleteLink(ctx context.Context, install *model.AppInstall) error {
|
|||||||
return appInstallResourceRepo.DeleteBy(ctx, appInstallResourceRepo.WithAppInstallId(install.ID))
|
return appInstallResourceRepo.DeleteBy(ctx, appInstallResourceRepo.WithAppInstallId(install.ID))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func backupInstall(ctx context.Context, install model.AppInstall) error {
|
||||||
|
var backup model.AppInstallBackup
|
||||||
|
appPath := install.GetPath()
|
||||||
|
backupDir := path.Join(constant.BackupDir, install.App.Key, install.Name)
|
||||||
|
fileOp := files.NewFileOp()
|
||||||
|
now := time.Now()
|
||||||
|
day := now.Format("2006-01-02-15-04")
|
||||||
|
fileName := fmt.Sprintf("%s-%s-%s%s", install.Name, install.Version, day, ".tar.gz")
|
||||||
|
if err := fileOp.Compress([]string{appPath}, backupDir, fileName, files.TarGz); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
backup.Name = fileName
|
||||||
|
backup.Path = backupDir
|
||||||
|
backup.AppInstallId = install.ID
|
||||||
|
backup.AppDetailId = install.AppDetailId
|
||||||
|
|
||||||
|
if err := appInstallBackupRepo.Create(ctx, backup); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func getContainerNames(install model.AppInstall) ([]string, error) {
|
func getContainerNames(install model.AppInstall) ([]string, error) {
|
||||||
composeMap := install.DockerCompose
|
composeMap := install.DockerCompose
|
||||||
envMap := make(map[string]string)
|
envMap := make(map[string]string)
|
||||||
@ -220,8 +242,8 @@ func checkRequiredAndLimit(app model.App) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func copyAppData(key, version, installName string, params map[string]interface{}) (err error) {
|
func copyAppData(key, version, installName string, params map[string]interface{}) (err error) {
|
||||||
resourceDir := path.Join(global.CONF.System.ResourceDir, "apps", key, version)
|
resourceDir := path.Join(constant.AppResourceDir, key, version)
|
||||||
installDir := path.Join(global.CONF.System.AppDir, key)
|
installDir := path.Join(constant.AppInstallDir, key)
|
||||||
installVersionDir := path.Join(installDir, version)
|
installVersionDir := path.Join(installDir, version)
|
||||||
fileOp := files.NewFileOp()
|
fileOp := files.NewFileOp()
|
||||||
if err = fileOp.Copy(resourceDir, installVersionDir); err != nil {
|
if err = fileOp.Copy(resourceDir, installVersionDir); err != nil {
|
||||||
|
@ -33,4 +33,5 @@ var (
|
|||||||
appInstallRepo = repo.RepoGroupApp.AppInstallRepo
|
appInstallRepo = repo.RepoGroupApp.AppInstallRepo
|
||||||
appInstallResourceRepo = repo.RepoGroupApp.AppInstallResourceRpo
|
appInstallResourceRepo = repo.RepoGroupApp.AppInstallResourceRpo
|
||||||
dataBaseRepo = repo.RepoGroupApp.DatabaseRepo
|
dataBaseRepo = repo.RepoGroupApp.DatabaseRepo
|
||||||
|
appInstallBackupRepo = repo.RepoGroupApp.AppInstallBackupRepo
|
||||||
)
|
)
|
||||||
|
13
backend/app/service/helper.go
Normal file
13
backend/app/service/helper.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/1Panel-dev/1Panel/global"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getTxAndContext() (tx *gorm.DB, ctx context.Context) {
|
||||||
|
tx = global.DB.Begin()
|
||||||
|
ctx = context.WithValue(context.Background(), "db", tx)
|
||||||
|
return
|
||||||
|
}
|
@ -5,7 +5,5 @@ type System struct {
|
|||||||
DbType string `mapstructure:"db_type"`
|
DbType string `mapstructure:"db_type"`
|
||||||
Level string `mapstructure:"level"`
|
Level string `mapstructure:"level"`
|
||||||
DataDir string `mapstructure:"data_dir"`
|
DataDir string `mapstructure:"data_dir"`
|
||||||
ResourceDir string `mapstructure:"resource_dir"`
|
|
||||||
AppDir string `mapstructure:"app_dir"`
|
|
||||||
AppOss string `mapstructure:"app_oss"`
|
AppOss string `mapstructure:"app_oss"`
|
||||||
}
|
}
|
||||||
|
14
backend/constant/dir.go
Normal file
14
backend/constant/dir.go
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
package constant
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
DefaultDataDir = "/opt/1Panel/data"
|
||||||
|
ResourceDir = path.Join(DefaultDataDir, "resource")
|
||||||
|
AppResourceDir = path.Join(ResourceDir, "apps")
|
||||||
|
AppInstallDir = path.Join(DefaultDataDir, "apps")
|
||||||
|
BackupDir = path.Join(DefaultDataDir, "backup")
|
||||||
|
AppBackupDir = path.Join(BackupDir, "apps")
|
||||||
|
)
|
@ -150,6 +150,6 @@ var AddTableCronjob = &gormigrate.Migration{
|
|||||||
var AddTableApp = &gormigrate.Migration{
|
var AddTableApp = &gormigrate.Migration{
|
||||||
ID: "20200921-add-table-app",
|
ID: "20200921-add-table-app",
|
||||||
Migrate: func(tx *gorm.DB) error {
|
Migrate: func(tx *gorm.DB) error {
|
||||||
return tx.AutoMigrate(&model.App{}, &model.AppDetail{}, &model.Tag{}, &model.AppTag{}, &model.AppInstall{}, &model.AppInstallResource{}, &model.Database{})
|
return tx.AutoMigrate(&model.App{}, &model.AppDetail{}, &model.Tag{}, &model.AppTag{}, &model.AppInstall{}, &model.AppInstallResource{}, &model.Database{}, &model.AppInstallBackup{})
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -2,9 +2,10 @@ package viper
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/configs"
|
"github.com/1Panel-dev/1Panel/configs"
|
||||||
|
"github.com/1Panel-dev/1Panel/constant"
|
||||||
"github.com/1Panel-dev/1Panel/global"
|
"github.com/1Panel-dev/1Panel/global"
|
||||||
|
"path"
|
||||||
|
|
||||||
"github.com/fsnotify/fsnotify"
|
"github.com/fsnotify/fsnotify"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
@ -30,4 +31,16 @@ func Init() {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
global.CONF = serverConfig
|
global.CONF = serverConfig
|
||||||
|
InitDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitDir() {
|
||||||
|
constant.DefaultDataDir = "/opt/1Panel/data"
|
||||||
|
constant.ResourceDir = path.Join(constant.DefaultDataDir, "resource")
|
||||||
|
constant.AppResourceDir = path.Join(constant.ResourceDir, "apps")
|
||||||
|
constant.AppInstallDir = path.Join(constant.DefaultDataDir, "apps")
|
||||||
|
constant.BackupDir = path.Join(constant.DefaultDataDir, "backup")
|
||||||
|
constant.AppBackupDir = path.Join(constant.BackupDir, "apps")
|
||||||
|
|
||||||
|
//TODO 创建文件夹
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,8 @@ func (a *AppRouter) InitAppRouter(Router *gin.RouterGroup) {
|
|||||||
appRouter.POST("/installed", baseApi.SearchInstalled)
|
appRouter.POST("/installed", baseApi.SearchInstalled)
|
||||||
appRouter.POST("/installed/op", baseApi.OperateInstalled)
|
appRouter.POST("/installed/op", baseApi.OperateInstalled)
|
||||||
appRouter.POST("/installed/sync", baseApi.SyncInstalled)
|
appRouter.POST("/installed/sync", baseApi.SyncInstalled)
|
||||||
|
appRouter.POST("/installed/backups", baseApi.SearchInstalledBackup)
|
||||||
|
appRouter.POST("/installed/backups/del", baseApi.DeleteAppBackup)
|
||||||
appRouter.GET("/services/:key", baseApi.GetServices)
|
appRouter.GET("/services/:key", baseApi.GetServices)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -341,6 +341,10 @@ func (f FileOp) Compress(srcRiles []string, dst string, name string, cType Compr
|
|||||||
fileMaps[s] = base
|
fileMaps[s] = base
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !f.Stat(dst) {
|
||||||
|
_ = f.CreateDir(dst, 0755)
|
||||||
|
}
|
||||||
|
|
||||||
files, err := archiver.FilesFromDisk(nil, fileMaps)
|
files, err := archiver.FilesFromDisk(nil, fileMaps)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -66,12 +66,12 @@ export namespace App {
|
|||||||
name: string;
|
name: string;
|
||||||
appId: string;
|
appId: string;
|
||||||
appDetailId: string;
|
appDetailId: string;
|
||||||
params: string;
|
env: string;
|
||||||
status: string;
|
status: string;
|
||||||
description: string;
|
description: string;
|
||||||
message: string;
|
message: string;
|
||||||
appName: string;
|
|
||||||
icon: string;
|
icon: string;
|
||||||
|
canUpdate: boolean;
|
||||||
app: App;
|
app: App;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,4 +84,19 @@ export namespace App {
|
|||||||
label: string;
|
label: string;
|
||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AppBackupReq extends ReqPage {
|
||||||
|
appInstallId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppBackupDelReq {
|
||||||
|
ids: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppBackup extends CommonModel {
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
appInstallId: string;
|
||||||
|
appDetail: AppDetail;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,3 +37,11 @@ export const SyncInstalledApp = () => {
|
|||||||
export const GetAppService = (key: string | undefined) => {
|
export const GetAppService = (key: string | undefined) => {
|
||||||
return http.get<any>(`apps/services/${key}`);
|
return http.get<any>(`apps/services/${key}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const GetAppBackups = (info: App.AppBackupReq) => {
|
||||||
|
return http.post<ResPage<App.AppBackup>>('apps/installed/backups', info);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DelAppBackups = (req: App.AppBackupDelReq) => {
|
||||||
|
return http.post<any>('apps/installed/backups/del', req);
|
||||||
|
};
|
||||||
|
@ -418,5 +418,9 @@ export default {
|
|||||||
deleteWarn:
|
deleteWarn:
|
||||||
'Delete the operation data and delete the operation data. This operation cannot be rolled back. Do you want to continue?',
|
'Delete the operation data and delete the operation data. This operation cannot be rolled back. Do you want to continue?',
|
||||||
canUpdate: 'CanUpdate',
|
canUpdate: 'CanUpdate',
|
||||||
|
backup: 'Backup',
|
||||||
|
backupName: 'Filename',
|
||||||
|
backupPath: 'Filepath',
|
||||||
|
backupdate: 'Backup Date',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -410,5 +410,9 @@ export default {
|
|||||||
deleteWarn: '删除操作会把数据一并删除,此操作不可回滚,是否继续?',
|
deleteWarn: '删除操作会把数据一并删除,此操作不可回滚,是否继续?',
|
||||||
syncSuccess: '同步成功',
|
syncSuccess: '同步成功',
|
||||||
canUpdate: '可更新',
|
canUpdate: '可更新',
|
||||||
|
backup: '备份',
|
||||||
|
backupName: '文件名称',
|
||||||
|
backupPath: '文件路径',
|
||||||
|
backupdate: '备份时间',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
109
frontend/src/views/app-store/installed/backups.vue
Normal file
109
frontend/src/views/app-store/installed/backups.vue
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog v-model="open" :title="$t('app.backup')" width="70%" :before-close="handleClose">
|
||||||
|
<ComplexTable :pagination-config="paginationConfig" :data="data" @search="search" v-loading="loading">
|
||||||
|
<template #toolbar>
|
||||||
|
<el-button type="primary" plain @click="backup">{{ $t('app.backup') }}</el-button>
|
||||||
|
</template>
|
||||||
|
<el-table-column :label="$t('app.backupName')" prop="name"></el-table-column>
|
||||||
|
<el-table-column :label="$t('app.backupPath')" prop="path"></el-table-column>
|
||||||
|
<el-table-column
|
||||||
|
prop="createdAt"
|
||||||
|
:label="$t('app.backupdate')"
|
||||||
|
:formatter="dateFromat"
|
||||||
|
show-overflow-tooltip
|
||||||
|
/>
|
||||||
|
<fu-table-operations
|
||||||
|
width="300px"
|
||||||
|
:ellipsis="10"
|
||||||
|
:buttons="buttons"
|
||||||
|
:label="$t('commons.table.operate')"
|
||||||
|
fixed="right"
|
||||||
|
fix
|
||||||
|
/>
|
||||||
|
</ComplexTable>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup name="installBackup">
|
||||||
|
import { DelAppBackups, GetAppBackups, InstalledOp } from '@/api/modules/app';
|
||||||
|
import { reactive, ref } from 'vue';
|
||||||
|
import ComplexTable from '@/components/complex-table/index.vue';
|
||||||
|
import { dateFromat } from '@/utils/util';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import i18n from '@/lang';
|
||||||
|
import { useDeleteData } from '@/hooks/use-delete-data';
|
||||||
|
|
||||||
|
interface InstallRrops {
|
||||||
|
appInstallId: number;
|
||||||
|
}
|
||||||
|
const installData = ref<InstallRrops>({
|
||||||
|
appInstallId: 0,
|
||||||
|
});
|
||||||
|
let open = ref(false);
|
||||||
|
let loading = ref(false);
|
||||||
|
let data = ref<any>();
|
||||||
|
const paginationConfig = reactive({
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
total: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
open.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const acceptParams = (props: InstallRrops) => {
|
||||||
|
installData.value.appInstallId = props.appInstallId;
|
||||||
|
search();
|
||||||
|
open.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const search = async () => {
|
||||||
|
const req = {
|
||||||
|
page: paginationConfig.currentPage,
|
||||||
|
pageSize: paginationConfig.pageSize,
|
||||||
|
appInstallId: installData.value.appInstallId,
|
||||||
|
};
|
||||||
|
await GetAppBackups(req).then((res) => {
|
||||||
|
data.value = res.data.items;
|
||||||
|
paginationConfig.total = res.data.total;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const backup = async () => {
|
||||||
|
const req = {
|
||||||
|
installId: installData.value.appInstallId,
|
||||||
|
operate: 'backup',
|
||||||
|
};
|
||||||
|
loading.value = true;
|
||||||
|
await InstalledOp(req)
|
||||||
|
.then(() => {
|
||||||
|
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
|
||||||
|
search();
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
loading.value = false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteBackup = async (ids: number[]) => {
|
||||||
|
const req = {
|
||||||
|
ids: ids,
|
||||||
|
};
|
||||||
|
await useDeleteData(DelAppBackups, req, 'commons.msg.delete', loading.value);
|
||||||
|
search();
|
||||||
|
};
|
||||||
|
|
||||||
|
const buttons = [
|
||||||
|
{
|
||||||
|
label: i18n.global.t('app.delete'),
|
||||||
|
click: (row: any) => {
|
||||||
|
deleteBackup([row.id]);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
acceptParams,
|
||||||
|
});
|
||||||
|
</script>
|
@ -10,8 +10,15 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<!-- <el-table-column :label="$t('app.description')" prop="description"></el-table-column> -->
|
<!-- <el-table-column :label="$t('app.description')" prop="description"></el-table-column> -->
|
||||||
<el-table-column :label="$t('app.appName')" prop="appName"></el-table-column>
|
<el-table-column :label="$t('app.appName')" prop="app.name"></el-table-column>
|
||||||
<el-table-column :label="$t('app.version')" prop="version"></el-table-column>
|
<el-table-column :label="$t('app.version')" prop="version"></el-table-column>
|
||||||
|
<el-table-column :label="$t('app.backup')">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-link :underline="false" @click="openBackups(row.id)" type="primary">
|
||||||
|
{{ $t('app.backup') }} ({{ row.backups.length }})
|
||||||
|
</el-link>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column :label="$t('app.status')">
|
<el-table-column :label="$t('app.status')">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-popover
|
<el-popover
|
||||||
@ -35,7 +42,7 @@
|
|||||||
show-overflow-tooltip
|
show-overflow-tooltip
|
||||||
/>
|
/>
|
||||||
<fu-table-operations
|
<fu-table-operations
|
||||||
width="250px"
|
width="300px"
|
||||||
:ellipsis="10"
|
:ellipsis="10"
|
||||||
:buttons="buttons"
|
:buttons="buttons"
|
||||||
:label="$t('commons.table.operate')"
|
:label="$t('commons.table.operate')"
|
||||||
@ -52,6 +59,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
<Backups ref="backupRef"></Backups>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
@ -61,6 +69,7 @@ import ComplexTable from '@/components/complex-table/index.vue';
|
|||||||
import { dateFromat } from '@/utils/util';
|
import { dateFromat } from '@/utils/util';
|
||||||
import i18n from '@/lang';
|
import i18n from '@/lang';
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
|
import Backups from './backups.vue';
|
||||||
|
|
||||||
let data = ref<any>();
|
let data = ref<any>();
|
||||||
let loading = ref(false);
|
let loading = ref(false);
|
||||||
@ -74,6 +83,7 @@ let operateReq = reactive({
|
|||||||
installId: 0,
|
installId: 0,
|
||||||
operate: '',
|
operate: '',
|
||||||
});
|
});
|
||||||
|
const backupRef = ref();
|
||||||
|
|
||||||
const sync = () => {
|
const sync = () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
@ -178,6 +188,13 @@ const buttons = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const openBackups = (installId: number) => {
|
||||||
|
let params = {
|
||||||
|
appInstallId: installId,
|
||||||
|
};
|
||||||
|
backupRef.value.acceptParams(params);
|
||||||
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
search();
|
search();
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user