Files
mysql-bkup/pkg/backup.go

418 lines
12 KiB
Go
Raw Normal View History

// Package internal /
2024-12-06 03:25:38 +01:00
/*
MIT License
Copyright (c) 2023 Jonas Kaninda
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
2024-12-07 02:22:35 +01:00
2024-12-06 20:53:46 +01:00
package pkg
import (
2025-03-12 13:27:31 +01:00
"errors"
"fmt"
2024-10-13 14:33:54 +02:00
"github.com/jkaninda/encryptor"
"github.com/jkaninda/go-storage/pkg/local"
2025-01-22 07:22:56 +01:00
goutils "github.com/jkaninda/go-utils"
"github.com/jkaninda/mysql-bkup/utils"
"github.com/robfig/cron/v3"
"github.com/spf13/cobra"
"log"
"os"
"os/exec"
"path/filepath"
"time"
)
func StartBackup(cmd *cobra.Command) {
intro()
2024-12-06 14:21:55 +01:00
// Initialize backup configs
config := initBackupConfig(cmd)
2024-12-06 14:21:55 +01:00
// Load backup configuration file
2024-10-09 12:23:14 +02:00
configFile, err := loadConfigFile()
if err != nil {
dbConf = initDbConfig(cmd)
if config.cronExpression == "" {
BackupTask(dbConf, config)
} else {
2024-10-09 12:23:14 +02:00
if utils.IsValidCronExpression(config.cronExpression) {
scheduledMode(dbConf, config)
} else {
2024-12-06 20:53:46 +01:00
utils.Fatal("Cron expression is not valid: %s", config.cronExpression)
2024-10-09 12:23:14 +02:00
}
}
2024-10-09 12:23:14 +02:00
} else {
startMultiBackup(config, configFile)
}
}
// scheduledMode Runs backup in scheduled mode
func scheduledMode(db *dbConfig, config *BackupConfig) {
2024-12-06 20:53:46 +01:00
utils.Info("Running in Scheduled mode")
utils.Info("Backup cron expression: %s", config.cronExpression)
utils.Info("The next scheduled time is: %v", utils.CronNextTime(config.cronExpression).Format(timeFormat))
utils.Info("Storage type %s ", config.storage)
2024-12-06 14:21:55 +01:00
// Test backup
2024-12-06 20:53:46 +01:00
utils.Info("Testing backup configurations...")
err := testDatabaseConnection(db)
if err != nil {
utils.Error("Error connecting to database: %s", db.dbName)
utils.Fatal("Error: %s", err)
}
2024-12-06 20:53:46 +01:00
utils.Info("Testing backup configurations...done")
utils.Info("Creating backup job...")
// Create a new cron instance
c := cron.New()
_, err = c.AddFunc(config.cronExpression, func() {
BackupTask(db, config)
2024-12-06 20:53:46 +01:00
utils.Info("Next backup time is: %v", utils.CronNextTime(config.cronExpression).Format(timeFormat))
})
if err != nil {
return
}
// Start the cron scheduler
c.Start()
2024-12-06 20:53:46 +01:00
utils.Info("Creating backup job...done")
utils.Info("Backup job started")
defer c.Stop()
select {}
}
// multiBackupTask backup multi database
func multiBackupTask(databases []Database, bkConfig *BackupConfig) {
for _, db := range databases {
2024-12-06 14:21:55 +01:00
// Check if path is defined in config file
if db.Path != "" {
bkConfig.remotePath = db.Path
}
BackupTask(getDatabase(db), bkConfig)
}
}
// BackupTask backups database
func BackupTask(db *dbConfig, config *BackupConfig) {
2024-12-06 20:53:46 +01:00
utils.Info("Starting backup task...")
2025-01-22 07:22:56 +01:00
startTime = time.Now()
2024-12-06 14:21:55 +01:00
// Generate file name
2024-09-30 02:02:37 +02:00
backupFileName := fmt.Sprintf("%s_%s.sql.gz", db.dbName, time.Now().Format("20060102_150405"))
if config.disableCompression {
2024-09-30 02:02:37 +02:00
backupFileName = fmt.Sprintf("%s_%s.sql", db.dbName, time.Now().Format("20060102_150405"))
}
config.backupFileName = backupFileName
switch config.storage {
case "local":
2024-09-29 19:50:26 +02:00
localBackup(db, config)
case "s3", "S3":
2024-09-29 19:50:26 +02:00
s3Backup(db, config)
2025-01-22 07:22:56 +01:00
case "ssh", "SSH", "remote", "sftp":
2024-09-29 19:50:26 +02:00
sshBackup(db, config)
case "ftp", "FTP":
2024-09-30 00:40:35 +02:00
ftpBackup(db, config)
2024-12-06 18:27:25 +01:00
case "azure":
azureBackup(db, config)
default:
2024-09-29 19:50:26 +02:00
localBackup(db, config)
}
}
2024-10-09 12:23:14 +02:00
func startMultiBackup(bkConfig *BackupConfig, configFile string) {
utils.Info("Starting Multi backup task...")
2024-10-09 12:23:14 +02:00
conf, err := readConf(configFile)
if err != nil {
2024-12-06 20:53:46 +01:00
utils.Fatal("Error reading config file: %s", err)
2024-10-09 12:23:14 +02:00
}
2024-12-06 14:21:55 +01:00
// Check if cronExpression is defined in config file
2024-10-09 12:23:14 +02:00
if conf.CronExpression != "" {
bkConfig.cronExpression = conf.CronExpression
}
2024-12-06 14:21:55 +01:00
if len(conf.Databases) == 0 {
2024-12-06 20:53:46 +01:00
utils.Fatal("No databases found")
2024-12-06 14:21:55 +01:00
}
2024-10-09 12:23:14 +02:00
// Check if cronExpression is defined
if bkConfig.cronExpression == "" {
multiBackupTask(conf.Databases, bkConfig)
} else {
backupRescueMode = conf.BackupRescueMode
2024-10-09 12:23:14 +02:00
// Check if cronExpression is valid
if utils.IsValidCronExpression(bkConfig.cronExpression) {
2024-12-06 20:53:46 +01:00
utils.Info("Running backup in Scheduled mode")
utils.Info("Backup cron expression: %s", bkConfig.cronExpression)
utils.Info("The next scheduled time is: %v", utils.CronNextTime(bkConfig.cronExpression).Format(timeFormat))
utils.Info("Storage type %s ", bkConfig.storage)
2024-12-06 14:21:55 +01:00
// Test backup
2024-12-06 20:53:46 +01:00
utils.Info("Testing backup configurations...")
2024-12-06 14:21:55 +01:00
for _, db := range conf.Databases {
err = testDatabaseConnection(getDatabase(db))
if err != nil {
recoverMode(err, fmt.Sprintf("Error connecting to database: %s", db.Name))
continue
}
2024-12-06 14:21:55 +01:00
}
2024-12-06 20:53:46 +01:00
utils.Info("Testing backup configurations...done")
utils.Info("Creating backup job...")
2024-10-09 12:23:14 +02:00
// Create a new cron instance
c := cron.New()
_, err := c.AddFunc(bkConfig.cronExpression, func() {
multiBackupTask(conf.Databases, bkConfig)
2024-12-06 20:53:46 +01:00
utils.Info("Next backup time is: %v", utils.CronNextTime(bkConfig.cronExpression).Format(timeFormat))
2024-10-09 12:23:14 +02:00
})
if err != nil {
return
}
// Start the cron scheduler
c.Start()
2024-12-06 20:53:46 +01:00
utils.Info("Creating backup job...done")
utils.Info("Backup job started")
2024-10-09 12:23:14 +02:00
defer c.Stop()
select {}
} else {
2024-12-06 20:53:46 +01:00
utils.Fatal("Cron expression is not valid: %s", bkConfig.cronExpression)
2024-10-09 12:23:14 +02:00
}
}
}
2024-01-19 06:56:19 +01:00
// BackupDatabase backup database
2025-03-12 15:50:30 +01:00
func BackupDatabase(db *dbConfig, backupFileName string, disableCompression, all bool) error {
storagePath = os.Getenv("STORAGE_PATH")
2024-12-06 20:53:46 +01:00
utils.Info("Starting database backup...")
2025-03-12 15:50:30 +01:00
//err := os.Setenv("MYSQL_PWD", db.dbPassword)
//if err != nil {
// return fmt.Errorf("failed to set MYSQL_PWD environment variable: %v", err)
//}
err := testDatabaseConnection(db)
if err != nil {
2025-03-12 13:27:31 +01:00
return errors.New(err.Error())
}
// Backup Database database
2024-12-06 20:53:46 +01:00
utils.Info("Backing up database...")
// Verify is compression is disabled
if disableCompression {
2025-03-12 15:50:30 +01:00
if all {
// Backup all databases
// Execute mysqldump
cmd := exec.Command("mysqldump",
fmt.Sprintf("--defaults-file=%s", mysqlClientConfig), "--all-databases", "--single-transaction", "--routines", "--triggers",
)
output, err := cmd.Output()
if err != nil {
return fmt.Errorf("failed to backup database: %v output: %v", err, string(output))
}
2025-03-12 15:50:30 +01:00
// save output
file, err := os.Create(filepath.Join(tmpPath, backupFileName))
2024-12-06 14:21:55 +01:00
if err != nil {
2025-03-12 15:50:30 +01:00
return fmt.Errorf("failed to create backup file: %v", err)
}
defer func(file *os.File) {
err := file.Close()
if err != nil {
return
}
}(file)
_, err = file.Write(output)
if err != nil {
return err
}
utils.Info("Database has been backed up")
} else {
// Execute mysqldump
cmd := exec.Command("mysqldump",
fmt.Sprintf("--defaults-file=%s", mysqlClientConfig), db.dbName,
)
output, err := cmd.Output()
if err != nil {
return fmt.Errorf("failed to backup database: %v output: %v", err, string(output))
2024-12-06 14:21:55 +01:00
}
2025-03-12 15:50:30 +01:00
// save output
file, err := os.Create(filepath.Join(tmpPath, backupFileName))
if err != nil {
return fmt.Errorf("failed to create backup file: %v", err)
}
defer func(file *os.File) {
err := file.Close()
if err != nil {
return
}
}(file)
_, err = file.Write(output)
if err != nil {
return err
}
utils.Info("Database has been backed up")
}
} else {
2025-03-12 15:50:30 +01:00
if all {
// Execute mysqldump
cmd := exec.Command("mysqldump", fmt.Sprintf("--defaults-file=%s", mysqlClientConfig), "--all-databases", "--single-transaction", "--routines", "--triggers")
stdout, err := cmd.StdoutPipe()
if err != nil {
return fmt.Errorf("failed to backup database: %v output: %v", err, stdout)
}
gzipCmd := exec.Command("gzip")
gzipCmd.Stdin = stdout
gzipCmd.Stdout, err = os.Create(filepath.Join(tmpPath, backupFileName))
err = gzipCmd.Start()
if err != nil {
return fmt.Errorf("failed to backup database: %v", err)
}
if err := cmd.Run(); err != nil {
log.Fatal(err)
}
if err := gzipCmd.Wait(); err != nil {
log.Fatal(err)
}
} else {
// Execute mysqldump
cmd := exec.Command("mysqldump", fmt.Sprintf("--defaults-file=%s", mysqlClientConfig), db.dbName)
stdout, err := cmd.StdoutPipe()
if err != nil {
return fmt.Errorf("failed to backup database: %v output: %v", err, stdout)
}
gzipCmd := exec.Command("gzip")
gzipCmd.Stdin = stdout
gzipCmd.Stdout, err = os.Create(filepath.Join(tmpPath, backupFileName))
err = gzipCmd.Start()
if err != nil {
return fmt.Errorf("failed to backup database: %v", err)
}
if err := cmd.Run(); err != nil {
log.Fatal(err)
}
if err := gzipCmd.Wait(); err != nil {
log.Fatal(err)
}
2025-03-12 15:50:30 +01:00
}
}
utils.Info("Database has been backed up")
return nil
}
2024-09-29 19:50:26 +02:00
func localBackup(db *dbConfig, config *BackupConfig) {
2024-12-06 20:53:46 +01:00
utils.Info("Backup database to local storage")
2025-03-12 15:50:30 +01:00
err := BackupDatabase(db, config.backupFileName, disableCompression, config.all)
if err != nil {
recoverMode(err, "Error backing up database")
return
}
2024-09-29 19:50:26 +02:00
finalFileName := config.backupFileName
if config.encryption {
encryptBackup(config)
2024-09-29 19:50:26 +02:00
finalFileName = fmt.Sprintf("%s.%s", config.backupFileName, gpgExtension)
}
fileInfo, err := os.Stat(filepath.Join(tmpPath, finalFileName))
if err != nil {
2024-12-06 20:53:46 +01:00
utils.Error("Error: %s", err)
}
backupSize = fileInfo.Size()
localStorage := local.NewStorage(local.Config{
LocalPath: tmpPath,
RemotePath: storagePath,
})
err = localStorage.Copy(finalFileName)
if err != nil {
2024-12-06 20:53:46 +01:00
utils.Fatal("Error copying backup file: %s", err)
}
utils.Info("Backup name is %s", finalFileName)
utils.Info("Backup size: %s", utils.ConvertBytes(uint64(backupSize)))
2024-12-06 20:53:46 +01:00
utils.Info("Backup saved in %s", filepath.Join(storagePath, finalFileName))
2025-01-22 07:22:56 +01:00
duration := goutils.FormatDuration(time.Since(startTime), 0)
2024-12-06 14:21:55 +01:00
// Send notification
utils.NotifySuccess(&utils.NotificationData{
File: finalFileName,
BackupSize: utils.ConvertBytes(uint64(backupSize)),
Database: db.dbName,
Storage: config.storage,
BackupLocation: filepath.Join(storagePath, finalFileName),
2025-01-22 07:22:56 +01:00
Duration: duration,
})
2024-12-06 14:21:55 +01:00
// Delete old backup
2024-09-29 19:50:26 +02:00
if config.prune {
err = localStorage.Prune(config.backupRetention)
if err != nil {
2024-12-06 20:53:46 +01:00
utils.Fatal("Error deleting old backup from %s storage: %s ", config.storage, err)
}
}
2024-12-06 14:21:55 +01:00
// Delete temp
deleteTemp()
2025-01-22 07:22:56 +01:00
utils.Info("Backup successfully completed in %s", duration)
}
func encryptBackup(config *BackupConfig) {
2024-10-13 14:33:54 +02:00
backupFile, err := os.ReadFile(filepath.Join(tmpPath, config.backupFileName))
outputFile := fmt.Sprintf("%s.%s", filepath.Join(tmpPath, config.backupFileName), gpgExtension)
if err != nil {
2024-12-06 20:53:46 +01:00
utils.Fatal("Error reading backup file: %s ", err)
2024-10-13 14:33:54 +02:00
}
if config.usingKey {
2024-12-06 20:53:46 +01:00
utils.Info("Encrypting backup using public key...")
2024-10-13 14:33:54 +02:00
pubKey, err := os.ReadFile(config.publicKey)
if err != nil {
2024-12-06 20:53:46 +01:00
utils.Fatal("Error reading public key: %s ", err)
}
2024-10-13 14:33:54 +02:00
err = encryptor.EncryptWithPublicKey(backupFile, fmt.Sprintf("%s.%s", filepath.Join(tmpPath, config.backupFileName), gpgExtension), pubKey)
if err != nil {
2024-12-06 20:53:46 +01:00
utils.Fatal("Error encrypting backup file: %v ", err)
2024-10-13 14:33:54 +02:00
}
2024-12-06 20:53:46 +01:00
utils.Info("Encrypting backup using public key...done")
2024-10-13 14:33:54 +02:00
} else if config.passphrase != "" {
2024-12-06 20:53:46 +01:00
utils.Info("Encrypting backup using passphrase...")
2024-10-13 14:33:54 +02:00
err := encryptor.Encrypt(backupFile, outputFile, config.passphrase)
if err != nil {
2024-12-06 20:53:46 +01:00
utils.Fatal("error during encrypting backup %v", err)
}
2024-12-06 20:53:46 +01:00
utils.Info("Encrypting backup using passphrase...done")
}
}
func recoverMode(err error, msg string) {
if err != nil {
if backupRescueMode {
utils.NotifyError(fmt.Sprintf("%s : %v", msg, err))
2025-01-26 13:54:41 +01:00
utils.Error("Error: %s", msg)
utils.Error("Backup rescue mode is enabled")
utils.Error("Backup will continue")
} else {
2025-03-12 13:27:31 +01:00
utils.Error("Error 10: %s", msg)
utils.Fatal("Error 10: %v", err)
return
}
}
}