add new databank service for backup logic
This commit is contained in:
@@ -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<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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user