Files
Guestlist/Guestlist/DatabaseBackupService.cs
T
2026-05-11 23:21:04 +02:00

145 lines
4.6 KiB
C#

using MongoDB.Bson;
using MongoDB.Driver;
namespace Guestlist;
public sealed record DatabaseBackupResult(
string BackupDatabaseName,
int CollectionCount,
long DocumentCount);
public sealed class DatabaseBackupService(MongoDbContext db)
{
private const string BackupMetadataCollectionName = "backup_metadata";
private const string BackupSeparator = "_backup_";
public async Task<DatabaseBackupResult> BackupAndDropActiveDatabaseAsync(string reason)
{
var backupDatabaseName = CreateBackupDatabaseName();
var backupDatabase = db.Client.GetDatabase(backupDatabaseName);
DatabaseBackupResult backupResult;
try
{
backupResult = await CopyDatabaseAsync(db.Database, backupDatabase);
await WriteBackupMetadataAsync(backupDatabase, reason, backupResult);
}
catch
{
await db.Client.DropDatabaseAsync(backupDatabaseName);
throw;
}
await db.Client.DropDatabaseAsync(db.DatabaseName);
return backupResult with { BackupDatabaseName = backupDatabaseName };
}
public async Task<DatabaseBackupResult?> RestoreLatestDatabaseBackupAsync()
{
var backupDatabaseName = await GetLatestBackupDatabaseNameAsync();
if (backupDatabaseName is null)
{
return null;
}
var backupDatabase = db.Client.GetDatabase(backupDatabaseName);
await db.Client.DropDatabaseAsync(db.DatabaseName);
return await CopyDatabaseAsync(backupDatabase, db.Database) with
{
BackupDatabaseName = backupDatabaseName
};
}
public async Task<bool> HasAnyDatabaseBackupsAsync()
{
return await GetLatestBackupDatabaseNameAsync() is not null;
}
private async Task<string?> GetLatestBackupDatabaseNameAsync()
{
var databaseNames = await db.Client.ListDatabaseNames().ToListAsync();
var backupPrefix = CreateBackupPrefix();
return databaseNames
.Where(name => name.StartsWith(backupPrefix, StringComparison.Ordinal))
.OrderByDescending(name => name)
.FirstOrDefault();
}
private string CreateBackupDatabaseName()
{
var suffix = $"{DateTime.UtcNow:yyyyMMddHHmmss}_{Guid.NewGuid():N}"[..23];
return $"{CreateBackupPrefix(suffix.Length)}{suffix}";
}
private string CreateBackupPrefix(int suffixLength = 23)
{
var maxBaseNameLength = Math.Max(1, 63 - BackupSeparator.Length - suffixLength);
var baseName = db.DatabaseName.Length > maxBaseNameLength
? db.DatabaseName[..maxBaseNameLength]
: db.DatabaseName;
return $"{baseName}{BackupSeparator}";
}
private static async Task<DatabaseBackupResult> CopyDatabaseAsync(
IMongoDatabase sourceDatabase,
IMongoDatabase targetDatabase)
{
var collectionNames = await sourceDatabase.ListCollectionNames().ToListAsync();
var copiedCollections = 0;
long copiedDocuments = 0;
foreach (var collectionName in collectionNames.Where(IsBackupEligibleCollection))
{
var sourceCollection = sourceDatabase.GetCollection<BsonDocument>(collectionName);
var targetCollection = targetDatabase.GetCollection<BsonDocument>(collectionName);
var documents = await sourceCollection
.Find(FilterDefinition<BsonDocument>.Empty)
.ToListAsync();
if (documents.Count > 0)
{
await targetCollection.InsertManyAsync(documents);
copiedDocuments += documents.Count;
}
else
{
await targetDatabase.CreateCollectionAsync(collectionName);
}
copiedCollections++;
}
return new DatabaseBackupResult("", copiedCollections, copiedDocuments);
}
private static bool IsBackupEligibleCollection(string collectionName)
{
return collectionName != BackupMetadataCollectionName &&
!collectionName.StartsWith("system.", StringComparison.Ordinal);
}
private static async Task WriteBackupMetadataAsync(
IMongoDatabase backupDatabase,
string reason,
DatabaseBackupResult backupResult)
{
var metadata = new BsonDocument
{
["reason"] = reason,
["backedUpAt"] = DateTime.UtcNow,
["collectionCount"] = backupResult.CollectionCount,
["documentCount"] = backupResult.DocumentCount
};
await backupDatabase
.GetCollection<BsonDocument>(BackupMetadataCollectionName)
.InsertOneAsync(metadata);
}
}