veza/veza-backend-api/internal/services/backup_service.go
senke 430cc5eef6 fix(security): validate exec.Command paths in Go services
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-11 21:32:38 +01:00

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
}