feat(server): split file storage into a separate file service with tickets, receipts and WebDAV backup

- new projects WebDrop.FileApi (file service), WebDrop.FileProtocol (shared tickets and receipts) and WebDrop.Sync (WebDAV)
- file content of any size goes to the file service; chat, avatars and space structure stay on the main backend
- tickets issued by the main backend, receipts by the file service, deletion queue and daily reconciliation
- daily database snapshot and file sync to external storage (OpenList/WebDAV) with a 30 day retention
- reorganize Core, Storage and Api into feature folders (namespaces unchanged)
- web client uploads file frames to the file service and reads them with tickets
This commit is contained in:
justme committed 2026-10-17 15:25:16 +08:00
1 parent d66c93fe98
commit 72a5e5b839
84 files changed
+2156 -39

No files matched your search

@@ -0,0 +1,28 @@
using WebDrop.FileProtocol;
namespace WebDrop.Core;
/// <summary>给客户端的文件服务入口:地址 + 一张短期票据</summary>
public sealed record FileGrant(string Url, string Ticket, long ExpiresAt);
/// <summary>
/// 主后端和文件服务之间的接口。关闭时(默认)所有帧仍然存在主后端自己的存储里,行为和以前完全一样;
/// 打开后,文件的内容(文件域的对象,不分大小)由客户端直接传给文件服务,主后端只登记元数据和摘要;聊天、头像、目录仍在主后端。
/// </summary>
public interface IFileGateway
{
bool Enabled { get; }
/// <summary>客户端拿到的文件服务根地址(经反向隧道对外的那个)</summary>
string PublicUrl { get; }
FileGrant Issue(FileOp op, Id16 channel, Id16 item, uint from, long nowMs);
bool TryVerifyReceipt(string? token, long nowMs, out FileReceipt receipt);
}
public sealed class NoFileGateway : IFileGateway
{
public static readonly NoFileGateway Instance = new();
public bool Enabled => false;
public string PublicUrl => "";
public FileGrant Issue(FileOp op, Id16 channel, Id16 item, uint from, long nowMs) => throw new WebDropException(ErrorCode.NotFound);
public bool TryVerifyReceipt(string? token, long nowMs, out FileReceipt receipt) { receipt = null!; return false; }
}