257 lines
6.9 KiB
Go
257 lines
6.9 KiB
Go
package services
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"veza-backend-api/internal/utils"
|
|
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// BackupService gère les sauvegardes de base de données
|
|
// BE-DB-016: Implement automated database backups with retention policy
|
|
type BackupService struct {
|
|
logger *zap.Logger
|
|
backupDir string
|
|
retentionDays int
|
|
databaseURL string
|
|
databaseName string
|
|
databaseUser string
|
|
databaseHost string
|
|
databasePort string
|
|
}
|
|
|
|
// BackupConfig contient la configuration pour les sauvegardes
|
|
type BackupConfig struct {
|
|
BackupDir string // Répertoire où stocker les backups
|
|
RetentionDays int // Nombre de jours de rétention
|
|
DatabaseURL string // URL de connexion à la base de données
|
|
DatabaseName string // Nom de la base de données
|
|
DatabaseUser string // Utilisateur de la base de données
|
|
DatabaseHost string // Hôte de la base de données
|
|
DatabasePort string // Port de la base de données
|
|
}
|
|
|
|
// BackupResult contient les informations sur une sauvegarde
|
|
type BackupResult struct {
|
|
Success bool
|
|
BackupPath string
|
|
BackupSize int64
|
|
Duration time.Duration
|
|
ErrorMessage string
|
|
CreatedAt time.Time
|
|
}
|
|
|
|
// NewBackupService crée un nouveau service de backup
|
|
func NewBackupService(config BackupConfig, logger *zap.Logger) (*BackupService, error) {
|
|
// Créer le répertoire de backup s'il n'existe pas
|
|
if err := os.MkdirAll(config.BackupDir, 0755); err != nil {
|
|
return nil, fmt.Errorf("failed to create backup directory: %w", err)
|
|
}
|
|
|
|
// Valeurs par défaut
|
|
retentionDays := config.RetentionDays
|
|
if retentionDays <= 0 {
|
|
retentionDays = 30 // 30 jours par défaut
|
|
}
|
|
|
|
return &BackupService{
|
|
logger: logger,
|
|
backupDir: config.BackupDir,
|
|
retentionDays: retentionDays,
|
|
databaseURL: config.DatabaseURL,
|
|
databaseName: config.DatabaseName,
|
|
databaseUser: config.DatabaseUser,
|
|
databaseHost: config.DatabaseHost,
|
|
databasePort: config.DatabasePort,
|
|
}, nil
|
|
}
|
|
|
|
// CreateBackup crée une sauvegarde de la base de données
|
|
func (bs *BackupService) CreateBackup(ctx context.Context) (*BackupResult, error) {
|
|
startTime := time.Now()
|
|
|
|
// Générer le nom du fichier de backup avec timestamp
|
|
timestamp := time.Now().Format("20060102_150405")
|
|
backupFileName := fmt.Sprintf("%s_%s.sql", bs.databaseName, timestamp)
|
|
backupPath := filepath.Join(bs.backupDir, backupFileName)
|
|
|
|
// SECURITY: Validate path for exec.Command
|
|
if !utils.ValidateExecPath(backupPath) {
|
|
return &BackupResult{
|
|
Success: false, ErrorMessage: "invalid backup path",
|
|
Duration: time.Since(startTime), CreatedAt: time.Now(),
|
|
}, fmt.Errorf("invalid backup path")
|
|
}
|
|
|
|
bs.logger.Info("Creating database backup",
|
|
zap.String("database", bs.databaseName),
|
|
zap.String("backup_path", backupPath))
|
|
|
|
// Construire la commande pg_dump
|
|
// Utiliser pg_dump avec format custom pour compression
|
|
var cmd *exec.Cmd
|
|
if bs.databaseURL != "" {
|
|
// Utiliser DATABASE_URL si disponible
|
|
cmd = exec.CommandContext(ctx, "pg_dump", "-Fc", "-f", backupPath, bs.databaseURL)
|
|
} else {
|
|
// Utiliser les paramètres individuels
|
|
args := []string{
|
|
"-h", bs.databaseHost,
|
|
"-p", bs.databasePort,
|
|
"-U", bs.databaseUser,
|
|
"-Fc", // Format custom (compressed)
|
|
"-f", backupPath,
|
|
bs.databaseName,
|
|
}
|
|
cmd = exec.CommandContext(ctx, "pg_dump", args...)
|
|
// Définir PGPASSWORD si nécessaire
|
|
if password := os.Getenv("PGPASSWORD"); password != "" {
|
|
cmd.Env = append(os.Environ(), fmt.Sprintf("PGPASSWORD=%s", password))
|
|
}
|
|
}
|
|
|
|
// Exécuter la commande
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
bs.logger.Error("Failed to create backup",
|
|
zap.Error(err),
|
|
zap.String("output", string(output)))
|
|
return &BackupResult{
|
|
Success: false,
|
|
ErrorMessage: fmt.Sprintf("pg_dump failed: %v: %s", err, string(output)),
|
|
Duration: time.Since(startTime),
|
|
CreatedAt: time.Now(),
|
|
}, fmt.Errorf("backup failed: %w", err)
|
|
}
|
|
|
|
// Obtenir la taille du fichier
|
|
fileInfo, err := os.Stat(backupPath)
|
|
if err != nil {
|
|
bs.logger.Warn("Failed to get backup file size", zap.Error(err))
|
|
}
|
|
|
|
var backupSize int64
|
|
if fileInfo != nil {
|
|
backupSize = fileInfo.Size()
|
|
}
|
|
|
|
duration := time.Since(startTime)
|
|
|
|
bs.logger.Info("Backup created successfully",
|
|
zap.String("backup_path", backupPath),
|
|
zap.Int64("backup_size", backupSize),
|
|
zap.Duration("duration", duration))
|
|
|
|
return &BackupResult{
|
|
Success: true,
|
|
BackupPath: backupPath,
|
|
BackupSize: backupSize,
|
|
Duration: duration,
|
|
CreatedAt: time.Now(),
|
|
}, nil
|
|
}
|
|
|
|
// CleanupOldBackups supprime les anciennes sauvegardes selon la politique de rétention
|
|
func (bs *BackupService) CleanupOldBackups(ctx context.Context) error {
|
|
bs.logger.Info("Cleaning up old backups",
|
|
zap.Int("retention_days", bs.retentionDays))
|
|
|
|
cutoffTime := time.Now().AddDate(0, 0, -bs.retentionDays)
|
|
|
|
// Lire le répertoire de backup
|
|
entries, err := os.ReadDir(bs.backupDir)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read backup directory: %w", err)
|
|
}
|
|
|
|
deletedCount := 0
|
|
for _, entry := range entries {
|
|
if entry.IsDir() {
|
|
continue
|
|
}
|
|
|
|
// Vérifier si le fichier correspond au pattern de backup
|
|
ext := filepath.Ext(entry.Name())
|
|
if ext != ".sql" && ext != ".dump" {
|
|
continue
|
|
}
|
|
|
|
// Obtenir les informations du fichier
|
|
filePath := filepath.Join(bs.backupDir, entry.Name())
|
|
fileInfo, err := entry.Info()
|
|
if err != nil {
|
|
bs.logger.Warn("Failed to get file info", zap.String("file", entry.Name()), zap.Error(err))
|
|
continue
|
|
}
|
|
|
|
// Vérifier si le fichier est plus ancien que la date de rétention
|
|
if fileInfo.ModTime().Before(cutoffTime) {
|
|
if err := os.Remove(filePath); err != nil {
|
|
bs.logger.Error("Failed to delete old backup",
|
|
zap.String("file", entry.Name()),
|
|
zap.Error(err))
|
|
continue
|
|
}
|
|
|
|
deletedCount++
|
|
bs.logger.Info("Deleted old backup",
|
|
zap.String("file", entry.Name()),
|
|
zap.Time("modified", fileInfo.ModTime()))
|
|
}
|
|
}
|
|
|
|
bs.logger.Info("Cleanup completed",
|
|
zap.Int("deleted_count", deletedCount))
|
|
|
|
return nil
|
|
}
|
|
|
|
// ListBackups liste toutes les sauvegardes disponibles
|
|
func (bs *BackupService) ListBackups(ctx context.Context) ([]BackupInfo, error) {
|
|
entries, err := os.ReadDir(bs.backupDir)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read backup directory: %w", err)
|
|
}
|
|
|
|
var backups []BackupInfo
|
|
for _, entry := range entries {
|
|
if entry.IsDir() {
|
|
continue
|
|
}
|
|
|
|
// Vérifier si le fichier correspond au pattern de backup
|
|
ext := filepath.Ext(entry.Name())
|
|
if ext != ".sql" && ext != ".dump" {
|
|
continue
|
|
}
|
|
|
|
filePath := filepath.Join(bs.backupDir, entry.Name())
|
|
fileInfo, err := entry.Info()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
backups = append(backups, BackupInfo{
|
|
FileName: entry.Name(),
|
|
FilePath: filePath,
|
|
Size: fileInfo.Size(),
|
|
CreatedAt: fileInfo.ModTime(),
|
|
})
|
|
}
|
|
|
|
return backups, nil
|
|
}
|
|
|
|
// BackupInfo contient les informations sur une sauvegarde
|
|
type BackupInfo struct {
|
|
FileName string
|
|
FilePath string
|
|
Size int64
|
|
CreatedAt time.Time
|
|
}
|