- 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
101 lines
3.9 KiB
C#
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;
|
|
}
|
|
}
|