diff --git a/backend/app/api/v1/website.go b/backend/app/api/v1/website.go index 8d3b52743..98bb6a431 100644 --- a/backend/app/api/v1/website.go +++ b/backend/app/api/v1/website.go @@ -1,6 +1,8 @@ package v1 import ( + "errors" + "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" @@ -25,6 +27,15 @@ func (b *BaseApi) PageWebsite(c *gin.Context) { }) } +func (b *BaseApi) GetWebsiteOptions(c *gin.Context) { + websites, err := websiteService.GetWebsiteOptions() + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, websites) +} + func (b *BaseApi) CreateWebsite(c *gin.Context) { var req dto.WebSiteCreate if err := c.ShouldBindJSON(&req); err != nil { @@ -40,12 +51,12 @@ func (b *BaseApi) CreateWebsite(c *gin.Context) { } func (b *BaseApi) BackupWebsite(c *gin.Context) { - id, err := helper.GetParamID(c) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + domain, ok := c.Params.Get("domain") + if !ok { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, errors.New("error domain in path")) return } - if err := websiteService.Backup(id); err != nil { + if err := websiteService.Backup(domain); err != nil { helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) return } diff --git a/backend/app/service/cornjob.go b/backend/app/service/cornjob.go index 257f9a9a5..1451df3ea 100644 --- a/backend/app/service/cornjob.go +++ b/backend/app/service/cornjob.go @@ -132,12 +132,21 @@ func (u *CronjobService) Download(down dto.CronjobDownload) (string, error) { if _, ok := varMap["dir"]; !ok { return "", errors.New("load local backup dir failed") } - dir := fmt.Sprintf("%v/%s/%s/", varMap["dir"], cronjob.Type, cronjob.Name) - name := fmt.Sprintf("%s%s.tar.gz", dir, record.StartTime.Format("20060102150405")) - if cronjob.Type == "database" { - name = fmt.Sprintf("%s%s.gz", dir, record.StartTime.Format("20060102150405")) + + switch cronjob.Type { + case "website": + return fmt.Sprintf("%v/website/%s/website_%s_%s.tar.gz", varMap["dir"], cronjob.Website, cronjob.Website, record.StartTime.Format("20060102150405")), nil + case "database": + mysqlInfo, err := appInstallRepo.LoadBaseInfoByKey("mysql") + if err != nil { + return "", fmt.Errorf("load mysqlInfo failed, err: %v", err) + } + return fmt.Sprintf("%v/database/mysql/%s/%s/db_%s_%s.sql.gz", varMap["dir"], mysqlInfo.Name, cronjob.DBName, cronjob.DBName, record.StartTime.Format("20060102150405")), nil + case "directory": + return fmt.Sprintf("%v/%s/%s/%s.tar.gz", varMap["dir"], cronjob.Type, cronjob.Name, record.StartTime.Format("20060102150405")), nil + default: + return "", fmt.Errorf("not support type %s", cronjob.Type) } - return name, nil } func (u *CronjobService) HandleOnce(id uint) error { diff --git a/backend/app/service/cronjob_helper.go b/backend/app/service/cronjob_helper.go index 5fde1330a..7dd588b7b 100644 --- a/backend/app/service/cronjob_helper.go +++ b/backend/app/service/cronjob_helper.go @@ -88,18 +88,25 @@ func (u *CronjobService) HandleBackup(cronjob *model.Cronjob, startTime time.Tim baseDir = constant.TmpDir } - if cronjob.Type == "database" { + switch cronjob.Type { + case "database": app, err := appInstallRepo.LoadBaseInfoByKey("mysql") if err != nil { return "", err } - fileName = fmt.Sprintf("db_%s_%s.sql.gz", cronjob.DBName, time.Now().Format("20060102150405")) + fileName = fmt.Sprintf("db_%s_%s.sql.gz", cronjob.DBName, startTime.Format("20060102150405")) backupDir = fmt.Sprintf("database/mysql/%s/%s", app.Name, cronjob.DBName) - err = backupMysql(backup.Type, baseDir, backupDir, app.Name, cronjob.DBName, fileName) - if err != nil { + if err = backupMysql(backup.Type, baseDir, backupDir, app.Name, cronjob.DBName, fileName); err != nil { return "", err } - } else { + case "website": + fileName = fmt.Sprintf("website_%s_%s", cronjob.Website, startTime.Format("20060102150405")) + backupDir = fmt.Sprintf("website/%s", cronjob.Website) + if err := handleWebsiteBackup(backup.Type, baseDir, backupDir, cronjob.Website, fileName); err != nil { + return "", err + } + fileName = fileName + ".tar.gz" + default: fileName = fmt.Sprintf("%s.tar.gz", startTime.Format("20060102150405")) backupDir = fmt.Sprintf("%s/%s", cronjob.Type, cronjob.Name) if err := handleTar(cronjob.SourceDir, baseDir+"/"+backupDir, fileName, cronjob.ExclusionRules); err != nil { diff --git a/backend/app/service/website.go b/backend/app/service/website.go index 5fa6fb619..aa795aece 100644 --- a/backend/app/service/website.go +++ b/backend/app/service/website.go @@ -26,6 +26,20 @@ import ( type WebsiteService struct { } +type IWebsiteService interface { + PageWebSite(req dto.WebSiteReq) (int64, []dto.WebSiteDTO, error) + CreateWebsite(create dto.WebSiteCreate) error + GetWebsiteOptions() ([]string, error) + Backup(domain string) error + Recover(req dto.WebSiteRecover) error + UpdateWebsite(req dto.WebSiteUpdate) error + DeleteWebSite(req dto.WebSiteDel) error +} + +func NewWebsiteService() IWebsiteService { + return &WebsiteService{} +} + func (w WebsiteService) PageWebSite(req dto.WebSiteReq) (int64, []dto.WebSiteDTO, error) { var websiteDTOs []dto.WebSiteDTO total, websites, err := websiteRepo.Page(req.Page, req.PageSize) @@ -107,99 +121,29 @@ func (w WebsiteService) CreateWebsite(create dto.WebSiteCreate) error { return nil } -func (w WebsiteService) Backup(id uint) error { - website, err := websiteRepo.GetFirst(commonRepo.WithByID(id)) +func (w WebsiteService) GetWebsiteOptions() ([]string, error) { + webs, err := websiteRepo.GetBy() if err != nil { - return err + return nil, err } - app, err := appInstallRepo.GetFirst(commonRepo.WithByID(website.AppInstallID)) - if err != nil { - return err - } - resource, err := appInstallResourceRepo.GetFirst(appInstallResourceRepo.WithAppInstallId(website.AppInstallID)) - if err != nil { - return err - } - mysqlInfo, err := appInstallRepo.LoadBaseInfoByKey(resource.Key) - if err != nil { - return err - } - nginxInfo, err := appInstallRepo.LoadBaseInfoByKey("nginx") - if err != nil { - return err - } - db, err := mysqlRepo.Get(commonRepo.WithByID(resource.ResourceId)) - if err != nil { - return err + var datas []string + for _, web := range webs { + datas = append(datas, web.PrimaryDomain) } + return datas, nil +} + +func (w WebsiteService) Backup(domain string) error { localDir, err := loadLocalDir() if err != nil { return err } - name := fmt.Sprintf("%s_%s", website.PrimaryDomain, time.Now().Format("20060102150405")) - backupDir := fmt.Sprintf("website/%s/%s", website.PrimaryDomain, name) - fullDir := fmt.Sprintf("%s/%s", localDir, backupDir) - if _, err := os.Stat(fullDir); err != nil && os.IsNotExist(err) { - if err = os.MkdirAll(fullDir, os.ModePerm); err != nil { - if err != nil { - return fmt.Errorf("mkdir %s failed, err: %v", fullDir, err) - } - } - } - nginxConfFile := fmt.Sprintf("%s/nginx/%s/conf/conf.d/%s.conf", constant.AppInstallDir, nginxInfo.Name, website.PrimaryDomain) - src, err := os.OpenFile(nginxConfFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0775) - if err != nil { + fileName := fmt.Sprintf("%s_%s", domain, time.Now().Format("20060102150405")) + backupDir := fmt.Sprintf("website/%s", domain) + + if err := handleWebsiteBackup("LOCAL", localDir, backupDir, domain, fileName); err != nil { return err } - defer src.Close() - out, err := os.Create(fmt.Sprintf("%s/%s.conf", fullDir, website.PrimaryDomain)) - if err != nil { - return err - } - defer out.Close() - _, _ = io.Copy(out, src) - - if website.Type == "deployment" { - dbFile := fmt.Sprintf("%s/%s.sql", fullDir, website.PrimaryDomain) - outfile, _ := os.OpenFile(dbFile, os.O_RDWR|os.O_CREATE, 0755) - cmd := exec.Command("docker", "exec", mysqlInfo.ContainerName, "mysqldump", "-uroot", "-p"+mysqlInfo.Password, db.Name) - cmd.Stdout = outfile - _ = cmd.Run() - _ = cmd.Wait() - - websiteDir := fmt.Sprintf("%s/%s/%s", constant.AppInstallDir, app.App.Key, app.Name) - if err := handleTar(websiteDir, fullDir, fmt.Sprintf("%s.web.tar.gz", website.PrimaryDomain), ""); err != nil { - return err - } - } else { - websiteDir := fmt.Sprintf("%s/nginx/%s/www/%s", constant.AppInstallDir, nginxInfo.Name, website.PrimaryDomain) - if err := handleTar(websiteDir, fullDir, fmt.Sprintf("%s.web.tar.gz", website.PrimaryDomain), ""); err != nil { - return err - } - } - tarDir := fmt.Sprintf("%s/website/%s", localDir, website.PrimaryDomain) - tarName := fmt.Sprintf("%s.tar.gz", name) - itemDir := strings.ReplaceAll(fullDir[strings.LastIndex(fullDir, "/"):], "/", "") - aheadDir := strings.ReplaceAll(fullDir, itemDir, "") - tarcmd := exec.Command("tar", "zcvf", fullDir+".tar.gz", "-C", aheadDir, itemDir) - stdout, err := tarcmd.CombinedOutput() - if err != nil { - return errors.New(string(stdout)) - } - _ = os.RemoveAll(fullDir) - - record := &model.BackupRecord{ - Type: "website-" + website.Type, - Name: website.PrimaryDomain, - DetailName: "", - Source: "LOCAL", - BackupType: "LOCAL", - FileDir: tarDir, - FileName: tarName, - } - if err := backupRepo.CreateRecord(record); err != nil { - global.LOG.Errorf("save backup record failed, err: %v", err) - } return nil } @@ -577,3 +521,97 @@ func (w WebsiteService) OpWebsiteHTTPS(req dto.WebsiteHTTPSOp) (dto.WebsiteHTTPS tx.Commit() return res, nil } + +func handleWebsiteBackup(backupType, baseDir, backupDir, domain, backupName string) error { + website, err := websiteRepo.GetFirst(websiteRepo.WithByDomain(domain)) + if err != nil { + return err + } + app, err := appInstallRepo.GetFirst(commonRepo.WithByID(website.AppInstallID)) + if err != nil { + return err + } + resource, err := appInstallResourceRepo.GetFirst(appInstallResourceRepo.WithAppInstallId(website.AppInstallID)) + if err != nil { + return err + } + mysqlInfo, err := appInstallRepo.LoadBaseInfoByKey(resource.Key) + if err != nil { + return err + } + nginxInfo, err := appInstallRepo.LoadBaseInfoByKey("nginx") + if err != nil { + return err + } + db, err := mysqlRepo.Get(commonRepo.WithByID(resource.ResourceId)) + if err != nil { + return err + } + + tmpDir := fmt.Sprintf("%s/%s/%s", baseDir, backupDir, backupName) + if _, err := os.Stat(tmpDir); err != nil && os.IsNotExist(err) { + if err = os.MkdirAll(tmpDir, os.ModePerm); err != nil { + if err != nil { + return fmt.Errorf("mkdir %s failed, err: %v", tmpDir, err) + } + } + } + nginxConfFile := fmt.Sprintf("%s/nginx/%s/conf/conf.d/%s.conf", constant.AppInstallDir, nginxInfo.Name, website.PrimaryDomain) + src, err := os.OpenFile(nginxConfFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0775) + if err != nil { + return err + } + defer src.Close() + out, err := os.Create(fmt.Sprintf("%s/%s.conf", tmpDir, website.PrimaryDomain)) + if err != nil { + return err + } + defer out.Close() + _, _ = io.Copy(out, src) + + if website.Type == "deployment" { + dbFile := fmt.Sprintf("%s/%s.sql", tmpDir, website.PrimaryDomain) + outfile, _ := os.OpenFile(dbFile, os.O_RDWR|os.O_CREATE, 0755) + cmd := exec.Command("docker", "exec", mysqlInfo.ContainerName, "mysqldump", "-uroot", "-p"+mysqlInfo.Password, db.Name) + cmd.Stdout = outfile + _ = cmd.Run() + _ = cmd.Wait() + + websiteDir := fmt.Sprintf("%s/%s/%s", constant.AppInstallDir, app.App.Key, app.Name) + if err := handleTar(websiteDir, tmpDir, fmt.Sprintf("%s.web.tar.gz", website.PrimaryDomain), ""); err != nil { + return err + } + } else { + websiteDir := fmt.Sprintf("%s/nginx/%s/www/%s", constant.AppInstallDir, nginxInfo.Name, website.PrimaryDomain) + if err := handleTar(websiteDir, tmpDir, fmt.Sprintf("%s.web.tar.gz", website.PrimaryDomain), ""); err != nil { + return err + } + } + + itemDir := strings.ReplaceAll(tmpDir[strings.LastIndex(tmpDir, "/"):], "/", "") + aheadDir := strings.ReplaceAll(tmpDir, itemDir, "") + tarcmd := exec.Command("tar", "zcvf", tmpDir+".tar.gz", "-C", aheadDir, itemDir) + stdout, err := tarcmd.CombinedOutput() + if err != nil { + return errors.New(string(stdout)) + } + _ = os.RemoveAll(tmpDir) + + record := &model.BackupRecord{ + Type: "website-" + website.Type, + Name: website.PrimaryDomain, + DetailName: "", + Source: backupType, + BackupType: backupType, + FileDir: backupDir, + FileName: fmt.Sprintf("%s.tar.gz", backupName), + } + if baseDir != constant.TmpDir || backupType == "LOCAL" { + record.Source = "LOCAL" + record.FileDir = fmt.Sprintf("%s/%s", baseDir, backupDir) + } + if err := backupRepo.CreateRecord(record); err != nil { + global.LOG.Errorf("save backup record failed, err: %v", err) + } + return nil +} diff --git a/backend/router/ro_database.go b/backend/router/ro_database.go index 110375692..506bd2395 100644 --- a/backend/router/ro_database.go +++ b/backend/router/ro_database.go @@ -35,7 +35,7 @@ func (s *DatabaseRouter) InitDatabaseRouter(Router *gin.RouterGroup) { cmdRouter.GET("/variables", baseApi.LoadVariables) cmdRouter.GET("/status", baseApi.LoadStatus) cmdRouter.GET("/baseinfo", baseApi.LoadBaseinfo) - cmdRouter.GET("/dbs", baseApi.ListDBName) + cmdRouter.GET("/options", baseApi.ListDBName) cmdRouter.GET("/redis/persistence/conf", baseApi.LoadPersistenceConf) cmdRouter.GET("/redis/status", baseApi.LoadRedisStatus) diff --git a/backend/router/ro_website.go b/backend/router/ro_website.go index 2025f2b0e..108af1b0c 100644 --- a/backend/router/ro_website.go +++ b/backend/router/ro_website.go @@ -16,7 +16,8 @@ func (a *WebsiteRouter) InitWebsiteRouter(Router *gin.RouterGroup) { baseApi := v1.ApiGroupApp.BaseApi { groupRouter.POST("", baseApi.CreateWebsite) - groupRouter.POST("/backup/:id", baseApi.BackupWebsite) + groupRouter.GET("/options", baseApi.GetWebsiteOptions) + groupRouter.POST("/backup/:domain", baseApi.BackupWebsite) groupRouter.POST("/recover", baseApi.RecoverWebsite) groupRouter.POST("/update", baseApi.UpdateWebSite) groupRouter.GET("/:id", baseApi.GetWebSite) diff --git a/frontend/src/api/modules/database.ts b/frontend/src/api/modules/database.ts index c741aa95c..fab012758 100644 --- a/frontend/src/api/modules/database.ts +++ b/frontend/src/api/modules/database.ts @@ -49,7 +49,7 @@ export const loadMysqlStatus = () => { return http.get(`/databases/status`); }; export const loadDBNames = () => { - return http.get>(`/databases/dbs`); + return http.get>(`/databases/options`); }; // redis diff --git a/frontend/src/api/modules/website.ts b/frontend/src/api/modules/website.ts index 98b20f33c..825d09351 100644 --- a/frontend/src/api/modules/website.ts +++ b/frontend/src/api/modules/website.ts @@ -26,6 +26,10 @@ export const GetWebsite = (id: number) => { return http.get(`/websites/${id}`); }; +export const GetWebsiteOptions = () => { + return http.get>(`/websites/options`); +}; + export const GetWebsiteNginx = (id: number) => { return http.get(`/websites/${id}/nginx`); }; diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index 87320d6ad..6d10fba28 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -443,6 +443,7 @@ export default { directory: 'Backup directory', sourceDir: 'Backup directory', exclusionRules: 'Exclusive rule', + saveLocal: 'Retain local backups (the same as the number of cloud storage copies)', url: 'URL Address', target: 'Target', retainCopies: 'Retain copies', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index d2ef532cb..a5a49e521 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -457,6 +457,7 @@ export default { directory: '备份目录', sourceDir: '备份目录', exclusionRules: '排除规则', + saveLocal: '同时保留本地备份(和云存储保留份数一致)', url: 'URL 地址', target: '备份到', retainCopies: '保留份数', diff --git a/frontend/src/views/cronjob/operate/index.vue b/frontend/src/views/cronjob/operate/index.vue index 9e8683041..b3e92e415 100644 --- a/frontend/src/views/cronjob/operate/index.vue +++ b/frontend/src/views/cronjob/operate/index.vue @@ -70,12 +70,7 @@ style="width: 100%" v-model="dialogData.rowData!.website" > - + @@ -117,7 +112,7 @@ - 同时保留本地备份(和云存储保留份数一致) + {{ $t('cronjob.saveLocal') }} @@ -171,6 +166,7 @@ import { Cronjob } from '@/api/interface/cronjob'; import { addCronjob, editCronjob } from '@/api/modules/cronjob'; import { loadDBNames } from '@/api/modules/database'; import { CheckAppInstalled } from '@/api/modules/app'; +import { GetWebsiteOptions } from '@/api/modules/website'; interface DialogProps { title: string; @@ -188,15 +184,12 @@ const acceptParams = (params: DialogProps): void => { cronjobVisiable.value = true; checkMysqlInstalled(); loadBackups(); + loadWebsites(); }; const localDirID = ref(); -const websiteOptions = ref([ - { label: '所有', value: 'all' }, - { label: '网站1', value: 'web1' }, - { label: '网站2', value: 'web2' }, -]); +const websiteOptions = ref(); const backupOptions = ref(); const emit = defineEmits<{ (e: 'search'): void }>(); @@ -310,6 +303,11 @@ const loadBackups = async () => { } }; +const loadWebsites = async () => { + const res = await GetWebsiteOptions(); + websiteOptions.value = res.data; +}; + const checkMysqlInstalled = async () => { const res = await CheckAppInstalled('mysql'); mysqlInfo.isExist = res.data.isExist; diff --git a/frontend/src/views/cronjob/record/index.vue b/frontend/src/views/cronjob/record/index.vue index 001e7407d..4db96ebd5 100644 --- a/frontend/src/views/cronjob/record/index.vue +++ b/frontend/src/views/cronjob/record/index.vue @@ -262,8 +262,8 @@ const acceptParams = async (params: DialogProps): Promise => { page: searchInfo.page, pageSize: searchInfo.pageSize, cronjobID: dialogData.value.rowData!.id, - startTime: searchInfo.startTime, - endTime: searchInfo.endTime, + startTime: new Date(new Date().setHours(0, 0, 0, 0)), + endTime: new Date(), status: searchInfo.status ? 'Stoped' : '', }; const res = await searchRecords(itemSearch); diff --git a/frontend/src/views/website/website/backup/index.vue b/frontend/src/views/website/website/backup/index.vue index 91d4bfe31..89c7519fa 100644 --- a/frontend/src/views/website/website/backup/index.vue +++ b/frontend/src/views/website/website/backup/index.vue @@ -93,7 +93,7 @@ const onRecover = async (row: Backup.RecordInfo) => { }; const onBackup = async () => { - await BackupWebsite(websiteID.value); + await BackupWebsite(websiteName.value); ElMessage.success(i18n.global.t('commons.msg.operationSuccess')); search(); }; diff --git a/frontend/src/views/website/website/index.vue b/frontend/src/views/website/website/index.vue index be3669027..40e2f8630 100644 --- a/frontend/src/views/website/website/index.vue +++ b/frontend/src/views/website/website/index.vue @@ -22,6 +22,11 @@ {{ row.primaryDomain }} + + +