using Data; using Microsoft.EntityFrameworkCore; namespace WebApp.Services; /// /// Creates SQLite database backups via VACUUM INTO. /// public class DatabaseBackupService : IDatabaseBackupService { private readonly AppDbContext _context; private readonly IWebHostEnvironment _environment; private readonly ILogger _logger; public DatabaseBackupService( AppDbContext context, IWebHostEnvironment environment, ILogger logger) { _context = context; _environment = environment; _logger = logger; } public async Task CreatePreRolloverBackupAsync(CancellationToken cancellationToken = default) { var backupsDir = Path.Combine(_environment.ContentRootPath, "Data", "backups"); Directory.CreateDirectory(backupsDir); var fileName = $"pre-rollover-{DateTime.Now:yyyyMMdd-HHmmss}.db"; var backupPath = Path.Combine(backupsDir, fileName); // Path is server-generated (never user input); escape single quotes for SQLite string literal. var escapedPath = backupPath.Replace("'", "''", StringComparison.Ordinal); #pragma warning disable EF1002 // Path is fully server-controlled; VACUUM INTO cannot use parameters. await _context.Database.ExecuteSqlRawAsync($"VACUUM INTO '{escapedPath}'", cancellationToken); #pragma warning restore EF1002 if (!File.Exists(backupPath)) { throw new InvalidOperationException( $"Database backup was requested but the file was not created at '{backupPath}'."); } _logger.LogInformation("Created pre-rollover database backup at {BackupPath}", backupPath); return backupPath; } }