- 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
133 lines
6.7 KiB
C#
133 lines
6.7 KiB
C#
using System.Text.Json.Serialization;
|
|
|
|
namespace WebDrop.Core;
|
|
|
|
/// <summary>服务端限制,由配置绑定。</summary>
|
|
public sealed class ServerLimits
|
|
{
|
|
public int MaxPayloadBytes { get; set; } = (1 << 20) + 16;
|
|
public int MaxFramesPerItem { get; set; } = 65_536;
|
|
public long MaxChannelBytes { get; set; } = 1L << 30;
|
|
public long MaxWorkspaceBytes { get; set; } = 1L << 40;
|
|
public long MaxWorkspaceChannelBytes { get; set; } = 10L << 30;
|
|
/// <summary>最大账号成员数。属性名和 info 字段保持旧线格式兼容。</summary>
|
|
public int MaxMembers { get; set; } = 64;
|
|
public int MaxVisitors { get; set; } = 10_000;
|
|
public int MinTtlSeconds { get; set; } = 60;
|
|
public int MaxTtlSeconds { get; set; } = 604_800;
|
|
public int MaxItemsLimit { get; set; } = 10_000;
|
|
public int InlineThresholdBytes { get; set; } = 64 * 1024;
|
|
public int DownloadGraceSeconds { get; set; } = 300;
|
|
public int TombstoneRetentionSeconds { get; set; } = 604_800;
|
|
}
|
|
|
|
public static class ServerSettingChoices
|
|
{
|
|
public static readonly int[] FileMaxDownloads = [0, 1, 3, 5, 10, 20];
|
|
public static bool IsFileMaxDownloadsAllowed(long value, int currentDefault) => value == currentDefault
|
|
|| value is >= int.MinValue and <= int.MaxValue && Array.IndexOf(FileMaxDownloads, (int)value) >= 0;
|
|
}
|
|
|
|
public sealed class ChannelSettings
|
|
{
|
|
[JsonPropertyName("join_policy")] public JoinPolicy JoinPolicy { get; set; } = JoinPolicy.Open;
|
|
[JsonPropertyName("name")] public string? Name { get; set; }
|
|
/// <summary>成员人数上限;线字段名保留为 seats 以兼容已有频道设置。</summary>
|
|
[JsonPropertyName("max_members")] public int MaxMembers { get; set; } = 2;
|
|
[JsonPropertyName("members_can_upload")] public bool MembersCanUpload { get; set; } = true;
|
|
[JsonPropertyName("delete_policy")] public DeletePolicy DeletePolicy { get; set; } = DeletePolicy.Own;
|
|
[JsonPropertyName("ttl_seconds")] public int TtlSeconds { get; set; } = 86_400;
|
|
[JsonPropertyName("max_items")] public int MaxItems { get; set; } = 1000;
|
|
[JsonPropertyName("file_max_downloads")] public int FileMaxDownloads { get; set; } = 10;
|
|
[JsonPropertyName("ui_restrict_copy_download")] public bool UiRestrictCopyDownload { get; set; }
|
|
|
|
public void Validate(ServerLimits limits)
|
|
{
|
|
if (!Enum.IsDefined(JoinPolicy)) throw new WebDropException(ErrorCode.BadRequest);
|
|
if (Name is { } name && (name.Length > 48 || name.Any(char.IsControl))) throw new WebDropException(ErrorCode.BadRequest);
|
|
if (MaxMembers < 1 || MaxMembers > limits.MaxMembers) throw new WebDropException(ErrorCode.BadRequest);
|
|
if (TtlSeconds < limits.MinTtlSeconds || TtlSeconds > limits.MaxTtlSeconds) throw new WebDropException(ErrorCode.BadRequest);
|
|
if (MaxItems < 1 || MaxItems > limits.MaxItemsLimit) throw new WebDropException(ErrorCode.BadRequest);
|
|
if (FileMaxDownloads < 0) throw new WebDropException(ErrorCode.BadRequest);
|
|
}
|
|
}
|
|
|
|
public sealed class ChannelSettingsPatch
|
|
{
|
|
[JsonPropertyName("join_policy")] public JoinPolicy? JoinPolicy { get; init; }
|
|
[JsonPropertyName("join_password")] public string? JoinPassword { get; init; }
|
|
[JsonPropertyName("name")] public string? Name { get; init; }
|
|
[JsonPropertyName("max_members")] public int? MaxMembers { get; init; }
|
|
[JsonPropertyName("members_can_upload")] public bool? MembersCanUpload { get; init; }
|
|
[JsonPropertyName("delete_policy")] public DeletePolicy? DeletePolicy { get; init; }
|
|
[JsonPropertyName("ttl_seconds")] public int? TtlSeconds { get; init; }
|
|
[JsonPropertyName("max_items")] public int? MaxItems { get; init; }
|
|
[JsonPropertyName("file_max_downloads")] public int? FileMaxDownloads { get; init; }
|
|
[JsonPropertyName("ui_restrict_copy_download")] public bool? UiRestrictCopyDownload { get; init; }
|
|
/// <summary>空间头像 data URL;不传表示不改,空字符串表示移除。只有空间主能改。</summary>
|
|
[JsonPropertyName("avatar")] public string? Avatar { get; init; }
|
|
/// <summary>空间头像的有损小图(data URL):空间列表、标题默认只分发它,原图在预览时才下载</summary>
|
|
[JsonPropertyName("avatar_thumb")] public string? AvatarThumb { get; init; }
|
|
|
|
public ChannelSettings ApplyTo(ChannelSettings s) => new()
|
|
{
|
|
JoinPolicy = JoinPolicy ?? s.JoinPolicy,
|
|
Name = Name is null ? s.Name : string.IsNullOrWhiteSpace(Name) ? null : Name.Trim(),
|
|
MaxMembers = MaxMembers ?? s.MaxMembers,
|
|
MembersCanUpload = MembersCanUpload ?? s.MembersCanUpload,
|
|
DeletePolicy = DeletePolicy ?? s.DeletePolicy,
|
|
TtlSeconds = TtlSeconds ?? s.TtlSeconds,
|
|
MaxItems = MaxItems ?? s.MaxItems,
|
|
FileMaxDownloads = FileMaxDownloads ?? s.FileMaxDownloads,
|
|
UiRestrictCopyDownload = UiRestrictCopyDownload ?? s.UiRestrictCopyDownload,
|
|
};
|
|
}
|
|
|
|
public sealed class ShareSettings
|
|
{
|
|
[JsonPropertyName("ttl_seconds")] public int TtlSeconds { get; set; } = 86_400;
|
|
[JsonPropertyName("max_visitors")] public int MaxVisitors { get; set; }
|
|
[JsonPropertyName("file_max_downloads")] public int FileMaxDownloads { get; set; } = 10;
|
|
|
|
public void Validate(ServerLimits limits)
|
|
{
|
|
if (TtlSeconds < limits.MinTtlSeconds || TtlSeconds > limits.MaxTtlSeconds
|
|
|| MaxVisitors < 0 || MaxVisitors > limits.MaxVisitors
|
|
|| FileMaxDownloads < 0)
|
|
throw new WebDropException(ErrorCode.BadRequest);
|
|
}
|
|
}
|
|
|
|
public sealed class ShareSettingsPatch
|
|
{
|
|
[JsonPropertyName("ttl_seconds")] public int? TtlSeconds { get; init; }
|
|
[JsonPropertyName("max_visitors")] public int? MaxVisitors { get; init; }
|
|
[JsonPropertyName("file_max_downloads")] public int? FileMaxDownloads { get; init; }
|
|
|
|
public ShareSettings ApplyTo(ShareSettings s) => new()
|
|
{
|
|
TtlSeconds = TtlSeconds ?? s.TtlSeconds,
|
|
MaxVisitors = MaxVisitors ?? s.MaxVisitors,
|
|
FileMaxDownloads = FileMaxDownloads ?? s.FileMaxDownloads,
|
|
};
|
|
}
|
|
|
|
/// <summary>创建接口与 /info 使用同一个配置来源;旧存储对象的字段默认值不作迁移。</summary>
|
|
public static class CreationDefaults
|
|
{
|
|
public const ChannelMode ChannelMode = WebDrop.Core.ChannelMode.Enc;
|
|
|
|
public static ChannelSettings Channel(ServerLimits limits) => new()
|
|
{
|
|
JoinPolicy = JoinPolicy.Invite,
|
|
MaxMembers = Math.Min(10, limits.MaxMembers),
|
|
TtlSeconds = Math.Clamp(86_400, limits.MinTtlSeconds, limits.MaxTtlSeconds),
|
|
MaxItems = Math.Min(1000, limits.MaxItemsLimit),
|
|
};
|
|
public static ShareSettings Share(ServerLimits limits) => new()
|
|
{
|
|
TtlSeconds = Math.Clamp(86_400, limits.MinTtlSeconds, limits.MaxTtlSeconds),
|
|
MaxVisitors = Math.Min(10, limits.MaxVisitors),
|
|
};
|
|
}
|