From 5d37788a2ce42902e5c94d82d2f8aa657059b78d Mon Sep 17 00:00:00 2001 From: BeatriceAl Date: Mon, 11 May 2026 23:21:04 +0200 Subject: [PATCH] add new databank service for backup logic --- Guestlist/DatabaseBackupService.cs | 144 +++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 Guestlist/DatabaseBackupService.cs diff --git a/Guestlist/DatabaseBackupService.cs b/Guestlist/DatabaseBackupService.cs new file mode 100644 index 0000000..e77621a --- /dev/null +++ b/Guestlist/DatabaseBackupService.cs @@ -0,0 +1,144 @@ +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 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 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 HasAnyDatabaseBackupsAsync() + { + return await GetLatestBackupDatabaseNameAsync() is not null; + } + + private async Task 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 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(collectionName); + var targetCollection = targetDatabase.GetCollection(collectionName); + var documents = await sourceCollection + .Find(FilterDefinition.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(BackupMetadataCollectionName) + .InsertOneAsync(metadata); + } +}