- 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
82 lines
4.0 KiB
C#
82 lines
4.0 KiB
C#
using System.Collections.Concurrent;
|
|
using System.Text;
|
|
using Microsoft.AspNetCore.Builder;
|
|
using Microsoft.AspNetCore.Hosting.Server;
|
|
using Microsoft.AspNetCore.Hosting.Server.Features;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace WebDrop.Api.Tests;
|
|
|
|
/// <summary>只实现备份用到的 WebDAV 子集(MKCOL、PUT、DELETE、PROPFIND Depth 1),数据放在内存里,带基本认证。</summary>
|
|
public sealed class FakeWebDav : IAsyncDisposable
|
|
{
|
|
private readonly WebApplication _app;
|
|
public ConcurrentDictionary<string, byte[]> Files { get; } = new();
|
|
public ConcurrentDictionary<string, bool> Dirs { get; } = new();
|
|
public bool Down { get; set; }
|
|
public string Url { get; private set; } = "";
|
|
public const string User = "backup", Password = "p@ss";
|
|
|
|
public FakeWebDav()
|
|
{
|
|
var builder = WebApplication.CreateSlimBuilder();
|
|
builder.Configuration["urls"] = "http://127.0.0.1:0";
|
|
builder.Logging.ClearProviders();
|
|
_app = builder.Build();
|
|
_app.Map("/dav/{**path}", async (HttpContext ctx, string? path) =>
|
|
{
|
|
if (Down) { ctx.Response.StatusCode = 503; return; }
|
|
string expected = "Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes($"{User}:{Password}"));
|
|
if (ctx.Request.Headers.Authorization != expected) { ctx.Response.StatusCode = 401; return; }
|
|
path = Uri.UnescapeDataString(path ?? "").Trim('/');
|
|
switch (ctx.Request.Method)
|
|
{
|
|
case "MKCOL":
|
|
ctx.Response.StatusCode = Dirs.TryAdd(path, true) ? 201 : 405;
|
|
break;
|
|
case "PUT":
|
|
using (var ms = new MemoryStream()) { await ctx.Request.Body.CopyToAsync(ms); Files[path] = ms.ToArray(); }
|
|
ctx.Response.StatusCode = 201;
|
|
break;
|
|
case "DELETE":
|
|
{
|
|
bool removed = Files.TryRemove(path, out _);
|
|
foreach (var key in Files.Keys.Where(k => k.StartsWith(path + "/", StringComparison.Ordinal)).ToList()) { Files.TryRemove(key, out _); removed = true; }
|
|
foreach (var key in Dirs.Keys.Where(k => k == path || k.StartsWith(path + "/", StringComparison.Ordinal)).ToList()) { Dirs.TryRemove(key, out _); removed = true; }
|
|
ctx.Response.StatusCode = removed ? 204 : 404;
|
|
break;
|
|
}
|
|
case "PROPFIND":
|
|
{
|
|
if (path.Length > 0 && !Dirs.ContainsKey(path)) { ctx.Response.StatusCode = 404; return; }
|
|
string prefix = path.Length == 0 ? "" : path + "/";
|
|
var children = Files.Keys.Concat(Dirs.Keys).Where(k => k.StartsWith(prefix, StringComparison.Ordinal) && k.Length > prefix.Length && !k[prefix.Length..].Contains('/')).Distinct().Order();
|
|
var xml = new StringBuilder("<?xml version=\"1.0\"?><d:multistatus xmlns:d=\"DAV:\">");
|
|
xml.Append($"<d:response><d:href>/dav/{path}/</d:href></d:response>");
|
|
foreach (var child in children) xml.Append($"<d:response><d:href>/dav/{child}</d:href></d:response>");
|
|
xml.Append("</d:multistatus>");
|
|
ctx.Response.StatusCode = 207;
|
|
ctx.Response.ContentType = "application/xml";
|
|
await ctx.Response.WriteAsync(xml.ToString());
|
|
break;
|
|
}
|
|
default: ctx.Response.StatusCode = 405; break;
|
|
}
|
|
});
|
|
}
|
|
|
|
public async Task StartAsync()
|
|
{
|
|
await _app.StartAsync();
|
|
Url = _app.Services.GetRequiredService<IServer>().Features.Get<IServerAddressesFeature>()!.Addresses.First().TrimEnd('/') + "/dav";
|
|
}
|
|
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
await _app.StopAsync();
|
|
await _app.DisposeAsync();
|
|
}
|
|
}
|