-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathProgram.cs
More file actions
115 lines (100 loc) · 5.11 KB
/
Copy pathProgram.cs
File metadata and controls
115 lines (100 loc) · 5.11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
using System.Text.Json;
using DbBackupTool;
using DbBackupTool.Config;
using DbBackupTool.Providers;
// Meant to run once and exit - "weekly" (or any interval) is a Windows Task
// Scheduler / cron trigger pointed at the built exe, not a loop inside this app.
// See README for setup. Every job in appsettings.json's Jobs[] list runs
// CONCURRENTLY in this one execution (Task.WhenAll below) - one Task Scheduler
// entry can back up several unrelated databases, even different engines, at once.
// One job failing never stops or is stopped by the others.
var configPath = Path.Combine(AppContext.BaseDirectory, "appsettings.json");
if (args.Length > 0 && File.Exists(args[0])) configPath = args[0];
if (!File.Exists(configPath))
{
Console.Error.WriteLine($"Config file not found: {configPath}");
Console.Error.WriteLine("Usage: DbBackupTool.exe [path-to-appsettings.json]");
Console.Error.WriteLine("Copy appsettings.example.json to appsettings.json and fill in your job(s) first.");
return 1;
}
AppConfig config;
try
{
var json = await File.ReadAllTextAsync(configPath);
config = JsonSerializer.Deserialize<AppConfig>(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true })
?? throw new InvalidOperationException("Config file is empty.");
}
catch (Exception ex)
{
Console.Error.WriteLine($"Failed to read config at {configPath}: {ex.Message}");
return 1;
}
if (config.Jobs.Count == 0)
{
Console.Error.WriteLine("No jobs configured - add at least one entry to \"Jobs\" in appsettings.json.");
return 1;
}
using var cts = new CancellationTokenSource();
Console.CancelKeyPress += (_, e) => { e.Cancel = true; cts.Cancel(); };
var results = await Task.WhenAll(config.Jobs.Select(job => RunJobAsync(job, cts.Token)));
return results.All(ok => ok) ? 0 : 1;
static async Task<bool> RunJobAsync(BackupJob job, CancellationToken ct)
{
JobLog? log = null;
string? runDir = null;
try
{
if (string.IsNullOrWhiteSpace(job.Label))
throw new InvalidOperationException("A job is missing Label.");
if (string.IsNullOrWhiteSpace(job.BackupFolder))
throw new InvalidOperationException($"Job '{job.Label}': BackupFolder is required.");
Directory.CreateDirectory(job.BackupFolder);
log = new JobLog(job.Label, Path.Combine(job.BackupFolder, "backup.log"));
var provider = CreateProvider(job);
var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
var fileBaseName = $"{job.Label}_{timestamp}";
runDir = Path.Combine(job.BackupFolder, fileBaseName);
log.Info($"Starting {job.DbType} backup -> {runDir}");
Directory.CreateDirectory(runDir);
await provider.RunAsync(runDir, fileBaseName, log, ct);
log.Info("Backup succeeded.");
BackupRetention.Prune(job.BackupFolder, job.Label, job.RetentionCount, log);
log.Info($"Retention applied - keeping newest {job.RetentionCount}.");
return true;
}
catch (Exception ex)
{
if (log != null)
{
log.Error($"Backup FAILED: {ex.Message}");
}
else
{
// Failed before a logger could even be created (e.g. bad BackupFolder) -
// this is the only case anything goes straight to the console instead.
Console.Error.WriteLine($"[{job.Label}] Backup FAILED before logging could start: {ex.Message}");
}
// A failed run never counts toward retention or gets mistaken for a real
// backup later - remove the partial output.
try { if (runDir != null && Directory.Exists(runDir)) Directory.Delete(runDir, recursive: true); }
catch { /* best effort */ }
return false;
}
}
static IBackupProvider CreateProvider(BackupJob job) => job.DbType.Trim().ToLowerInvariant() switch
{
"oracle" => new OracleBackupProvider(job.Oracle
?? throw new InvalidOperationException($"Job '{job.Label}': Oracle config section is required when DbType is Oracle.")),
"sqlserver" => new SqlServerBackupProvider(job.SqlServer
?? throw new InvalidOperationException($"Job '{job.Label}': SqlServer config section is required when DbType is SqlServer.")),
"mysql" or "mariadb" => new MySqlBackupProvider(job.MySql
?? throw new InvalidOperationException($"Job '{job.Label}': MySql config section is required when DbType is MySql/MariaDb.")),
"postgresql" or "postgres" => new PostgreSqlBackupProvider(job.PostgreSql
?? throw new InvalidOperationException($"Job '{job.Label}': PostgreSql config section is required when DbType is PostgreSql.")),
"mongodb" or "mongo" => new MongoDbBackupProvider(job.MongoDb
?? throw new InvalidOperationException($"Job '{job.Label}': MongoDb config section is required when DbType is MongoDb.")),
"sqlite" => new SqliteBackupProvider(job.Sqlite
?? throw new InvalidOperationException($"Job '{job.Label}': Sqlite config section is required when DbType is Sqlite.")),
_ => throw new InvalidOperationException(
$"Job '{job.Label}': unknown DbType '{job.DbType}'. Expected Oracle, SqlServer, MySql, MariaDb, PostgreSql, MongoDb, or Sqlite.")
};