跳到主要内容
版本:3.1

WebDAV 文件存储(Aegis.FileManager.WebDav)

Aegis.FileManager.WebDav 是 Aegis.FileManager 的 WebDAV 协议存储实现,对接 nginx 等 WebDAV 静态资源站。零第三方依赖:纯 HttpClient 发请求,System.Xml.Linq 解析 PROPFIND 响应,不引任何 SDK。

StorageKey(ObjectKey)的拼装规则属于业务领域,详见 文件管理 — 核心概念:业务自拼 Key。本页只讲 WebDAV 后端的接入与配置。

组件概览​

字段说明
组件名称WebDAV 文件存储
真实类库Aegis.FileManager.WebDav
最新版本3.1.4-preview4
组件定位对接 nginx 等 WebDAV 静态资源站的文件管理实现
引入方式安装 NuGet,Component.deps.json 声明 WebDavFileManager(或 Startup 调用 AddWebDavFileManager)
核心能力IFileManager 实现、IFileStorage 集合管理、服务器端 COPY(自动降级流式复制)、双地址内外网分离、诊断三件套
主要配置WebDavStorage
典型配套文件管理基础层(Aegis.FileManager)、诊断组件(Aegis.Diagnostics)

什么时候要用它​

适合场景:

  • 文件已经放在(或打算放在)一台 nginx 静态资源站上,应用直接读写它
  • 已有基础设施只有 WebDAV,没有 S3、也没法挂 NAS
  • 需要 nginx 天然的静态资源服务能力:直连 URL、gzip、缓存头、限流都由 nginx 配置承担
  • 内网上传下载、外网只读访问的双平面部署(配 PublicBaseUrl)

不适合场景:

  • 需要 Presigned URL、细粒度权限、版本控制的对象存储能力(用 S3 对象存储)
  • 应用与存储在同一台机器、直接读写本地磁盘(用 NAS 本地存储 更简单,没有网络开销)

最小可运行路径​

第一步:准备 nginx 服务端​

WebDAV 不是 nginx 默认全开的。最低可用(上传 / 下载 / 删除 / 存在性)只需要官方镜像自带的 ngx_http_dav_module;列举、集合管理、文件元数据还要 PROPFIND,那需要 nginx-dav-ext-module(官方镜像不带)。完整配置见下方 nginx 服务端配置要求 一节,照抄即可。

第二步:安装组件​

dotnet add package Aegis.FileManager.WebDav

第三步:准备配置并注册​

appsettings.json:

{
"WebDavStorage": {
"BaseUrl": "http://10.16.94.204:8080/",
"UserName": "admin",
"Password": "admin123",
"DefaultGroup": "",
"AutoCreateGroup": true,
"EnableMd5Calculation": true,
"TimeoutSeconds": 100
}
}

注册推荐走 Component.deps.json 声明(demo/Aegis.WebDavDemo/Component.deps.json 就是这个用法,配套 Diagnostics 让启动期巡检可见):

{
"Components": {
"Load": [
"Cors",
"Swagger",
"WebDavFileManager",
"Diagnostics"
]
}
}

声明了组件但缺 WebDavStorage 配置节时,启动横幅报 Failure(附修复指引),不再静默跳过。

也可以走 Startup 手动注册(两条路径二选一,不要同时用,见单注入守卫):

using Aegis.FileManager.WebDav;
using Aegis.FileManager.WebDav.Utils;

services.AddWebDavFileManager(ConfigManager.Get<WebDavOptions>("WebDavStorage"));

第四步:在业务里使用​

public class AttachmentService
{
private readonly IFileManager _fileManager;

public AttachmentService(IFileManager fileManager) => _fileManager = fileManager;

public async Task<FileUploadResult> Upload(string fileName, Stream stream, string group = "attachments")
{
return await _fileManager.UploadFileAsync(fileName, stream, group);
}

public Task<string> GetPublicUrl(string fileName, string group = "attachments")
{
return _fileManager.GetFileUriAsync(fileName, group);
}
}
和 NAS、S3 一样受单注入守卫约束

AddWebDavFileManager 同时注册 IFileManager 与 IFileStorage。3.1.4-preview4 起单注入守卫生效:IFileManager 已注册时第二个 AddXxxFileManager 抛 InvalidOperationException,多后端不能再同时注册。

配置项说明(WebDavOptions)​

配置项类型默认值说明
BaseUrlstring""必填。WebDAV 服务根地址,如 http://10.16.94.204:8080/ 或 http://host/dav/。所有数据操作(PUT / GET / DELETE / COPY / MKCOL / PROPFIND)基于此地址
PublicBaseUrlstring?null对外访问基地址。配置后 GetFileUriAsync 返回的直连 URL 基于它拼接;留空时与 BaseUrl 相同。典型用法:内网地址上传下载、外网地址对外提供访问,对齐 S3 的双 Endpoint
UserNamestring?nullBasic 认证用户名,对应 nginx auth_basic。与 Password 均非空时启用
Passwordstring?nullBasic 认证密码
DefaultGroupstring?null默认组名。groupName 参数为空时回退到此值(对齐 S3 的 DefaultBucket);留空表示直接使用根集合
AutoCreateGroupboolfalse上传前逐级 MKCOL 创建缺失的祖先集合。服务端配了 create_full_put_path on 时可以关掉,两边留一个就够
EnableMd5CalculationbooltrueGetFileInfoAsync 是否下载文件内容计算 MD5。见下方提示
TimeoutSecondsint100HTTP 请求超时秒数。设为 0 表示不限制,适合大文件慢传输场景
EnableMd5Calculation 为什么默认开、又为什么建议你考虑关

WebDAV 服务的 ETag 不是内容 MD5。nginx 的 ETag 是 mtime-size 组合,内容变了 mtime 才变,拿它当 MD5 用会出错。所以 FileObject.ContentMd5 只能在客户端算:启用时 GetFileInfoAsync 会额外发起一次 GET 把文件完整下载下来。

小文件无所谓。大文件、高频调用 GetFileInfoAsync 的场景,每次都全量下载一遍代价不小,建议关掉这个开关,业务侧用别的方式校验完整性。

内外网分离配置示例​

后端在内网直连 nginx 写文件,浏览器 / App 从公网域名读文件:

{
"WebDavStorage": {
"BaseUrl": "http://10.16.94.204:8080/",
"PublicBaseUrl": "https://static.example.com/",
"UserName": "admin",
"Password": "admin123",
"AutoCreateGroup": true
}
}
操作走哪个地址URL 形态
上传 / 下载 / 删除 / 列举http://10.16.94.204:8080(内网直连)—
GetFileUriAsync 返回值https://static.example.com(公网)https://static.example.com/attachments/report.pdf

和 S3 双 Endpoint 的区别:WebDAV 没有 presigned 标准,GetFileUriAsync 返回的是不含凭据的直连 URL,公网一侧的访问控制完全靠服务端配置(auth_basic、IP allowlist、只读限制等)。

反向代理子路径配置示例​

nginx 把 WebDAV 挂在 /dav/ 子路径下时,BaseUrl 带上路径部分即可:

{
"WebDavStorage": {
"BaseUrl": "http://static.example.com/dav/",
"UserName": "admin",
"Password": "admin123"
}
}

地址尾部的 / 写不写都行,框架统一规整为以 / 结尾后再拼接相对路径(不补的话相对路径拼接会把最后一段当文件名替换掉,这是 WebDavSession 在构造时处理掉的)。

groupName 的职责​

groupName 决定文件存到服务端的哪个目录(WebDAV 术语叫 collection),不参与 StorageKey 的拼装。

groupName 参数实际目录说明
""(空字符串)DefaultGroup 配置值未配置 DefaultGroup 时直接使用根集合
"attachments"{BaseUrl}/attachments/服务端一个真实目录
"emr/202509"{BaseUrl}/emr/202509/支持多级,AutoCreateGroup=true 时逐级创建

目录名没有 S3 Bucket 那样的命名约束,但它会成为 URL 的一部分,避开空格和特殊字符能省很多麻烦。

nginx 服务端配置要求​

能力矩阵​

先分清 nginx 的两个 DAV 模块,这决定了你的服务端能做什么:

能力IFileManager / IFileStorage 方法依赖官方 Docker 镜像
上传UploadFileAsyncdav_methods PUT自带
下载GetFileStreamAsync / GetFileBytesAsync / GetFileAsync普通静态文件 GET自带
删除DeleteFileAsyncdav_methods DELETE自带
存在性DoesFileExistAsyncHEAD自带
建目录 / 删除目录CreateGroupAsync / DeleteGroupAsyncdav_methods MKCOL(DELETE 删目录)自带
服务器端复制CopyFileAsyncdav_methods COPY MOVE自带
列举ListFilesAsync / ListGroupsAsyncdav_ext_methods PROPFIND不带,需自编译
文件元数据GetFileInfoAsync / GetGroupInfoAsync / GroupExistsAsyncdav_ext_methods PROPFIND不带,需自编译

也就是说:只做上传下载删除,官方 nginx 镜像开箱即用;要用列举和元数据,得装 nginx-dav-ext-module 并在配置里声明 dav_ext_methods。不想自编译的话,换 Apache、rclone serve webdav 或 bytemark/webdav 这类自带完整 WebDAV 的服务端也一样能对接。

最佳实践配置(可直接照抄)​

下面这份配置把动词开关、路径映射、权限、认证、大小上限一次配齐,并附上每一行为什么要这么写。排查期建议 autoindex on,稳定后再关:

server {
listen 8080;
server_name _;

location /dav/ {
# ① 路径映射。alias 会"替换"location 前缀:URL /dav/a.txt → /data/dav/a.txt
# 若改用 root,语义是"拼上"location 名:URL /dav/a.txt → {root}/dav/a.txt,
# root 必须指到 /data(dav 的父目录)。两者写反是最常见的 404/409 来源
alias /data/dav/;

# ② 基础 DAV 动词(ngx_http_dav_module,官方 nginx 自带)
dav_methods PUT DELETE MKCOL COPY MOVE;
# PUT 落到不存在的深层路径时自动逐级建父目录(客户端 AutoCreateGroup 的服务端版,开一个就够)
create_full_put_path on;
# nginx 新建文件的权限。默认是 user:rw(600),其他进程/用户将无法读取这些文件
dav_access user:rw group:rw all:r;

# ③ 列举与元数据(需 nginx-dav-ext-module,官方镜像不带,需自编译)
dav_ext_methods PROPFIND OPTIONS;

# ④ 上传大小上限:nginx 默认 1m,超限直接 413
client_max_body_size 2g;

# ⑤ 认证按方法分离:GET/HEAD/OPTIONS/PROPFIND 匿名放行(浏览器能直接读),
# 写类动词(PUT/DELETE/MKCOL/COPY/MOVE)要求 Basic 认证。
# 对应客户端配置 UserName / Password;全部匿名时整个 limit_except 块可删
limit_except GET HEAD OPTIONS PROPFIND {
auth_basic "Aegis WebDAV";
auth_basic_user_file /etc/nginx/.htpasswd;
}

# ⑥ 排查期开着:浏览器直接打开 /dav/ 能看到目录列表,路径映射对错一眼可辨
autoindex on;
}
}

对应客户端配置 BaseUrl: "http://host:8080/dav/"。

服务器侧目录准备(目录存在但 nginx 无写权限、或 nginx 建的文件其他进程读不了,都是高频翻车点):

mkdir -p /data/dav
# 属主改为 nginx worker 的运行用户(ps aux | grep "nginx: worker" 第一列确认,常见 www-data / nginx)
chown -R www-data:www-data /data/dav
chmod 775 /data/dav

# CentOS / RHEL 开着 SELinux 时还要补写标签,否则 mkdir 一律被拒:
chcon -R -t httpd_sys_rw_content_t /data/dav

# 认证文件(启用 limit_except 时):
htpasswd -c /etc/nginx/.htpasswd admin

配好后的验收三连(Windows 测试机用 PowerShell,Linux/macOS 用 curl):

# 1. 浏览器打开 http://host:8080/dav/ → 看到目录列表 = 路径映射正确(404 = 映射错位)
# 2. PUT 探测:写权限 + 动词开关
Invoke-WebRequest -Uri "http://host:8080/dav/probe.txt" -Method Put -Body "probe" -UseBasicParsing
# 3. MKCOL 探测:建目录能力
curl.exe -X MKCOL -i "http://host:8080/dav/probe-dir" # Win10 1803+ 自带 curl.exe;旧系统跳过本条
结果含义
PUT 201 / MKCOL 201通道就绪,客户端可直接接入
PUT 403目录属主不是 nginx 运行用户,回到上面的 chown
MKCOL 405dav_methods 没含 MKCOL
MKCOL 409 而 PUT 201服务端拒绝建目录但可写文件:客户端置 AutoCreateGroup: false 平铺文件即可,或依赖 create_full_put_path on
任何 401认证配置与 UserName / Password 不一致

组件异常消息里带完整的请求行与服务端响应摘要,撞到错误时把异常里的 URL 贴进浏览器即可复现现场。

接口方法与 WebDAV 动作对应关系​

IFileManager 方法WebDAV 动作说明
DoesFileExistAsyncHEAD只依赖 nginx 核心,不依赖 dav_ext
UploadFileAsync(byte[] / Stream / filePath)PUT流式发送:可 seek 的流自动带 Content-Length,否则 chunked;ContentType 由 FileTypeHelper 按扩展名推断
GetFileStreamAsyncGETResponseHeadersRead 模式,拿到响应头就返回;包装流随 Dispose 释放响应
GetFileBytesAsyncGET完整读为字节数组
GetFileAsyncGET下载到本地路径(目录 / 完整路径的识别规则与 S3、Nas 一致)
GetFileInfoAsyncPROPFIND Depth:0服务端 405(无 dav_ext)时自动降级 HEAD;MD5 见上文 EnableMd5Calculation
GetFileUriAsync—(PublicBaseUrl ?? BaseUrl) 拼接的直连 URL,不含凭据
DeleteFileAsyncDELETE404 静默忽略(幂等),与 Nas 一致
ListFilesAsyncPROPFIND Depth:1仅直接子文件,非递归(与 Nas EnumerateFiles 语义一致);prefix 为客户端过滤;分页 token 是上一页最后一个文件名
CopyFileAsyncCOPY + Destination + Overwrite:T服务器端复制,不消耗应用带宽;405 / 501 / 502 时自动降级 GET→PUT 流式复制

列举与分页语义​

ListFilesAsync 用 PROPFIND Depth:1 拉取集合的直接子项,过滤掉子集合后按文件名排序。分页与 Nas 后端同一套语义:

  • maxKeys:本页条数
  • IsTruncated:还有没有下一页
  • NextContinuationToken:上一页最后一个文件名,下次传入即从该名称之后继续

prefix 过滤在客户端做(WebDAV 没有 ListObjects 那种服务端前缀查询),所以单目录文件量极大时,PROPFIND 响应本身会比较大,这种情况建议按多级 group 拆目录。

COPY 的降级链​

CopyFileAsync 首选服务器端 COPY:nginx 原生支持,数据不流经应用。服务端返回 405 / 501 / 502(不支持或网关异常)时,自动降级为 GET→PUT 流式复制,中途不需要调用方做任何事,只是大文件会占用应用带宽。

IFileStorage 集合管理​

WebDAV 模块同时实现了 IFileStorage 接口,group 对应服务端一个目录:

IFileStorage 方法WebDAV 动作说明
GroupExistsAsyncPROPFIND Depth:0目录存在即 true
CreateGroupAsyncMKCOL幂等:已存在(405)同样返回 true
DeleteGroupAsyncPROPFIND + DELETE先检查目录非空才删
ListGroupsAsyncPROPFIND Depth:1根集合下过滤出子目录
GetGroupInfoAsyncPROPFIND Depth:0/1汇总直接子文件的 count 与 size
DeleteGroupAsync 只删空目录

WebDAV 协议的 DELETE 是递归语义:对着非空目录发 DELETE,整棵子树会被服务端删掉。框架为了对齐 Nas 的契约(只删空组),删除前会先 PROPFIND 检查目录是否非空,非空直接返回 false,不发 DELETE。真的要递归删除,自己拼逻辑逐层清空再删。

GetGroupInfoAsync 返回的 CreatedDate 以集合的 LastModified 近似(多数 WebDAV 服务不提供目录创建时间);Region 为 null。

诊断三件套​

需要装配 诊断组件(Aegis.Diagnostics)(deps.json 声明 Diagnostics)。未装配时三件套是无人消费的惰性单例,零开销。

诊断能力内容
健康探针探针名「文件存储 · WebDav」。对 BaseUrl 发 PROPFIND depth 0 验证可达性与认证,复用 WebDavSession.Client 的真实凭证路径(构造时已把 Basic 凭证写入默认请求头),5 秒超时。BlocksReady = true:存储不可用即摘流量。启动即巡检(dueTime 0)并周期复巡
诊断段/diag 页面「文件存储 · WebDav」段:类型、BaseUrl、PublicBaseUrl(未配置时标注)、UserName(匿名时标注)、AutoCreateGroup。密码不入段
网络分层诊断/diag 段头「一键诊断 ›」触发(端点 ?diagnose=):BaseUrl 解析 → DNS → ICMP → TCP →(https 时)TLS → 应用层 PROPFIND depth 0

与 NAS、S3 后端的差异对比​

特性NAS 后端S3 后端WebDAV 后端
存储类型本地文件系统 / NAS 挂载对象存储(S3 协议)HTTP 文件服务(nginx 等)
groupName 含义RootPath 下子目录Bucket服务端目录(collection)
GetFileUriAsync 返回值本地文件绝对路径Presigned URL(带过期时间)直连 URL(不过期、不含凭据)
对外访问控制不适用签名自带服务端配置(auth_basic / allowlist)
FileObject.ContentMd5按需计算S3 ETag(分片上传时可能不是 MD5)按需客户端计算(ETag 不可用)
文件列举目录遍历ListObjectsV2 服务端前缀PROPFIND + 客户端过滤
复制CopyFileAsync 跨组CopyObject 跨 Bucket服务器端 COPY(降级流式)
额外依赖无AWSSDK.S3无
服务端前提磁盘可写S3 兼容服务DAV 模块配置正确

选型上一句话:能挂盘就用 Nas,要对象存储能力就用 S3,文件服务已经是 nginx 且只需要简单读写,WebDAV 后端最省事。

安全注意事项​

路径遍历防护​

所有 group 与 file 的路径段在拼接 URL 前逐段校验(WebDavPathHelper.SplitSegments):统一分隔符、去空段,遇到 . 或 .. 段直接抛 ArgumentException,每段再经 Uri.EscapeDataString 编码。业务侧拼的 key 含路径穿越片段时,请求不会发出去。

凭证管理​

  • UserName / Password 是 Basic 认证的明文凭证,不要硬编码进代码或提交到版本库,用环境变量或配置中心注入
  • Basic 认证是 Base64 不是加密,生产环境要走 HTTPS,或在可信内网使用
  • GetFileUriAsync 返回的 URL 不含凭据,公网直连能不能访问、谁能访问,取决于服务端的 auth_basic 与 allowlist 配置——对外只读场景建议公网 location 单独收紧

网络边界​

  • BaseUrl 填内网地址、PublicBaseUrl 填公网地址时,注意公网一侧的写权限:默认 dav_methods 对整个 location 生效,公网 server 块只保留 GET(不配 dav_methods)就是纯只读静态站
  • 上传上限(client_max_body_size)与超时(TimeoutSeconds)按业务实际文件大小设置,避免大文件传一半被掐断

接入后怎么确认生效​

按下面几项验收:

  • 启动横幅出现「已注入服务 WebDavFileManager」,且「检查遗漏服务」不再提示「当前未加载 WebDavFileManager」
  • 启动无报错,WebDavFileManager 与 IFileStorage 可以正常注入
  • DoesFileExistAsync 对一个已上传文件返回 true(只依赖 HEAD,最轻的连通性检查)
  • UploadFileAsync 后,在服务端目录(或浏览器直接访问直连 URL)能看到文件
  • GetFileStreamAsync / GetFileBytesAsync 读回的内容与上传一致
  • ListFilesAsync 返回的列表与目录实际内容一致(这一步过了说明 PROPFIND 配置正确)
  • DeleteFileAsync 后再 DoesFileExistAsync,返回 false;对已删文件重复删除不报错
  • GetFileUriAsync 返回的 URL 在浏览器中能直接打开(配了 PublicBaseUrl 时注意从外网验证)

也可以直接跑 demo/Aegis.WebDavDemo,里面的 diagnose / upload-text / list / copy 等端点把每一步都暴露成了 HTTP 接口,配合 README 里的 curl 命令逐个打。

常见问题​

上传报 409(创建集合失败 / Conflict)​

RFC 4918 里 MKCOL 的 409 表示中间集合不存在。最常见的原因:nginx root 指向的站点根目录在服务器上根本没有创建。两个易混点:

  • nginx 的 MKCOL 从不递归创建父目录,客户端 AutoCreateGroup 的逐级 MKCOL 也只能从已存在的根目录往下建
  • create_full_put_path on 只作用于 PUT,救不了 MKCOL

处理:在服务端把根目录建出来并交给 nginx 运行用户(mkdir -p /data/webdav && chown www-data:www-data /data/webdav,路径与 nginx root 一致),同时核对 BaseUrl 里的路径段真实存在。开了 AutoCreateGroup 的上传、PUT 都会撞上这一条。

还有第三种情况:MKCOL 不通但 PUT 正常(服务端 MKCOL 实现异常或被特殊配置拒绝)。此时组件的 MKCOL 幂等逻辑(405 视为已存在)会静默跳过建目录,最终错误落在 PUT 上。绕过方式:AutoCreateGroup: false + groupName 留空让文件平铺,或服务端补 create_full_put_path on 让 PUT 自建父目录。区分方法很简单——直接 PUT 一个文件到同目录,能 201 就属于这种。

组件在异常消息里会带上完整的请求行(方法 + URL)与服务端响应体摘要,拿浏览器打开异常里的请求 URL 即可快速区分"路径错位"和"目录缺失"。

上传报 413​

nginx 的 client_max_body_size 默认只有 1m。调大它,框架侧会把 413 映射为 FileLargeException 并在异常消息里提示这一条。

调用 ListFilesAsync / GetFileInfoAsync 报"服务端不支持 PROPFIND (HTTP 405)"​

服务端没有装 nginx-dav-ext-module,或装了但没配 dav_ext_methods PROPFIND。两点都确认后重启 nginx。GetFileInfoAsync 在 405 时会自动降级为 HEAD 返回基础属性(大小、时间、类型),但 ListFilesAsync、ListGroupsAsync、GroupExistsAsync、GetGroupInfoAsync 没有替代路径,异常会直接抛出,消息里带同样的修复指引。

报 401 / 403​

  • UserName / Password 与服务端 auth_basic_user_file 里的账号对不上
  • 服务端对该 location 配了 IP allowlist,应用服务器不在名单里
  • 密码里有特殊字符时,检查配置文件是否被转义损坏

GetFileInfoAsync 很慢​

默认 EnableMd5Calculation: true,每次调用都会把文件完整下载一遍算 MD5。大文件或高频调用场景,把它关掉。

CopyFileAsync 走了网络复制而不是服务器端复制​

这是降级机制在工作:服务端对 COPY 返回了 405 / 501 / 502(常见于反代链路上游不支持、或服务端未开 dav_methods COPY)。检查 nginx 的 dav_methods 是否包含 COPY。降级路径功能等价,只是大文件会占应用带宽。

与 NAS / S3 同时注册时 IFileManager 注入冲突​

3.1.4-preview4 起单注入守卫拒绝了这种用法:第二个 AddXxxFileManager 抛 InvalidOperationException(deps 路径表现为横幅 Failure、首个生效,Startup 手动路径中止启动)。一个进程只支持一个默认 IFileManager,双后端需求现阶段在业务层自行编排或提 issue 跟踪多注入方案。升级后报「IFileManager 已注册」就删掉多余的注册。

配套阅读​