| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256 | package clientimport (	"backup-x/entity"	"backup-x/util"	"fmt"	"io/ioutil"	"log"	"os"	"os/exec"	"runtime"	"strings"	"sync"	"time")// 数据库备份最小的文件大小const minFileSize = 1000// backupLoopertype backupLooper struct {	Wg      sync.WaitGroup	Tickers []*time.Ticker}var bl = &backupLooper{Wg: sync.WaitGroup{}}// RunLoop backup db loopfunc RunLoop(firstDelay time.Duration) {	conf, err := entity.GetConfigCache()	if err != nil {		return	}	time.Sleep(firstDelay)	// clear	bl.Tickers = []*time.Ticker{}	for _, backupConf := range conf.BackupConfig {		if !backupConf.NotEmptyProject() {			continue		}		if backupConf.Enabled != 0 {			log.Println(backupConf.ProjectName + " 项目被停用")			continue		}		if !backupConf.CheckPeriod() {			log.Println(backupConf.ProjectName + " 项目的周期值不正确")			continue		}		delay := util.GetDelaySeconds(backupConf.StartTime)		ticker := time.NewTicker(delay)		log.Printf("%s项目将在%.1f小时后运行\n", backupConf.ProjectName, delay.Hours())		bl.Wg.Add(1)		go func(backupConf entity.BackupConfig) {			defer bl.Wg.Done()			for {				<-ticker.C				run(conf, backupConf)				ticker.Reset(time.Minute * time.Duration(backupConf.Period))				log.Printf("%s项目将等待%d分钟后循环运行\n", backupConf.ProjectName, backupConf.Period)			}		}(backupConf)		bl.Tickers = append(bl.Tickers, ticker)	}	bl.Wg.Wait()}// StopRunLoopfunc StopRunLoop() {	for _, ticker := range bl.Tickers {		if ticker != nil {			ticker.Stop()		}	}}// RunOnce 运行一次func RunOnce() {	conf, err := entity.GetConfigCache()	if err != nil {		return	}	for _, backupConf := range conf.BackupConfig {		run(conf, backupConf)	}}// 运行指定的索引号func RunByIdx(idx int) {	conf, err := entity.GetConfigCache()	if err != nil {		return	}	run(conf, conf.BackupConfig[idx])}// runfunc run(conf entity.Config, backupConf entity.BackupConfig) {	if backupConf.NotEmptyProject() && backupConf.Enabled == 0 {		err := prepare(backupConf)		if err != nil {			log.Println(err)			return		}		// backup		outFileName, err := backup(backupConf, conf.EncryptKey, conf.S3Config)		result := entity.BackupResult{ProjectName: backupConf.ProjectName, Result: "失败"}		if err == nil {			// webhook			if outFileName != nil {				result.FileName = outFileName.Name()				result.FileSize = fmt.Sprintf("%d MB", outFileName.Size()/1000/1000)				// send file to s3				if conf.S3Config.CheckNotEmpty() {					go conf.S3Config.UploadFile(backupConf.GetProjectPath() + string(os.PathSeparator) + outFileName.Name())				}			}			result.Result = "成功"		}		conf.ExecWebhook(result)	}}// preparefunc prepare(backupConf entity.BackupConfig) (err error) {	// create floder	os.MkdirAll(backupConf.GetProjectPath(), 0750)	return}func backup(backupConf entity.BackupConfig, encryptKey string, s3Conf entity.S3Config) (outFileName os.FileInfo, err error) {	projectName := backupConf.ProjectName	log.Printf("正在备份项目: %s ...", projectName)	todayString := time.Now().Format(util.FileNameFormatStr)	shellString := strings.ReplaceAll(backupConf.Command, "#{DATE}", todayString)	// 解密pwd	pwd := ""	if backupConf.Pwd != "" {		pwd, err = util.DecryptByEncryptKey(encryptKey, backupConf.Pwd)		if err != nil {			err = fmt.Errorf("解密失败")			log.Println(err)			return nil, err		}	}	// 解密s3 SecretKey	secretKey := ""	if s3Conf.SecretKey != "" {		secretKey, err = util.DecryptByEncryptKey(encryptKey, s3Conf.SecretKey)		if err != nil {			err = fmt.Errorf("解密失败")			log.Println(err)			return nil, err		}	}	shellString = strings.ReplaceAll(shellString, "#{PWD}", pwd)	shellString = strings.ReplaceAll(shellString, "#{AccessKey}", s3Conf.AccessKey)	shellString = strings.ReplaceAll(shellString, "#{SecretKey}", secretKey)	shellString = strings.ReplaceAll(shellString, "#{Endpoint}", s3Conf.Endpoint)	shellString = strings.ReplaceAll(shellString, "#{BucketName}", s3Conf.BucketName)	// create shell file	var shellName string	if runtime.GOOS == "windows" {		shellName = time.Now().Format("shell-"+util.FileNameFormatStr+"-") + "backup.bat"	} else {		shellString = strings.ReplaceAll(shellString, "\r\n", "\n") // windows to linux		shellName = time.Now().Format("shell-"+util.FileNameFormatStr+"-") + "backup.sh"	}	shellFile, err := os.Create(backupConf.GetProjectPath() + string(os.PathSeparator) + shellName)	shellFile.Chmod(0700)	if err == nil {		shellFile.WriteString(shellString)		shellFile.Close()	} else {		log.Println("Create file with error: ", err)	}	// run shell file	var shell *exec.Cmd	if runtime.GOOS == "windows" {		shell = exec.Command("cmd", "/c", shellName)	} else {		shell = exec.Command("bash", shellName)	}	shell.Dir = backupConf.GetProjectPath()	outputBytes, err := shell.CombinedOutput()	if len(outputBytes) > 0 {		if util.IsGBK(outputBytes) {			outputBytes, _ = util.GbkToUtf8(outputBytes)		}		log.Printf("<span style='color: #7983f5;font-weight: bold;'>%s</span> 执行shell的输出: <span class='click-layer' onclick='showLayer(this)' tip=\"%s\" style='cursor: pointer; color: #4a3a3a; font-weight: bold; border: 2px dashed;'>点击此处查看</span>\n", backupConf.ProjectName, util.EscapeShell(string(outputBytes)))	} else {		log.Printf("执行shell的输出为空\n")	}	// execute shell success	if err == nil {		// find backup file by todayString		outFileName, err = findBackupFile(backupConf, todayString)		if backupConf.BackupType == 0 {			// 备份数据库			// check file size			if err != nil {				log.Println(err)			} else if outFileName.Size() >= minFileSize {				log.Printf("成功备份项目: %s, 文件名: %s\n", projectName, outFileName.Name())			} else {				err = fmt.Errorf("%s 备份后的文件小于 %d 字节, 当前为:%d 字节", projectName, minFileSize, outFileName.Size())				log.Println(err)			}		} else {			// 1 同步文件			// err = nil			err = nil		}	} else {		err = fmt.Errorf("执行备份shell失败: %s", util.EscapeShell(string(outputBytes)))		log.Println(err)	}	// remove shell file	os.Remove(shellFile.Name())	return}// find backup file by todayStringfunc findBackupFile(backupConf entity.BackupConfig, todayString string) (backupFile os.FileInfo, err error) {	files, err := ioutil.ReadDir(backupConf.GetProjectPath())	for _, file := range files {		if strings.Contains(file.Name(), todayString) && !strings.HasPrefix(file.Name(), "shell-") {			backupFile = file			return		}	}	err = fmt.Errorf("项目 %s 没有输出包含 %s 的文件名", backupConf.ProjectName, todayString)	return}
 |