feat: 计划任务适配网站定时备份

pull/55/head
ssongliu 2022-11-30 15:47:11 +08:00 committed by ssongliu
parent f50656320b
commit b64b2e1017
14 changed files with 191 additions and 116 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -49,7 +49,7 @@ export const loadMysqlStatus = () => {
return http.get<Database.MysqlStatus>(`/databases/status`);
};
export const loadDBNames = () => {
return http.get<Array<string>>(`/databases/dbs`);
return http.get<Array<string>>(`/databases/options`);
};
// redis

View File

@ -26,6 +26,10 @@ export const GetWebsite = (id: number) => {
return http.get<WebSite.WebSiteDTO>(`/websites/${id}`);
};
export const GetWebsiteOptions = () => {
return http.get<Array<string>>(`/websites/options`);
};
export const GetWebsiteNginx = (id: number) => {
return http.get<File.File>(`/websites/${id}/nginx`);
};

View File

@ -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',

View File

@ -457,6 +457,7 @@ export default {
directory: '',
sourceDir: '',
exclusionRules: '',
saveLocal: '',
url: 'URL ',
target: '',
retainCopies: '',

View File

@ -70,12 +70,7 @@
style="width: 100%"
v-model="dialogData.rowData!.website"
>
<el-option
v-for="item in websiteOptions"
:key="item.label"
:value="item.value"
:label="item.label"
/>
<el-option v-for="item in websiteOptions" :key="item" :value="item" :label="item" />
</el-select>
</el-form-item>
@ -117,7 +112,7 @@
</el-form-item>
<el-form-item v-if="dialogData.rowData!.targetDirID !== localDirID">
<el-checkbox v-model="dialogData.rowData!.keepLocal">
同时保留本地备份和云存储保留份数一致
{{ $t('cronjob.saveLocal') }}
</el-checkbox>
</el-form-item>
<el-form-item :label="$t('cronjob.retainCopies')" prop="retainCopies">
@ -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;

View File

@ -262,8 +262,8 @@ const acceptParams = async (params: DialogProps): Promise<void> => {
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);

View File

@ -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();
};

View File

@ -22,6 +22,11 @@
<el-link @click="openConfig(row.id)">{{ row.primaryDomain }}</el-link>
</template>
</el-table-column>
<el-table-column :label="$t('commons.table.type')" fix prop="type">
<template #default="{ row }">
{{ $t('website.' + row.type) }}
</template>
</el-table-column>
<el-table-column :label="$t('commons.table.status')" prop="status"></el-table-column>
<el-table-column :label="$t('website.remark')" prop="remark"></el-table-column>
<el-table-column :label="$t('website.protocol')" prop="protocol"></el-table-column>