Files
WebDrop/server/src/WebDrop.Core/Common/Id16.cs
T
justme 72a5e5b839 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
2026-10-17 15:25:16 +08:00

101 lines
3.9 KiB
C#

using System.Buffers.Binary;
using System.Buffers.Text;
namespace WebDrop.Core;
/// <summary>16 字节标识(频道 id、条目 id),可作字典键。</summary>
public readonly struct Id16 : IEquatable<Id16>
{
public const int Size = 16;
private const int Base64Length = 22;
private readonly ulong _hi;
private readonly ulong _lo;
private Id16(ulong hi, ulong lo)
{
_hi = hi;
_lo = lo;
}
public static Id16 FromBytes(ReadOnlySpan<byte> bytes)
{
if (bytes.Length != Size) throw new ArgumentException("Id16 需要 16 字节", nameof(bytes));
return new Id16(BinaryPrimitives.ReadUInt64BigEndian(bytes), BinaryPrimitives.ReadUInt64BigEndian(bytes[8..]));
}
/// <summary>只接受规范的 22 字符 base64url,拒绝同一值的非规范编码。</summary>
/// <summary>32 个十六进制字符(文件服务和 blob 键里的写法)</summary>
public static bool TryParseHex(string? hex, out Id16 id)
{
id = default;
if (hex is not { Length: 32 }) return false;
try { id = FromBytes(Convert.FromHexString(hex)); return true; }
catch (FormatException) { return false; }
}
public static bool TryParse(ReadOnlySpan<char> text, out Id16 id)
{
id = default;
if (text.Length != Base64Length) return false;
Span<byte> buf = stackalloc byte[Size];
if (!Codec.TryDecodeExact(text, buf)) return false;
id = FromBytes(buf);
return true;
}
public void WriteTo(Span<byte> destination)
{
BinaryPrimitives.WriteUInt64BigEndian(destination, _hi);
BinaryPrimitives.WriteUInt64BigEndian(destination[8..], _lo);
}
public byte[] ToArray()
{
var bytes = new byte[Size];
WriteTo(bytes);
return bytes;
}
public string ToBase64Url() => Codec.Encode(ToArray());
/// <summary>小写十六进制。用于文件路径,避免大小写不敏感的文件系统造成不同 id 冲突。</summary>
public string ToHex() => Convert.ToHexStringLower(ToArray());
public bool Equals(Id16 other) => _hi == other._hi && _lo == other._lo;
public override bool Equals(object? obj) => obj is Id16 other && Equals(other);
public override int GetHashCode() => HashCode.Combine(_hi, _lo);
public static bool operator ==(Id16 a, Id16 b) => a.Equals(b);
public static bool operator !=(Id16 a, Id16 b) => !a.Equals(b);
public override string ToString() => ToBase64Url();
}
/// <summary>base64url(无填充)编解码,解码要求规范编码。</summary>
public static class Codec
{
public static string Encode(ReadOnlySpan<byte> bytes) => Base64Url.EncodeToString(bytes);
/// <summary>解码为恰好 destination.Length 字节,且重新编码后必须与输入逐字符一致。</summary>
public static bool TryDecodeExact(ReadOnlySpan<char> text, Span<byte> destination)
{
// 注意:Base64Url.TryDecodeFromChars 遇到非法输入会抛 FormatException,这里用返回状态码的版本
var status = Base64Url.DecodeFromChars(text, destination, out int consumed, out int written, isFinalBlock: true);
if (status != System.Buffers.OperationStatus.Done || consumed != text.Length || written != destination.Length)
return false;
Span<char> check = stackalloc char[Base64Url.GetEncodedLength(destination.Length)];
return Base64Url.TryEncodeToChars(destination, check, out int n)
&& n == text.Length
&& check[..n].SequenceEqual(text);
}
public static bool TryDecodeExact(string? text, int length, out byte[] bytes)
{
bytes = [];
if (string.IsNullOrEmpty(text) || text.Length != Base64Url.GetEncodedLength(length)) return false;
var buf = new byte[length];
if (!TryDecodeExact(text.AsSpan(), buf)) return false;
bytes = buf;
return true;
}
}