261 lines
12 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|