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; /// 只实现备份用到的 WebDAV 子集(MKCOL、PUT、DELETE、PROPFIND Depth 1),数据放在内存里,带基本认证。 public sealed class FakeWebDav : IAsyncDisposable { private readonly WebApplication _app; public ConcurrentDictionary Files { get; } = new(); public ConcurrentDictionary 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.Append($"/dav/{path}/"); foreach (var child in children) xml.Append($"/dav/{child}"); xml.Append(""); 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().Features.Get()!.Addresses.First().TrimEnd('/') + "/dav"; } public async ValueTask DisposeAsync() { await _app.StopAsync(); await _app.DisposeAsync(); } }