Add automated backup system with SQLite VACUUM
- Implement backup package with SQLite VACUUM INTO for safe backups - Add scheduler with configurable interval (default: 24h) - Add retention policy with automatic cleanup (default: 7 days) - Add optional gzip compression (~10x size reduction) - Add comprehensive tests for backup creation and cleanup - Integrate backup scheduler in main.go with graceful shutdown - Add backup configuration variables (BACKUP_ENABLED, BACKUP_INTERVAL, BACKUP_RETENTION_DAYS, BACKUP_PATH, BACKUP_COMPRESS) - Backups run automatically in background goroutine - Coverage: backup package 52.7%
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
package backup
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"apocapoc-api/internal/infrastructure/logger"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Enabled bool
|
||||
Interval time.Duration
|
||||
RetentionDays int
|
||||
Path string
|
||||
Compress bool
|
||||
DatabasePath string
|
||||
}
|
||||
|
||||
func CreateBackup(db *sql.DB, config Config) error {
|
||||
if !config.Enabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(config.Path, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create backup directory: %w", err)
|
||||
}
|
||||
|
||||
timestamp := time.Now().Format("20060102_150405")
|
||||
filename := fmt.Sprintf("apocapoc_%s.db", timestamp)
|
||||
backupPath := filepath.Join(config.Path, filename)
|
||||
|
||||
logger.Info().
|
||||
Str("backup_path", backupPath).
|
||||
Msg("Starting database backup")
|
||||
|
||||
if err := backupDatabase(db, backupPath); err != nil {
|
||||
logger.Error().
|
||||
Err(err).
|
||||
Str("backup_path", backupPath).
|
||||
Msg("Backup failed")
|
||||
return fmt.Errorf("backup failed: %w", err)
|
||||
}
|
||||
|
||||
if config.Compress {
|
||||
compressedPath := backupPath + ".gz"
|
||||
if err := compressFile(backupPath, compressedPath); err != nil {
|
||||
logger.Warn().
|
||||
Err(err).
|
||||
Str("backup_path", backupPath).
|
||||
Msg("Compression failed, keeping uncompressed backup")
|
||||
} else {
|
||||
os.Remove(backupPath)
|
||||
backupPath = compressedPath
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info().
|
||||
Str("backup_path", backupPath).
|
||||
Msg("Backup completed successfully")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func backupDatabase(db *sql.DB, destPath string) error {
|
||||
_, err := db.Exec(fmt.Sprintf("VACUUM INTO '%s'", destPath))
|
||||
if err != nil {
|
||||
return fmt.Errorf("vacuum into failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func compressFile(srcPath, destPath string) error {
|
||||
srcFile, err := os.Open(srcPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer srcFile.Close()
|
||||
|
||||
destFile, err := os.Create(destPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer destFile.Close()
|
||||
|
||||
gzipWriter := gzip.NewWriter(destFile)
|
||||
defer gzipWriter.Close()
|
||||
|
||||
_, err = io.Copy(gzipWriter, srcFile)
|
||||
return err
|
||||
}
|
||||
Reference in New Issue
Block a user