diff --git a/backend/app/api/v1/website.go b/backend/app/api/v1/website.go index 581fab6bc..8d3b52743 100644 --- a/backend/app/api/v1/website.go +++ b/backend/app/api/v1/website.go @@ -4,6 +4,7 @@ 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" ) @@ -51,6 +52,24 @@ func (b *BaseApi) BackupWebsite(c *gin.Context) { helper.SuccessWithData(c, nil) } +func (b *BaseApi) RecoverWebsite(c *gin.Context) { + var req dto.WebSiteRecover + 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 := websiteService.Recover(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + func (b *BaseApi) DeleteWebSite(c *gin.Context) { var req dto.WebSiteDel if err := c.ShouldBindJSON(&req); err != nil { diff --git a/backend/app/dto/website.go b/backend/app/dto/website.go index c3e842172..6dc38a9ce 100644 --- a/backend/app/dto/website.go +++ b/backend/app/dto/website.go @@ -51,6 +51,12 @@ type WebSiteDel struct { DeleteBackup bool `json:"deleteBackup"` } +type WebSiteRecover struct { + WebsiteName string `json:"websiteName" validate:"required"` + Type string `json:"type" validate:"required"` + BackupName string `json:"backupName" validate:"required"` +} + type WebSiteDTO struct { model.WebSite } diff --git a/backend/app/repo/website.go b/backend/app/repo/website.go index 7c64700d5..730c2888a 100644 --- a/backend/app/repo/website.go +++ b/backend/app/repo/website.go @@ -2,6 +2,7 @@ package repo import ( "context" + "github.com/1Panel-dev/1Panel/backend/app/model" "gorm.io/gorm" "gorm.io/gorm/clause" @@ -16,6 +17,12 @@ func (w WebSiteRepo) WithAppInstallId(appInstallId uint) DBOption { } } +func (w WebSiteRepo) WithByDomain(domain string) DBOption { + return func(db *gorm.DB) *gorm.DB { + return db.Where("primary_domain = ?", domain) + } +} + func (w WebSiteRepo) Page(page, size int, opts ...DBOption) (int64, []model.WebSite, error) { var websites []model.WebSite db := getDb(opts...).Model(&model.WebSite{}) diff --git a/backend/app/service/website.go b/backend/app/service/website.go index a98de45ef..5fa6fb619 100644 --- a/backend/app/service/website.go +++ b/backend/app/service/website.go @@ -17,6 +17,7 @@ import ( "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/compose" "github.com/1Panel-dev/1Panel/backend/utils/files" "github.com/pkg/errors" "gorm.io/gorm" @@ -185,6 +186,7 @@ func (w WebsiteService) Backup(id uint) error { if err != nil { return errors.New(string(stdout)) } + _ = os.RemoveAll(fullDir) record := &model.BackupRecord{ Type: "website-" + website.Type, @@ -201,6 +203,93 @@ func (w WebsiteService) Backup(id uint) error { return nil } +func (w WebsiteService) Recover(req dto.WebSiteRecover) error { + website, err := websiteRepo.GetFirst(websiteRepo.WithByDomain(req.WebsiteName)) + if err != nil { + return err + } + app, err := appInstallRepo.GetFirst(commonRepo.WithByID(website.AppInstallID)) + if err != nil { + return err + } + if !strings.Contains(req.BackupName, "/") { + return errors.New("error path of request") + } + fileDir := req.BackupName[:strings.LastIndex(req.BackupName, "/")] + fileName := strings.ReplaceAll(req.BackupName[strings.LastIndex(req.BackupName, "/"):], ".tar.gz", "") + if err := handleUnTar(req.BackupName, fileDir); err != nil { + return err + } + fileDir = fileDir + "/" + fileName + resource, err := appInstallResourceRepo.GetFirst(appInstallResourceRepo.WithAppInstallId(website.AppInstallID)) + if err != nil { + return err + } + nginxInfo, err := appInstallRepo.LoadBaseInfoByKey("nginx") + if err != nil { + return err + } + mysqlInfo, err := appInstallRepo.LoadBaseInfoByKey("mysql") + if err != nil { + return err + } + db, err := mysqlRepo.Get(commonRepo.WithByID(resource.ResourceId)) + if err != nil { + return err + } + src, err := os.OpenFile(fmt.Sprintf("%s/%s.conf", fileDir, website.PrimaryDomain), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0775) + if err != nil { + return err + } + defer src.Close() + var out *os.File + nginxConfDir := fmt.Sprintf("%s/nginx/%s/conf/conf.d/%s.conf", constant.AppInstallDir, nginxInfo.Name, website.PrimaryDomain) + if _, err := os.Stat(nginxConfDir); err != nil { + out, err = os.Create(fmt.Sprintf("%s/%s.conf", nginxConfDir, website.PrimaryDomain)) + if err != nil { + return err + } + } else { + out, err = os.OpenFile(nginxConfDir, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) + if err != nil { + return err + } + } + defer out.Close() + _, _ = io.Copy(out, src) + if website.Type == "deployment" { + cmd := exec.Command("docker", "exec", "-i", mysqlInfo.ContainerName, "mysql", "-uroot", "-p"+mysqlInfo.Password, db.Name) + sql, err := os.OpenFile(fmt.Sprintf("%s/%s.sql", fileDir, website.PrimaryDomain), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) + if err != nil { + return err + } + cmd.Stdin = sql + stdout, err := cmd.CombinedOutput() + if err != nil { + return errors.New(string(stdout)) + } + + appDir := fmt.Sprintf("%s/%s", constant.AppInstallDir, app.App.Key) + if err := handleUnTar(fmt.Sprintf("%s/%s.web.tar.gz", fileDir, website.PrimaryDomain), appDir); err != nil { + return err + } + if _, err := compose.Restart(fmt.Sprintf("%s/%s/docker-compose.yml", appDir, app.Name)); err != nil { + return err + } + } else { + appDir := fmt.Sprintf("%s/nginx/%s/www", constant.AppInstallDir, nginxInfo.Name) + if err := handleUnTar(fmt.Sprintf("%s/%s.web.tar.gz", fileDir, website.PrimaryDomain), appDir); err != nil { + return err + } + } + cmd := exec.Command("docker", "exec", "-i", nginxInfo.ContainerName, "nginx", "-s", "reload") + stdout, err := cmd.CombinedOutput() + if err != nil { + return errors.New(string(stdout)) + } + return nil +} + func (w WebsiteService) UpdateWebsite(req dto.WebSiteUpdate) error { website, err := websiteRepo.GetFirst(commonRepo.WithByID(req.ID)) if err != nil { diff --git a/backend/router/ro_website.go b/backend/router/ro_website.go index 5f94025fd..2025f2b0e 100644 --- a/backend/router/ro_website.go +++ b/backend/router/ro_website.go @@ -17,6 +17,7 @@ func (a *WebsiteRouter) InitWebsiteRouter(Router *gin.RouterGroup) { { groupRouter.POST("", baseApi.CreateWebsite) groupRouter.POST("/backup/:id", baseApi.BackupWebsite) + groupRouter.POST("/recover", baseApi.RecoverWebsite) groupRouter.POST("/update", baseApi.UpdateWebSite) groupRouter.GET("/:id", baseApi.GetWebSite) groupRouter.GET("/:id/nginx", baseApi.GetWebSiteNginx) diff --git a/frontend/src/api/interface/website.ts b/frontend/src/api/interface/website.ts index cdd5c0fea..b767fd333 100644 --- a/frontend/src/api/interface/website.ts +++ b/frontend/src/api/interface/website.ts @@ -38,6 +38,12 @@ export namespace WebSite { name: string; } + export interface WebSiteRecover { + websiteName: string; + type: string; + backupName: string; + } + export interface WebSiteDel { id: number; deleteApp: boolean; diff --git a/frontend/src/api/modules/website.ts b/frontend/src/api/modules/website.ts index b16989edc..98b20f33c 100644 --- a/frontend/src/api/modules/website.ts +++ b/frontend/src/api/modules/website.ts @@ -14,6 +14,9 @@ export const CreateWebsite = (req: WebSite.WebSiteCreateReq) => { export const BackupWebsite = (id: number) => { return http.post(`/websites/backup/${id}`); }; +export const RecoverWebsite = (req: WebSite.WebSiteRecover) => { + return http.post(`/websites/recover`, req); +}; export const UpdateWebsite = (req: WebSite.WebSiteUpdateReq) => { return http.post(`/websites/update`, req); diff --git a/frontend/src/views/website/website/backup/index.vue b/frontend/src/views/website/website/backup/index.vue index 42433f04a..91d4bfe31 100644 --- a/frontend/src/views/website/website/backup/index.vue +++ b/frontend/src/views/website/website/backup/index.vue @@ -40,7 +40,7 @@ import i18n from '@/lang'; import { ElMessage } from 'element-plus'; import { deleteBackupRecord, downloadBackupRecord, searchBackupRecords } from '@/api/modules/backup'; import { Backup } from '@/api/interface/backup'; -import { BackupWebsite } from '@/api/modules/website'; +import { BackupWebsite, RecoverWebsite } from '@/api/modules/website'; const selects = ref([]); @@ -82,6 +82,16 @@ const search = async () => { paginationConfig.total = res.data.total; }; +const onRecover = async (row: Backup.RecordInfo) => { + let params = { + websiteName: websiteName.value, + type: websiteType.value, + backupName: row.fileDir + '/' + row.fileName, + }; + await RecoverWebsite(params); + ElMessage.success(i18n.global.t('commons.msg.operationSuccess')); +}; + const onBackup = async () => { await BackupWebsite(websiteID.value); ElMessage.success(i18n.global.t('commons.msg.operationSuccess')); @@ -124,12 +134,12 @@ const buttons = [ onBatchDelete(row); }, }, - // { - // label: i18n.global.t('commons.button.recover'), - // click: (row: Backup.RecordInfo) => { - // onRecover(row); - // }, - // }, + { + label: i18n.global.t('commons.button.recover'), + click: (row: Backup.RecordInfo) => { + onRecover(row); + }, + }, { label: i18n.global.t('commons.button.download'), click: (row: Backup.RecordInfo) => {