Files
SocialPub/PrivaPub/Infrastructure/Backup/Backups.cs
T

89 lines
3.3 KiB
C#
Raw Normal View History

using Microsoft.Extensions.Options;
using MongoDB.Entities;
using PrivaPub.Domain.Media;
using PrivaPub.Models;
namespace PrivaPub.Infrastructure.Backup
{
// The server's backups, wherever they are started from: the CLI, the nightly schedule, the administrator's page.
public class Backups(IOptionsMonitor<BackupOptions> options, IOptionsMonitor<AppConfiguration> app, IMediaService media)
{
/// <summary>Where backups are kept: Backups:Root (production's /var/lib/privapub/backups), else beside the media root.</summary>
public string Root => Path.GetFullPath(options.CurrentValue.Root ?? media.Root.TrimEnd(Path.DirectorySeparatorChar) + "-backups");
public BackupOptions Options => options.CurrentValue;
public string Host => app.CurrentValue.BackendBaseAddress?.TrimEnd('/');
public BackupContext Context() => new(DB.Default.Database(), Root, media.Root, media.TrashRoot, Host, options.CurrentValue);
public Task<(BackupInfo Backup, string Error)> Create(string kind, bool dbOnly, CancellationToken token) =>
ServerBackup.Create(Context(), kind, dbOnly, token);
public List<BackupInfo> List() => ServerBackup.List(Root);
public BackupInfo Find(string id) => ServerBackup.Find(Root, id);
public Task<List<string>> Verify(string id, CancellationToken token) => ServerBackup.Verify(Root, id, token);
public bool Delete(string id) => ServerBackup.Delete(Root, id);
}
// A nightly backup at Backups:NightlyAt (UTC), or as soon as the service is up after missing it; the old ones rotated
// out as each is made.
public class BackupScheduler(Backups backups, ILogger<BackupScheduler> logger) : BackgroundService
{
static readonly TimeSpan Interval = TimeSpan.FromMinutes(10);
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
await Task.Delay(Interval, stoppingToken);
}
catch (OperationCanceledException)
{
return;
}
try
{
await RunIfDue(DateTime.UtcNow, stoppingToken);
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
logger.LogError(ex, "The nightly backup failed");
}
}
}
/// <summary>Backs up when the day's time has come and there is no nightly backup since: whether it did.</summary>
public async Task<bool> RunIfDue(DateTime now, CancellationToken token)
{
var options = backups.Options;
if (!options.Nightly || !TimeOnly.TryParse(options.NightlyAt, System.Globalization.CultureInfo.InvariantCulture, out var at)
|| !IsDue(now, at, backups.List().Where(b => b.Kind == "nightly").Select(b => b.CreatedAt)))
return false;
var (made, error) = await backups.Create("nightly", dbOnly: false, token);
if (made == default)
{
logger.LogWarning("The nightly backup was not made: {Error}", error);
return false;
}
logger.LogInformation("Nightly backup {Id}: {Collections} collections, {Files} media files", made.Id, made.Manifest.Collections.Count, made.Manifest.Media.Files);
return true;
}
/// <summary>Whether the last time of day for a nightly backup has passed with no nightly backup made since.</summary>
public static bool IsDue(DateTime now, TimeOnly at, IEnumerable<DateTime> nightlies)
{
var due = now.Date + at.ToTimeSpan();
if (now < due)
due = due.AddDays(-1);
return !nightlies.Any(made => made >= due);
}
}
}