Files
justmeandClaude Sonnet 5.5 7274390aa1
ci/woodpecker/push/ci Pipeline failed
ci/woodpecker/push/web-build Pipeline failed
ci/woodpecker/push/dev-image-api unknown status
perf(host): store avatars as content-addressed files and serve them from disk
Space info now carries avatar_version instead of the inlined thumbnail; the worker fetches the thumbnail once per version. Legacy base64 rows are migrated to files on startup, and the daily backup uploads avatar files.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> [CI]
2026-10-19 14:16:50 +08:00

52 lines
2.4 KiB
C#

using Microsoft.Data.Sqlite;
using WebDrop.Core;
using WebDrop.Storage;
namespace WebDrop.Core.Tests;
public sealed class AvatarFilesTests : IDisposable
{
private readonly string _dir = Path.Combine(Path.GetTempPath(), "wd-avatar-" + Guid.NewGuid().ToString("N"));
public void Dispose() { SqliteConnection.ClearAllPools(); try { Directory.Delete(_dir, true); } catch (IOException) { } }
private static string Gif(byte fill) => "data:image/gif;base64," + Convert.ToBase64String([(byte)'G', (byte)'I', (byte)'F', fill, fill, fill, fill, fill]);
[Fact]
public void SavedAvatarsAreContentAddressedFilesAndResolveBack()
{
var files = new AvatarFiles(Path.Combine(_dir, "a"));
string first = files.Save(Gif(1)), again = files.Save(Gif(1)), other = files.Save(Gif(2));
Assert.Equal(first, again);
Assert.NotEqual(first, other);
var blob = files.Resolve(first)!;
Assert.Equal("image/gif", blob.Mime);
Assert.Equal(8, new FileInfo(blob.Path).Length);
Assert.Null(files.Resolve("../../etc/passwd"));
Assert.Null(files.Resolve(new string('0', 64) + ".gif"));
}
[Fact]
public void LegacyDataUrlRowsAreMovedToFilesOnOpenAndOrphansAreSwept()
{
string db = Path.Combine(_dir, "v2", "webdrop.db");
var id = Id16.FromBytes(Enumerable.Range(1, 16).Select(i => (byte)i).ToArray());
using (var store = new SqliteChannelStore(db)) { }
using (var conn = new SqliteConnection($"Data Source={db}"))
{
conn.Open();
using var cmd = conn.CreateCommand();
cmd.CommandText = "INSERT INTO channel_avatars(space_id, avatar, thumb) VALUES ($c, $a, $t)";
cmd.Parameters.AddWithValue("$c", id.ToArray()); cmd.Parameters.AddWithValue("$a", Gif(3)); cmd.Parameters.AddWithValue("$t", Gif(4));
cmd.ExecuteNonQuery();
}
string avatars = Path.Combine(_dir, "v2", "avatars", "space");
File.WriteAllBytes(Path.Combine(avatars, "orphan.gif"), [1]);
using var reopened = new SqliteChannelStore(db);
var (full, thumb) = reopened.Read(tx => (tx.GetChannelAvatar(id, thumb: false), tx.GetChannelAvatar(id, thumb: true)));
Assert.NotNull(full); Assert.NotNull(thumb);
Assert.NotEqual(full!.Hash, thumb!.Hash);
Assert.False(File.Exists(Path.Combine(avatars, "orphan.gif")));
Assert.Equal(2, Directory.GetFiles(avatars).Length);
}
}