PlanTempusAdmin/Services/BackupService.cs
Janus C. H. Knudsen 77d35ff965 Initial commit
2026-02-03 00:17:08 +01:00

261 lines
12 KiB
C#

using Dapper;
using Npgsql;
using PlanTempusAdmin.Models;
namespace PlanTempusAdmin.Services;
public class BackupService
{
private readonly string _connectionString;
private readonly ILogger<BackupService> _logger;
static BackupService()
{
DefaultTypeMap.MatchNamesWithUnderscores = true;
}
public BackupService(IConfiguration configuration, ILogger<BackupService> logger)
{
_connectionString = configuration.GetConnectionString("BackupDb")
?? throw new InvalidOperationException("BackupDb connection string not configured");
_logger = logger;
}
public async Task<List<BackupLog>> GetLogsAsync(int limit = 100)
{
try
{
await using var connection = new NpgsqlConnection(_connectionString);
var logs = await connection.QueryAsync<BackupLog>(@"
SELECT id, started_at, completed_at, duration_ms, backup_type, source_name, source_path,
destination, remote_path, status, size_bytes, file_count, error_message, error_code,
retry_count, hostname, script_version, checksum, created_at
FROM backup_logs
ORDER BY started_at DESC
LIMIT @limit", new { limit });
return logs.ToList();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error fetching backup logs");
return new List<BackupLog>();
}
}
public async Task<BackupSummary> GetSummaryAsync()
{
try
{
await using var connection = new NpgsqlConnection(_connectionString);
var summary = await connection.QuerySingleOrDefaultAsync<BackupSummary>(@"
SELECT
COUNT(*)::int as total_backups,
COUNT(*) FILTER (WHERE status = 'success')::int as successful_backups,
COUNT(*) FILTER (WHERE status = 'failed')::int as failed_backups,
MAX(started_at) as last_backup,
MAX(started_at) FILTER (WHERE status = 'success') as last_successful_backup,
COALESCE(SUM(size_bytes) FILTER (WHERE status = 'success'), 0) as total_size_bytes
FROM backup_logs");
return summary ?? new BackupSummary();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error fetching backup summary");
return new BackupSummary();
}
}
public async Task<List<RepositorySummary>> GetRepositorySummariesAsync()
{
try
{
await using var connection = new NpgsqlConnection(_connectionString);
var summaries = await connection.QueryAsync<RepositorySummary>(@"
WITH ranked_backups AS (
SELECT
source_name,
backup_type,
size_bytes,
started_at,
status,
ROW_NUMBER() OVER (PARTITION BY source_name ORDER BY started_at DESC) as rn
FROM backup_logs
WHERE status = 'success'
)
SELECT
bl.source_name,
bl.backup_type,
COUNT(*)::int as total_backups,
COUNT(*) FILTER (WHERE bl.status = 'success')::int as successful_backups,
COUNT(*) FILTER (WHERE bl.status = 'failed')::int as failed_backups,
MAX(bl.started_at) as last_backup,
MAX(bl.started_at) FILTER (WHERE bl.status = 'success') as last_successful_backup,
COALESCE(SUM(bl.size_bytes) FILTER (WHERE bl.status = 'success'), 0) as total_size_bytes,
(SELECT rb.size_bytes FROM ranked_backups rb WHERE rb.source_name = bl.source_name AND rb.rn = 1) as last_backup_size_bytes,
(SELECT rb.size_bytes FROM ranked_backups rb WHERE rb.source_name = bl.source_name AND rb.rn = 2) as previous_backup_size_bytes
FROM backup_logs bl
GROUP BY bl.source_name, bl.backup_type
ORDER BY last_backup DESC NULLS LAST");
return summaries.ToList();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error fetching repository summaries");
return new List<RepositorySummary>();
}
}
public async Task<BackupDashboard> GetDashboardAsync()
{
var dashboard = new BackupDashboard();
try
{
await using var connection = new NpgsqlConnection(_connectionString);
// Overall stats
var stats = await connection.QuerySingleOrDefaultAsync<dynamic>(@"
SELECT
COUNT(*)::int as total_backups,
COUNT(*) FILTER (WHERE status = 'success')::int as successful_backups,
COUNT(*) FILTER (WHERE status = 'failed')::int as failed_backups,
COUNT(*) FILTER (WHERE status = 'running')::int as running_backups,
COALESCE(SUM(size_bytes) FILTER (WHERE status = 'success'), 0) as total_size_bytes,
MAX(started_at) as last_backup,
MAX(started_at) FILTER (WHERE status = 'success') as last_successful_backup,
COUNT(*) FILTER (WHERE started_at > NOW() - INTERVAL '24 hours')::int as backups_last_24_hours,
COUNT(*) FILTER (WHERE started_at > NOW() - INTERVAL '7 days')::int as backups_last_7_days,
COALESCE(SUM(size_bytes) FILTER (WHERE status = 'success' AND started_at > NOW() - INTERVAL '24 hours'), 0) as size_last_24_hours,
COALESCE(SUM(size_bytes) FILTER (WHERE status = 'success' AND started_at > NOW() - INTERVAL '7 days'), 0) as size_last_7_days
FROM backup_logs");
if (stats != null)
{
dashboard.TotalBackups = (int)stats.total_backups;
dashboard.SuccessfulBackups = (int)stats.successful_backups;
dashboard.FailedBackups = (int)stats.failed_backups;
dashboard.RunningBackups = (int)stats.running_backups;
dashboard.TotalSizeBytes = (long)stats.total_size_bytes;
dashboard.LastBackup = stats.last_backup;
dashboard.LastSuccessfulBackup = stats.last_successful_backup;
dashboard.BackupsLast24Hours = (int)stats.backups_last_24_hours;
dashboard.BackupsLast7Days = (int)stats.backups_last_7_days;
dashboard.SizeLast24Hours = (long)stats.size_last_24_hours;
dashboard.SizeLast7Days = (long)stats.size_last_7_days;
}
// By backup type
dashboard.ByType = (await connection.QueryAsync<BackupTypeStat>(@"
SELECT
backup_type,
COUNT(*)::int as total,
COUNT(*) FILTER (WHERE status = 'success')::int as successful,
COUNT(*) FILTER (WHERE status = 'failed')::int as failed,
COUNT(*) FILTER (WHERE status = 'running')::int as running,
COALESCE(SUM(size_bytes) FILTER (WHERE status = 'success'), 0) as total_size,
MAX(started_at) as last_backup
FROM backup_logs
GROUP BY backup_type
ORDER BY total DESC")).ToList();
// By destination
dashboard.ByDestination = (await connection.QueryAsync<DestinationStat>(@"
SELECT
destination,
COUNT(*)::int as total,
COUNT(*) FILTER (WHERE status = 'success')::int as successful,
COUNT(*) FILTER (WHERE status = 'failed')::int as failed,
COALESCE(SUM(size_bytes) FILTER (WHERE status = 'success'), 0) as total_size,
MAX(started_at) as last_backup
FROM backup_logs
GROUP BY destination
ORDER BY total DESC")).ToList();
// By host
dashboard.ByHost = (await connection.QueryAsync<HostStat>(@"
SELECT
COALESCE(hostname, 'unknown') as hostname,
COUNT(*)::int as total,
COUNT(*) FILTER (WHERE status = 'success')::int as successful,
COUNT(*) FILTER (WHERE status = 'failed')::int as failed,
COUNT(*) FILTER (WHERE status = 'running')::int as running,
MAX(started_at) as last_backup,
(SELECT script_version FROM backup_logs b2
WHERE COALESCE(b2.hostname, 'unknown') = COALESCE(backup_logs.hostname, 'unknown')
ORDER BY started_at DESC LIMIT 1) as script_version
FROM backup_logs
GROUP BY COALESCE(hostname, 'unknown')
ORDER BY total DESC")).ToList();
// Top errors
dashboard.TopErrors = (await connection.QueryAsync<ErrorStat>(@"
SELECT
COALESCE(error_code, 'UNKNOWN') as error_code,
COUNT(*)::int as count,
MAX(started_at) as last_occurrence,
(SELECT error_message FROM backup_logs b2
WHERE COALESCE(b2.error_code, 'UNKNOWN') = COALESCE(backup_logs.error_code, 'UNKNOWN')
AND b2.status = 'failed'
ORDER BY started_at DESC LIMIT 1) as last_message
FROM backup_logs
WHERE status = 'failed'
GROUP BY COALESCE(error_code, 'UNKNOWN')
ORDER BY count DESC
LIMIT 5")).ToList();
// Daily stats (last 14 days)
dashboard.DailyStats = (await connection.QueryAsync<DailyStat>(@"
SELECT
DATE(started_at) as date,
COUNT(*)::int as total,
COUNT(*) FILTER (WHERE status = 'success')::int as successful,
COUNT(*) FILTER (WHERE status = 'failed')::int as failed,
COALESCE(SUM(size_bytes) FILTER (WHERE status = 'success'), 0) as total_size
FROM backup_logs
WHERE started_at > NOW() - INTERVAL '14 days'
GROUP BY DATE(started_at)
ORDER BY date DESC")).ToList();
// Running now
dashboard.RunningNow = (await connection.QueryAsync<BackupLog>(@"
SELECT * FROM backup_logs
WHERE status = 'running'
ORDER BY started_at DESC")).ToList();
// Recent successes
dashboard.RecentSuccesses = (await connection.QueryAsync<BackupLog>(@"
SELECT * FROM backup_logs
WHERE status = 'success'
ORDER BY started_at DESC
LIMIT 5")).ToList();
// Recent failures
dashboard.RecentFailures = (await connection.QueryAsync<BackupLog>(@"
SELECT * FROM backup_logs
WHERE status = 'failed'
ORDER BY started_at DESC
LIMIT 5")).ToList();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error fetching backup dashboard");
}
return dashboard;
}
public async Task<bool> TestConnectionAsync()
{
try
{
await using var connection = new NpgsqlConnection(_connectionString);
await connection.OpenAsync();
return true;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Could not connect to backup database");
return false;
}
}
}