package client import ( "backup-x/entity" "backup-x/util" "fmt" "io/ioutil" "log" "os" "os/exec" "runtime" "strings" "sync" "time" ) // 数据库备份最小的文件大小 const minFileSize = 1000 // backupLooper type backupLooper struct { Wg sync.WaitGroup Tickers []*time.Ticker } var bl = &backupLooper{Wg: sync.WaitGroup{}} // RunLoop backup db loop func 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() } // StopRunLoop func 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]) } // run func 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) } } // prepare func 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("%s 执行shell的输出: 点击此处查看\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 todayString func 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 }