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 @@
-
+