diff --git a/apps/mysql/5.7.39/params.json b/apps/mysql/5.7.39/config.json similarity index 100% rename from apps/mysql/5.7.39/params.json rename to apps/mysql/5.7.39/config.json diff --git a/apps/mysql/8.0.30/params.json b/apps/mysql/8.0.30/config.json similarity index 100% rename from apps/mysql/8.0.30/params.json rename to apps/mysql/8.0.30/config.json diff --git a/apps/nginx/1.23.1/params.json b/apps/nginx/1.23.1/config.json similarity index 100% rename from apps/nginx/1.23.1/params.json rename to apps/nginx/1.23.1/config.json diff --git a/apps/wordpress/6.0.1/params.json b/apps/wordpress/6.0.1/config.json similarity index 100% rename from apps/wordpress/6.0.1/params.json rename to apps/wordpress/6.0.1/config.json diff --git a/apps/wordpress/6.0.2/params.json b/apps/wordpress/6.0.2/config.json similarity index 100% rename from apps/wordpress/6.0.2/params.json rename to apps/wordpress/6.0.2/config.json diff --git a/backend/app/dto/app.go b/backend/app/dto/app.go index 0925a0547..cbfdf735d 100644 --- a/backend/app/dto/app.go +++ b/backend/app/dto/app.go @@ -110,10 +110,12 @@ var ( Delete AppOperate = "delete" Sync AppOperate = "sync" Backup AppOperate = "backup" + Restore AppOperate = "restore" ) type AppInstallOperate struct { InstallId uint `json:"installId" validate:"required"` + BackupId uint `json:"backupId"` Operate AppOperate `json:"operate" validate:"required"` } diff --git a/backend/app/model/app_install_backup.go b/backend/app/model/app_install_backup.go index 07302dea2..1b254729e 100644 --- a/backend/app/model/app_install_backup.go +++ b/backend/app/model/app_install_backup.go @@ -4,6 +4,7 @@ type AppInstallBackup struct { BaseModel Name string `gorm:"type:varchar(64);not null" json:"name"` Path string `gorm:"type:varchar(64);not null" json:"path"` + Param string `json:"param" gorm:"type:longtext;"` AppDetailId uint `gorm:"type:varchar(64);not null" json:"app_detail_id"` AppInstallId uint `gorm:"type:integer;not null" json:"app_install_id"` } diff --git a/backend/app/repo/app_install.go b/backend/app/repo/app_install.go index fefff2475..3eb97d02c 100644 --- a/backend/app/repo/app_install.go +++ b/backend/app/repo/app_install.go @@ -56,7 +56,7 @@ func (a AppInstallRepo) Create(ctx context.Context, install *model.AppInstall) e return db.Create(&install).Error } -func (a AppInstallRepo) Save(install model.AppInstall) error { +func (a AppInstallRepo) Save(install *model.AppInstall) error { return getDb().Save(&install).Error } diff --git a/backend/app/repo/app_install_backup.go b/backend/app/repo/app_install_backup.go index 7142dd376..04eed1f7a 100644 --- a/backend/app/repo/app_install_backup.go +++ b/backend/app/repo/app_install_backup.go @@ -31,6 +31,13 @@ func (a AppInstallBackupRepo) GetBy(opts ...DBOption) ([]model.AppInstallBackup, return backups, nil } +func (a AppInstallBackupRepo) GetFirst(opts ...DBOption) (model.AppInstallBackup, error) { + var backup model.AppInstallBackup + db := getDb(opts...).Model(&model.AppInstallBackup{}) + err := db.First(&backup).Error + return backup, err +} + func (a AppInstallBackupRepo) Page(page, size int, opts ...DBOption) (int64, []model.AppInstallBackup, error) { var backups []model.AppInstallBackup db := getDb(opts...).Model(&model.AppInstallBackup{}) diff --git a/backend/app/repo/common.go b/backend/app/repo/common.go index 853ba3346..d432e560e 100644 --- a/backend/app/repo/common.go +++ b/backend/app/repo/common.go @@ -70,6 +70,9 @@ func (c *CommonRepo) WithIdsNotIn(ids []uint) DBOption { } func getTx(ctx context.Context, opts ...DBOption) *gorm.DB { + if ctx == nil { + return getDb() + } tx := ctx.Value("db").(*gorm.DB) for _, opt := range opts { tx = opt(tx) diff --git a/backend/app/service/app.go b/backend/app/service/app.go index 42c71ea2d..76f080b53 100644 --- a/backend/app/service/app.go +++ b/backend/app/service/app.go @@ -208,14 +208,21 @@ func (a AppService) OperateInstall(req dto.AppInstallOperate) error { tx, ctx := getTxAndContext() if err := backupInstall(ctx, install); err != nil { tx.Rollback() + return err } tx.Commit() return nil + case dto.Restore: + installBackup, err := appInstallBackupRepo.GetFirst(commonRepo.WithByID(req.BackupId)) + if err != nil { + return err + } + return restoreInstall(install, installBackup) default: return errors.New("operate not support") } - return appInstallRepo.Save(install) + return appInstallRepo.Save(&install) } func (a AppService) Install(name string, appDetailId uint, params map[string]interface{}) error { @@ -328,12 +335,19 @@ func (a AppService) DeleteBackup(req dto.AppBackupDeleteRequest) error { } fileOp := files.NewFileOp() + var errStr strings.Builder for _, backup := range backups { dst := path.Join(backup.Path, backup.Name) if err := fileOp.DeleteFile(dst); err != nil { + errStr.WriteString(err.Error()) continue } - appInstallBackupRepo.Delete(commonRepo.WithIdsIn(req.Ids)) + if err := appInstallBackupRepo.Delete(commonRepo.WithIdsIn(req.Ids)); err != nil { + errStr.WriteString(err.Error()) + } + } + if errStr.String() != "" { + return errors.New(errStr.String()) } return nil } @@ -411,11 +425,11 @@ func (a AppService) SyncInstalled(installId uint) error { if containerCount == 0 { appInstall.Status = constant.Error appInstall.Message = "container is not found" - return appInstallRepo.Save(appInstall) + return appInstallRepo.Save(&appInstall) } if errCount == 0 && notFoundCount == 0 { appInstall.Status = constant.Running - return appInstallRepo.Save(appInstall) + return appInstallRepo.Save(&appInstall) } if errCount == normalCount { appInstall.Status = constant.Error @@ -443,7 +457,7 @@ func (a AppService) SyncInstalled(installId uint) error { errMsg.Write([]byte("\n")) } appInstall.Message = errMsg.String() - return appInstallRepo.Save(appInstall) + return appInstallRepo.Save(&appInstall) } func (a AppService) SyncAppList() error { @@ -513,7 +527,7 @@ func (a AppService) SyncAppList() error { continue } detail.DockerCompose = string(dockerComposeStr) - paramStr, err := os.ReadFile(path.Join(detailPath, "params.json")) + paramStr, err := os.ReadFile(path.Join(detailPath, "config.json")) if err != nil { global.LOG.Errorf("get [%s] form.json error: %s", detailPath, err.Error()) } diff --git a/backend/app/service/app_utils.go b/backend/app/service/app_utils.go index 9ac011cea..6a03d4464 100644 --- a/backend/app/service/app_utils.go +++ b/backend/app/service/app_utils.go @@ -13,7 +13,9 @@ import ( "github.com/1Panel-dev/1Panel/utils/files" "github.com/joho/godotenv" "github.com/pkg/errors" + "gopkg.in/yaml.v3" "math" + "os" "path" "reflect" "strconv" @@ -175,6 +177,7 @@ func backupInstall(ctx context.Context, install model.AppInstall) error { backup.Path = backupDir backup.AppInstallId = install.ID backup.AppDetailId = install.AppDetailId + backup.Param = install.Param if err := appInstallBackupRepo.Create(ctx, backup); err != nil { return err @@ -182,6 +185,76 @@ func backupInstall(ctx context.Context, install model.AppInstall) error { return nil } +func restoreInstall(install model.AppInstall, backup model.AppInstallBackup) error { + if _, err := compose.Down(install.GetComposePath()); err != nil { + return err + } + installKeyDir := path.Join(constant.AppInstallDir, install.App.Key) + installDir := path.Join(installKeyDir, install.Name) + backupFile := path.Join(backup.Path, backup.Name) + fileOp := files.NewFileOp() + if !fileOp.Stat(backupFile) { + return errors.New(fmt.Sprintf("%s file is not exist", backup.Name)) + } + backDir := installDir + "_back" + if fileOp.Stat(backDir) { + if err := fileOp.DeleteDir(backDir); err != nil { + return err + } + } + if err := fileOp.Rename(installDir, backDir); err != nil { + return err + } + if err := fileOp.Decompress(backupFile, installKeyDir, files.TarGz); err != nil { + return err + } + composeContent, err := os.ReadFile(install.GetComposePath()) + if err != nil { + return err + } + install.DockerCompose = string(composeContent) + envContent, err := os.ReadFile(path.Join(installDir, ".env")) + if err != nil { + return err + } + install.Env = string(envContent) + envMaps, err := godotenv.Unmarshal(string(envContent)) + if err != nil { + return err + } + install.HttpPort = 0 + httpPort, ok := envMaps["PANEL_APP_PORT_HTTP"] + if ok { + httpPortN, _ := strconv.Atoi(httpPort) + install.HttpPort = httpPortN + } + install.HttpsPort = 0 + httpsPort, ok := envMaps["PANEL_APP_PORT_HTTPS"] + if ok { + httpsPortN, _ := strconv.Atoi(httpsPort) + install.HttpsPort = httpsPortN + } + + composeMap := make(map[string]interface{}) + if err := yaml.Unmarshal(composeContent, &composeMap); err != nil { + return err + } + servicesMap := composeMap["services"].(map[string]interface{}) + for k, v := range servicesMap { + install.ServiceName = k + value := v.(map[string]interface{}) + install.ContainerName = value["container_name"].(string) + } + + install.Param = backup.Param + _ = fileOp.DeleteDir(backDir) + if out, err := compose.Up(install.GetComposePath()); err != nil { + return handleErr(install, err, out) + } + install.Status = constant.Running + return appInstallRepo.Save(&install) +} + func getContainerNames(install model.AppInstall) ([]string, error) { composeMap := install.DockerCompose envMap := make(map[string]string) @@ -241,6 +314,19 @@ func checkRequiredAndLimit(app model.App) error { return nil } +func handleMap(params map[string]interface{}, envParams map[string]string) { + for k, v := range params { + switch t := v.(type) { + case string: + envParams[k] = t + case float64: + envParams[k] = strconv.FormatFloat(t, 'f', -1, 32) + default: + envParams[k] = t.(string) + } + } +} + func copyAppData(key, version, installName string, params map[string]interface{}) (err error) { resourceDir := path.Join(constant.AppResourceDir, key, version) installDir := path.Join(constant.AppInstallDir, key) @@ -256,16 +342,7 @@ func copyAppData(key, version, installName string, params map[string]interface{} envPath := path.Join(appDir, ".env") envParams := make(map[string]string, len(params)) - for k, v := range params { - switch t := v.(type) { - case string: - envParams[k] = t - case float64: - envParams[k] = strconv.FormatFloat(t, 'f', -1, 32) - default: - envParams[k] = t.(string) - } - } + handleMap(params, envParams) if err = godotenv.Write(envParams, envPath); err != nil { return } @@ -281,10 +358,10 @@ func upApp(composeFilePath string, appInstall model.AppInstall) { appInstall.Message = err.Error() } appInstall.Status = constant.Error - _ = appInstallRepo.Save(appInstall) + _ = appInstallRepo.Save(&appInstall) } else { appInstall.Status = constant.Running - _ = appInstallRepo.Save(appInstall) + _ = appInstallRepo.Save(&appInstall) } } @@ -342,6 +419,6 @@ func handleErr(install model.AppInstall, err error, out string) error { install.Message = out reErr = errors.New(out) } - _ = appInstallRepo.Save(install) + _ = appInstallRepo.Save(&install) return reErr } diff --git a/frontend/src/api/interface/app.ts b/frontend/src/api/interface/app.ts index 752c1bdaa..4ab63bb60 100644 --- a/frontend/src/api/interface/app.ts +++ b/frontend/src/api/interface/app.ts @@ -78,6 +78,7 @@ export namespace App { export interface AppInstalledOp { installId: number; operate: string; + backupId?: number; } export interface AppService { diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index 0de8daf48..94197a87c 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -422,5 +422,8 @@ export default { backupName: 'Filename', backupPath: 'Filepath', backupdate: 'Backup Date', + restore: 'Restore', + restoreWarn: + 'The rollback operation will restart the application and replace the data. This operation cannot be rolled back. Do you want to continue?', }, }; diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index d96d634af..4208cac58 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -414,5 +414,7 @@ export default { backupName: '文件名称', backupPath: '文件路径', backupdate: '备份时间', + restore: '回滚', + restoreWarn: '回滚操作会重启应用,并替换数据,此操作不可回滚,是否继续?', }, }; diff --git a/frontend/src/views/app-store/installed/backups.vue b/frontend/src/views/app-store/installed/backups.vue index 87d1caecc..a8eecb23f 100644 --- a/frontend/src/views/app-store/installed/backups.vue +++ b/frontend/src/views/app-store/installed/backups.vue @@ -21,6 +21,19 @@ fix /> + + + + @@ -42,14 +55,22 @@ const installData = ref({ let open = ref(false); let loading = ref(false); let data = ref(); +let openRestorePage = ref(false); const paginationConfig = reactive({ currentPage: 1, pageSize: 20, total: 0, }); +let req = reactive({ + installId: installData.value.appInstallId, + operate: 'restore', + backupId: -1, +}); +const em = defineEmits(['close']); const handleClose = () => { open.value = false; + em('close', open); }; const acceptParams = (props: InstallRrops) => { @@ -86,6 +107,26 @@ const backup = async () => { }); }; +const openRestore = (backupId: number) => { + openRestorePage.value = true; + req.backupId = backupId; + req.operate = 'restore'; + req.installId = installData.value.appInstallId; +}; + +const restore = async () => { + loading.value = true; + await InstalledOp(req) + .then(() => { + ElMessage.success(i18n.global.t('commons.msg.operationSuccess')); + openRestorePage.value = false; + search(); + }) + .finally(() => { + loading.value = false; + }); +}; + const deleteBackup = async (ids: number[]) => { const req = { ids: ids, @@ -101,6 +142,12 @@ const buttons = [ deleteBackup([row.id]); }, }, + { + label: i18n.global.t('app.restore'), + click: (row: any) => { + openRestore(row.id); + }, + }, ]; defineExpose({ diff --git a/frontend/src/views/app-store/installed/index.vue b/frontend/src/views/app-store/installed/index.vue index c77d07ee6..4f8f9bf35 100644 --- a/frontend/src/views/app-store/installed/index.vue +++ b/frontend/src/views/app-store/installed/index.vue @@ -59,7 +59,7 @@ - +