using System.IO.Compression; using Microsoft.Data.Sqlite; using Microsoft.Extensions.DependencyInjection; using static WebDrop.Api.Tests.ServerFixture; namespace WebDrop.Api.Tests; /// 主后端每天把数据库快照压缩后传到外部存储(S3 协议),按保留期清理旧的。 public sealed class BackupTests : IAsyncLifetime { private readonly FakeS3 _s3 = new(); private ServerFixture _server = null!; public async Task InitializeAsync() { await _s3.StartAsync(); _server = ServerFixture.WithSettings(new() { ["WebDrop:Backup:S3:Endpoint"] = _s3.Url, ["WebDrop:Backup:S3:AccessKey"] = FakeS3.AccessKey, ["WebDrop:Backup:S3:SecretKey"] = FakeS3.SecretKey, ["WebDrop:Backup:S3:BasePath"] = "webdrop", ["WebDrop:Backup:S3:Bucket"] = FakeS3.Bucket, ["WebDrop:Backup:RetainDays"] = "30", }); await _server.InitializeAsync(); } public async Task DisposeAsync() { await _server.DisposeAsync(); await _s3.DisposeAsync(); } [Fact] public async Task TheSnapshotIsAConsistentCompressedDatabaseAndOldOnesAreRetired() { var ch = await _server.CreateAsync(); var worker = _server.App.Services.GetServices().OfType().Single(); // 40 天前做的一份,和今天的一份 var old = await worker.RunOnceAsync(DateTime.Now.AddDays(-40), CancellationToken.None); Assert.Equal(0, old.Purged); Assert.True(_s3.Files.ContainsKey($"webdrop/chat/{old.Name}")); var today = await worker.RunOnceAsync(DateTime.Now, CancellationToken.None); Assert.Equal(1, today.Purged); // 超过 30 天的那份被清掉 Assert.False(_s3.Files.ContainsKey($"webdrop/chat/{old.Name}")); Assert.True(_s3.Files.ContainsKey($"webdrop/chat/{today.Name}")); // 备份能还原成数据库,里面有刚建的频道和账号 string path = Path.Combine(Path.GetTempPath(), "restore-" + Guid.NewGuid().ToString("N") + ".db"); try { using (var input = new MemoryStream(_s3.Files[$"webdrop/chat/{today.Name}"])) using (var zip = new GZipStream(input, CompressionMode.Decompress)) using (var output = File.Create(path)) zip.CopyTo(output); using var conn = new SqliteConnection($"Data Source={path};Mode=ReadOnly"); conn.Open(); using var count = conn.CreateCommand(); count.CommandText = "SELECT (SELECT COUNT(*) FROM channels), (SELECT COUNT(*) FROM accounts)"; using var reader = count.ExecuteReader(); Assert.True(reader.Read()); Assert.True(reader.GetInt64(0) >= 1 && reader.GetInt64(1) >= 2); } finally { SqliteConnection.ClearAllPools(); try { File.Delete(path); } catch (IOException) { } } // 本地不留临时文件 Assert.Empty(Directory.EnumerateFiles(Path.Combine(_server.DataDir, "v2", "backup-tmp"))); } }