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();
}
}