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:
1 parent
d66c93fe98
commit
72a5e5b839
84 files changed
+2156
-39
No files matched your search
@@ -0,0 +1,128 @@
|
||||
using System.Buffers.Binary;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace WebDrop.Core;
|
||||
|
||||
public sealed class CreateChannelRequest
|
||||
{
|
||||
[JsonPropertyName("key_package")] public string? KeyPackage { get; init; }
|
||||
[JsonPropertyName("join_password")] public string? JoinPassword { get; init; }
|
||||
[JsonPropertyName("id")] public string? Id { get; init; }
|
||||
[JsonPropertyName("mode")] public ChannelMode? Mode { get; init; }
|
||||
[JsonPropertyName("settings")] public ChannelSettings? Settings { get; init; }
|
||||
[JsonPropertyName("read_hash")] public string? ReadHash { get; init; }
|
||||
[JsonPropertyName("write_hash")] public string? WriteHash { get; init; }
|
||||
[JsonPropertyName("owner_hash")] public string? OwnerHash { get; init; }
|
||||
}
|
||||
|
||||
public sealed class CreateShareRequest
|
||||
{
|
||||
[JsonPropertyName("id")] public string? Id { get; init; }
|
||||
[JsonPropertyName("read_hash")] public string? ReadHash { get; init; }
|
||||
[JsonPropertyName("owner_hash")] public string? OwnerHash { get; init; }
|
||||
[JsonPropertyName("settings")] public ShareSettings? Settings { get; init; }
|
||||
}
|
||||
|
||||
public sealed class FinishShareRequest
|
||||
{
|
||||
[JsonPropertyName("manifest_item_id")] public string? ManifestItemId { get; init; }
|
||||
}
|
||||
|
||||
public sealed class ShareInfo
|
||||
{
|
||||
[JsonPropertyName("id")] public required string Id { get; init; }
|
||||
[JsonPropertyName("state")] public required string State { get; init; }
|
||||
[JsonPropertyName("expires_at")] public required long ExpiresAt { get; init; }
|
||||
[JsonPropertyName("created_at")] public required long CreatedAt { get; init; }
|
||||
[JsonPropertyName("sealed_at")] public long? SealedAt { get; init; }
|
||||
[JsonPropertyName("manifest_item_id")] public string? ManifestItemId { get; init; }
|
||||
[JsonPropertyName("settings")] public required ShareSettings Settings { get; init; }
|
||||
[JsonPropertyName("items")] public required IReadOnlyList<ShareItemInfo> Items { get; init; }
|
||||
}
|
||||
|
||||
public sealed class ShareItemInfo
|
||||
{
|
||||
[JsonPropertyName("complete")] public bool Complete { get; init; }
|
||||
[JsonPropertyName("item_id")] public required string ItemId { get; init; }
|
||||
[JsonPropertyName("bytes")] public required long Bytes { get; init; }
|
||||
[JsonPropertyName("frames")] public required int Frames { get; init; }
|
||||
[JsonPropertyName("max_downloads")] public required long MaxDownloads { get; init; }
|
||||
[JsonPropertyName("downloads_left")] [JsonIgnore(Condition = JsonIgnoreCondition.Never)] public long? DownloadsLeft { get; init; }
|
||||
}
|
||||
|
||||
public sealed class ChannelInfo
|
||||
{
|
||||
[JsonPropertyName("can_upload")] public bool CanUpload { get; init; }
|
||||
[JsonPropertyName("can_manage_structure")] public bool CanManageStructure { get; init; }
|
||||
[JsonPropertyName("password_required")] public bool PasswordRequired { get; init; }
|
||||
[JsonPropertyName("creator_account")] public string? CreatorAccount { get; init; }
|
||||
/// <summary>空间头像(data URL,PNG/JPEG/WebP/GIF),没有设置时为 null。</summary>
|
||||
[JsonPropertyName("avatar")] public string? Avatar { get; init; }
|
||||
[JsonPropertyName("id")] public required string Id { get; init; }
|
||||
[JsonPropertyName("mode")] public required ChannelMode Mode { get; init; }
|
||||
[JsonPropertyName("settings")] public required ChannelSettings Settings { get; init; }
|
||||
/// <summary>频道成员数(账号),不统计账号设备数。</summary>
|
||||
[JsonPropertyName("members_used")] public required int MembersUsed { get; init; }
|
||||
|
||||
/// <summary>从该 seq 起的续传有效;0 < from < oldest_seq 的续传会得到 410,需从 0 全量同步。</summary>
|
||||
[JsonPropertyName("oldest_seq")] public required long OldestSeq { get; init; }
|
||||
|
||||
[JsonPropertyName("last_seq")] public required long LastSeq { get; init; }
|
||||
[JsonPropertyName("created_at")] public required long CreatedAt { get; init; }
|
||||
[JsonPropertyName("expires_at")] public required long ExpiresAt { get; init; }
|
||||
}
|
||||
|
||||
public sealed class JoinResult
|
||||
{
|
||||
[JsonPropertyName("joined_at")] public required long JoinedAt { get; init; }
|
||||
/// <summary>加入后的成员人数,同一账号的其他终端不增加此值。</summary>
|
||||
[JsonPropertyName("members_used")] public required int MembersUsed { get; init; }
|
||||
[JsonPropertyName("members_max")] public required int MembersMax { get; init; }
|
||||
}
|
||||
|
||||
public sealed class AppendResult
|
||||
{
|
||||
[JsonPropertyName("seq")] public required long Seq { get; init; }
|
||||
|
||||
/// <summary>true 为新追加(201),false 为幂等重发(200)。</summary>
|
||||
[JsonIgnore] public bool Created { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>一条日志记录的线格式:seq(u64) ‖ kind(u8) ‖ body。</summary>
|
||||
public sealed record LogRecord(long Seq, byte[] Bytes)
|
||||
{
|
||||
public static byte[] Encode(long seq, LogKind kind, ReadOnlySpan<byte> body)
|
||||
{
|
||||
var bytes = new byte[9 + body.Length];
|
||||
BinaryPrimitives.WriteInt64BigEndian(bytes, seq);
|
||||
bytes[8] = (byte)kind;
|
||||
body.CopyTo(bytes.AsSpan(9));
|
||||
return bytes;
|
||||
}
|
||||
|
||||
public static byte[] EncodeTombstone(long seq, Id16 itemId, TombstoneReason reason)
|
||||
{
|
||||
Span<byte> body = stackalloc byte[Id16.Size + 1];
|
||||
itemId.WriteTo(body);
|
||||
body[Id16.Size] = (byte)reason;
|
||||
return Encode(seq, LogKind.Tombstone, body);
|
||||
}
|
||||
|
||||
public static byte[] EncodeAnnounce(long seq, Id16 itemId, FrameFlags flags, long createdMs)
|
||||
{
|
||||
Span<byte> body = stackalloc byte[Id16.Size + 1 + 8];
|
||||
itemId.WriteTo(body);
|
||||
body[Id16.Size] = (byte)flags;
|
||||
BinaryPrimitives.WriteInt64BigEndian(body[(Id16.Size + 1)..], createdMs);
|
||||
return Encode(seq, LogKind.Announce, body);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record LogPage(IReadOnlyList<LogRecord> Records, long LastSeq);
|
||||
|
||||
/// <summary>GATED 条目拉取的一个批次:自 FromIndex 起连续的完整封装。</summary>
|
||||
public sealed record ItemChunk(IReadOnlyList<byte[]> Frames, uint NextIndex, bool Complete);
|
||||
|
||||
[JsonSerializable(typeof(ChannelSettings))]
|
||||
[JsonSerializable(typeof(ShareSettings))]
|
||||
public sealed partial class CoreJsonContext : JsonSerializerContext;
|
||||
Reference in new issue
Block a user