WebDAV 文件管理是如何实现的?从 HTTP 方法到跨存储 FileProvider 架构
在上一篇中,我们讨论了 iOS 如何通过 SMB 连接 NAS 和 Windows 共享目录。
SMB 更像是一个真正的网络文件系统协议。
这一篇我们换一个方向:
WebDAV。
很多人第一次接触 WebDAV 时会觉得它只是:
“一个能上传下载文件的 HTTP 服务。”
但实际上,WebDAV 的价值远不止 GET 和 PUT。
它提供了一套建立在 HTTP 之上的远程文件管理能力,例如:
- 列目录
- 创建文件夹
- 上传文件
- 下载文件
- 删除文件
- 移动文件
- 重命名文件
- 读取文件属性
对于文件浏览器来说,这意味着:
只要把 HTTP 请求抽象好,就可以把一个 WebDAV Server 当成远程文件来源。
整个架构可以理解为:
iPhone
│
│ WebDAV
▼
HTTP / HTTPS
│
▼
Remote Server
│
▼
Files
从这一篇开始,我们会进一步把前几篇的:
FileItem
FileProvider
FileOperation
真正应用到多种远程存储中。
1. WebDAV 到底是什么?
WebDAV 可以简单理解为:
HTTP 的文件管理扩展。
普通 HTTP 常见操作:
GET
POST
PUT
DELETE
WebDAV 在此基础上增加了一些非常适合文件系统的能力,例如:
PROPFIND
MKCOL
MOVE
COPY
PROPPATCH
LOCK
UNLOCK
于是一个普通 HTTP 服务:
URL
↓
GET
↓
Document
被扩展成:
Remote Folder
│
├── List
├── Create
├── Upload
├── Delete
├── Move
└── Metadata
这就是为什么 WebDAV 经常被用于:
- 私有云
- NAS
- 文档服务器
- 网盘挂载
- 跨平台文件访问
- 远程办公文件系统
2. WebDAV 和 SMB 有什么区别?
从用户角度看,它们都可以:
打开服务器
↓
浏览目录
↓
上传/下载文件
但底层完全不同。
SMB:
iPhone
↓
SMB Protocol
↓
NAS / Windows
WebDAV:
iPhone
↓
HTTP / HTTPS
↓
WebDAV Server
所以两者最大的区别之一是:
WebDAV 天然运行在 HTTP 体系上。
这带来几个特点:
更容易跨公网
更容易经过反向代理
更容易使用 HTTPS
更容易和 Web Server 集成
而 SMB 更常见于:
局域网
NAS
Windows Share
3. 一个 WebDAV 文件浏览器的基础流程
用户添加服务器:
Add WebDAV
↓
Server URL
↓
Username
↓
Password
↓
Connect
↓
Authenticate
↓
PROPFIND
↓
Directory Listing
例如:
https://dav.example.com/files/
认证成功以后返回:
Documents/
Photos/
Videos/
Backup/
用户点击:
Documents
客户端再次执行:
PROPFIND /files/Documents/
最终获得下一级目录。
所以 WebDAV 文件浏览器最基础的动作其实不是 GET,而是:
PROPFIND。
4. PROPFIND 是 WebDAV 的核心
普通 HTTP:
GET /Documents/
通常意味着获取一个资源。
但文件浏览器真正需要的是:
当前目录里面有哪些文件?
这就需要:
PROPFIND
例如请求:
PROPFIND /Documents/ HTTP/1.1
Depth: 1
其中:
Depth: 1
通常可以理解为:
获取当前资源以及它的直接子资源。
服务器可能返回:
<multistatus>
...
</multistatus>
里面包含:
- 文件名
- 是否目录
- 大小
- 修改时间
- Content Type
- ETag
- HTTP 状态
于是客户端可以转换成:
struct FileItem {
let name: String
let isDirectory: Bool
let size: Int64?
let modifiedDate: Date?
let location: FileLocation
}
5. WebDAV 返回的是 XML,不是漂亮的 JSON
现代 App 开发者更习惯:
{
"name": "movie.mp4"
}
但 WebDAV 很多核心响应使用:
XML。
例如概念上:
<response>
<href>/Documents/movie.mp4</href>
<propstat>
<prop>
<getcontentlength>
1073741824
</getcontentlength>
<getlastmodified>
...
</getlastmodified>
</prop>
</propstat>
</response>
所以 WebDAV Client 需要一个:
XML Parser
流程:
HTTP Response
↓
XML
↓
Parser
↓
WebDAV Resource
↓
FileItem
这个转换层非常重要。
UI 不应该直接理解 XML。
6. WebDAV Resource 应该独立建模
可以定义:
struct WebDAVResource {
let href: String
let name: String
let isCollection: Bool
let contentLength: Int64?
let contentType: String?
let modifiedDate: Date?
let etag: String?
}
然后:
WebDAVResource
↓
Mapper
↓
FileItem
这样可以把:
协议模型
和:
业务文件模型
分开。
这是非常值得坚持的一层。
7. 不要让 UI 直接处理 href
WebDAV 返回的:
href
可能是:
/files/Documents/test.pdf
也可能经过:
- URL Encoding
- Unicode
- 特殊字符
- 路径规范化
- Server Base Path
例如:
报告 2026.pdf
可能在协议层表现为编码后的 URL。
所以:
href
应该由 WebDAV Provider 处理。
UI 只看到:
name = "报告 2026.pdf"
而不是到处执行:
removingPercentEncoding
这能避免很多路径 Bug。
8. 下载文件其实就是 GET
相比目录列表,下载反而很简单。
用户点击:
movie.mp4
客户端请求:
GET /Videos/movie.mp4
数据流:
WebDAV Server
↓
HTTP Response Body
↓
Network Stream
↓
iPhone
如果只是一个小文件,可以直接读取。
但如果是:
20 GB
就又回到了第二篇的问题:
不能全部读入 Data。
9. WebDAV 大文件下载必须 Streaming
错误思路:
GET
↓
20GB Response
↓
Data
↓
Memory
正确方向:
GET
↓
HTTP Stream
↓
Chunk
↓
Temporary File
↓
Chunk
↓
Temporary File
理想关系:
20 GB Remote File
Memory ≠ 20 GB
而应该只保留:
Network Buffer
+
IO Buffer
所以:
WebDAV Provider 负责协议,FileOperationManager 负责大文件生命周期。
不要把两个职责混在一起。
10. 上传文件使用 PUT
上传通常更加直观。
例如:
local.pdf
上传到:
/Documents/local.pdf
可以使用:
PUT /Documents/local.pdf
Request Body:
File Data
整体:
Local File
↓
Stream
↓
HTTP PUT
↓
WebDAV Server
同样:
如果文件有 15GB,
不要:
File
↓
Data
↓
PUT
而应该:
FileHandle
↓
Streaming Request Body
↓
Server
11. 创建文件夹使用 MKCOL
WebDAV 有一个非常直接的方法:
MKCOL
例如用户点击:
New Folder
创建:
Projects
请求:
MKCOL /Documents/Projects/
成功后:
Documents/
└── Projects/
因此 FileProvider:
func createDirectory(
name: String,
at location: FileLocation
) async throws
在本地:
FileManager.createDirectory
在 WebDAV:
MKCOL
上层完全不用知道区别。
12. 删除文件就是 DELETE
删除:
report.pdf
请求:
DELETE /Documents/report.pdf
目录同样可以删除。
所以业务层:
func delete(_ item: FileItem) async throws
底层根据 Provider:
Local
↓
FileManager.removeItem
WebDAV
↓
HTTP DELETE
这就是 FileProvider 抽象开始真正发挥作用的地方。
13. 重命名其实可以通过 MOVE 实现
WebDAV 中:
Rename
经常可以理解为:
把资源移动到同一目录的新名称。
例如:
report.pdf
改成:
report-final.pdf
可以使用:
MOVE
概念:
Source:
/Documents/report.pdf
Destination:
/Documents/report-final.pdf
所以:
Rename
从协议角度可能只是:
MOVE
的一种特殊情况。
14. MOVE 同样可以实现跨目录移动
例如:
/Documents/report.pdf
移动到:
/Archive/report.pdf
依旧:
MOVE
逻辑:
Source
↓
MOVE
↓
Destination
所以:
func move(
_ item: FileItem,
to destination: FileLocation
) async throws
对于 WebDAV Provider 来说非常自然。
15. COPY 也是 WebDAV 的原生能力
如果 Server 支持:
COPY
就可以让服务器完成:
Remote File
↓
COPY
↓
Remote Destination
这比:
Download to iPhone
↓
Upload again
高效得多。
例如:
WebDAV A
内部同服务器 Copy
理想路线:
Server
↓
Server-side COPY
↓
Done
而不是:
Server
↓
iPhone
↓
Server
16. 这说明 Copy Operation 需要能力协商
前面我们一直把:
Copy
抽象成统一任务。
但到了远程 Provider,会发现 Copy 有不同最佳路径。
例如:
Local → Local
使用:
FileManager
WebDAV 同服务器:
COPY
WebDAV → Local:
GET
↓
Stream
↓
Local File
Local → WebDAV:
Local File
↓
PUT
所以 FileOperationManager 不应该永远只执行一种 Copy Pipeline。
可以理解为:
Copy Request
↓
Check Provider Capabilities
↓
Choose Strategy
例如:
Server-side Copy?
│
Yes
↓
Use COPY
No
↓
Stream Source → Destination
17. Provider 应该声明自己的能力
可以进一步设计:
struct FileProviderCapabilities {
let supportsMove: Bool
let supportsCopy: Bool
let supportsRangeRead: Bool
let supportsResumeUpload: Bool
let supportsServerSideCopy: Bool
}
不同 Provider:
Local
WebDAV
SMB
FTP
Cloud
能力可能不完全一样。
于是上层可以动态决定:
Can Rename?
Can Move?
Can Resume?
Can Stream?
而不是:
所有 Provider 必须实现完全一样的能力
这对长期扩展非常重要。
18. ETag 是一个很有价值的字段
WebDAV/HTTP 体系里经常会看到:
ETag
它可以用来表示资源版本。
例如第一次:
report.pdf
ETag: "abc123"
用户离开页面以后,服务器文件被修改:
ETag: "xyz789"
这说明:
Remote Resource Changed
所以 ETag 可以用于:
- Cache Validation
- Sync
- Resume Validation
- Conflict Detection
这是 WebDAV 相比一些传统文件协议非常方便的一点:
可以天然利用 HTTP 的缓存和条件请求体系。
19. 缓存目录时可以结合 ETag
假设打开:
/Documents/
第一次:
PROPFIND
↓
100 Files
↓
Cache
第二次打开:
Cache
↓
Immediate UI
同时后台:
Refresh
然后对比:
name
size
modifiedDate
etag
判断:
Added
Removed
Changed
最终实现:
Cached First
↓
Background Refresh
↓
Diff Update
这和上一篇 SMB 的 Directory Cache 是同一个思路。
20. WebDAV Server 并不总是完全一致
这是开发 WebDAV 时非常现实的问题。
标准定义是一回事。
不同 Server 的实现又是另一回事。
可能出现:
Server A
Server B
NAS C
Cloud D
对于:
- XML Namespace
- 状态码
- Date Format
- 路径编码
- Trailing Slash
- MOVE/COPY
- Locking
处理方式并不完全一致。
所以不要把 WebDAV Client 写成:
只要第一次 Server 能跑通
=
所有 Server 都兼容
真正产品化需要大量:
Compatibility Testing。
21. HTTP 状态码必须被认真处理
例如:
200
201
204
207
401
403
404
409
423
500
507
对于 WebDAV 来说:
207 Multi-Status
尤其重要。
因为一个 PROPFIND 请求中可能包含多个资源结果。
不能简单:
if statusCode == 200 {
success
}
而要按照 WebDAV 语义解析。
22. 401 和 403 不是一回事
例如:
401 Unauthorized
通常说明:
Authentication Required / Failed
而:
403 Forbidden
可能意味着:
已经识别用户
但没有访问该资源的权限
用户提示应该不同:
401
↓
检查用户名或密码
403
↓
没有访问该目录的权限
一个成熟的文件工具不能只显示:
Network Error。
23. 507 对文件上传很重要
WebDAV 中可能遇到:
507 Insufficient Storage
意味着远程存储空间不足。
例如上传:
10GB backup.zip
服务器空间不足。
这时候应该告诉用户:
服务器存储空间不足。
而不是:
上传失败。
这类错误映射能够极大改善文件工具体验。
24. Credentials 应该继续使用统一 CredentialStore
和 SMB 一样,WebDAV 可能需要:
Username
Password
所以我们不需要重新设计一套密码系统。
继续复用:
CredentialStore
↓
Keychain
例如:
struct RemoteServer {
let id: UUID
let type: ServerType
let endpoint: URL
let credentialID: String?
}
密码通过:
credentialID
↓
Keychain
获取。
不要把:
username:password@
长期保存到 URL。
25. HTTPS 对 WebDAV 尤其重要
SMB 经常运行在家庭局域网。
但 WebDAV 很可能使用:
Internet
例如:
iPhone
↓
5G
↓
Internet
↓
WebDAV Server
如果使用:
HTTP
认证信息和文件数据都存在明显风险。
因此面向公网时,应优先考虑:
HTTPS
架构:
WebDAV
↓
HTTP Semantics
↓
TLS
↓
Encrypted Transport
这也是 WebDAV 很适合远程访问的原因之一。
26. 上传进度仍然应该进入 FileOperation
用户上传:
backup.zip
32GB
依然需要:
Progress
Cancel
Retry
Failure
所以:
WebDAV Upload
不要自己维护一套:
uploadProgress
而应该创建:
FileOperation
例如:
type = upload
source = local
destination = webDAV
progress = 0.46
state = running
任务中心就可以统一显示:
Uploading backup.zip
14.7 GB / 32 GB
46%
27. WebDAV 下载也一样
例如:
WebDAV
↓
movie.mkv
↓
Local
创建:
Download Operation
内部:
GET
↓
Stream
↓
Temporary File
↓
Progress
↓
Validation
↓
Finalization
整个流程和:
SMB → Local
非常相似。
唯一不同的是:
Source Provider。
28. 这就是统一 Transfer Pipeline 的价值
现在我们已经有:
Local
SMB
WebDAV
于是 Copy/Transfer 可以进一步统一:
Source Provider
↓
Input Stream
↓
Transfer Pipeline
↓
Output Stream
↓
Destination Provider
例如:
SMB → Local
WebDAV → Local
Local → WebDAV
SMB → WebDAV
都不需要重新设计 UI。
用户仍然只是:
Copy
29. WebDAV → WebDAV 可以进一步优化
如果:
Source
和:
Destination
位于同一个 WebDAV Server,
可以尝试:
COPY
让服务器内部完成。
如果是:
Server A
↓
Server B
可能需要:
GET from A
↓
iPhone Stream
↓
PUT to B
所以 Operation Manager 最终会越来越像一个:
Transfer Planner。
30. 一个更完整的 WebDAV Provider 架构
最终可以整理成:
┌──────────────────────────────┐
│ UI │
│ │
│ File Browser / Task Center │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ FileService │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ FileOperationManager │
│ │
│ Copy │
│ Move │
│ Upload │
│ Download │
│ Retry │
│ Progress │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ FileProvider │
└──────────┬─────────┬─────────┘
│ │
▼ ▼
Local SMB
│
▼
WebDAV
│
▼
WebDAV Client
│
┌────────────┼────────────┐
▼ ▼ ▼
PROPFIND GET PUT
│ │ │
├── DELETE ├── MOVE └── MKCOL
│
▼
Server
旁边还有:
CredentialStore
DirectoryCache
NetworkMonitor
XMLParser
RetryPolicy
CapabilityResolver
31. TS File Explorer 中 WebDAV 的真正价值是什么?
从开发者角度,我们看到的是:
PROPFIND
PUT
GET
DELETE
MOVE
MKCOL
XML
HTTP Status
但用户真正想解决的问题往往只是:
“我的远程服务器文件,能不能直接在 iPhone 里管理?”
例如:
WebDAV Server
↓
Documents
↓
report.pdf
↓
iPhone
用户希望:
Open
Download
Rename
Move
Delete
Share
而不是先:
电脑下载
↓
再上传到其他地方
↓
iPhone 再下载
所以对文件浏览器来说:
WebDAV 不是单纯“多支持一个协议”。
而是:
再增加一种可以统一管理的文件来源。
32. 到第五篇以后,File Explorer 架构已经非常清晰
我们现在已经拥有:
File Browser
│
▼
FileItem
│
▼
FileService
│
▼
FileOperationManager
│
▼
FileProvider
│
┌─────────────┼─────────────┐
▼ ▼ ▼
Local SMB WebDAV
未来增加:
FTP
Google Drive
Dropbox
Other Cloud
核心架构仍然不需要推倒重来。
这才是 FileProvider 抽象真正的价值。
33. 开发 WebDAV 功能之前,先回答这 16 个问题
如果准备自己实现 WebDAV Client,建议提前回答:
1. PROPFIND 如何解析?
2. Depth 如何使用?
3. href 如何规范化?
4. URL Encoding 怎么处理?
5. XML Namespace 如何兼容?
6. Authentication 如何保存?
7. 是否强制 HTTPS?
8. 207 Multi-Status 如何处理?
9. PUT 是否支持大文件 Streaming?
10. GET 是否支持 Range?
11. 上传失败是否保留临时状态?
12. Server-side COPY 是否支持?
13. MOVE 如何处理冲突?
14. Directory Cache 如何刷新?
15. Server 差异如何兼容?
16. WebDAV 如何接入统一 FileProvider?
这些问题如果不提前处理,代码很容易变成:
一个 API
一个特殊判断
再一个 API
再一个特殊判断
最后越来越难维护。
写在最后
WebDAV 有意思的地方在于:
它没有重新发明整套互联网网络栈。
而是在:
HTTP
之上增加:
File System Semantics
于是:
PROPFIND
GET
PUT
DELETE
MOVE
COPY
MKCOL
组合在一起,就形成了一个远程文件系统。
而对于文件浏览器来说,真正重要的仍然不是某一个协议 API。
而是:
如何把不同协议都转换成统一的文件模型与操作模型。
到目前为止,这个系列已经完成:
01
iOS File Browser Architecture
↓
02
Large File IO
↓
03
Wi-Fi Transfer
↓
04
SMB / NAS
↓
05
WebDAV
现在已经从:
“手机里的文件”
逐渐扩展成:
“用户所有可以访问的文件来源”
这也是我在开发 TS File Explorer 时越来越明确的一点:
文件管理器真正需要统一的,不是协议,而是文件操作体验。
SMB 可以是 SMB。
WebDAV 可以是 HTTP。
FTP 也可以使用自己的协议。
但是对于用户来说:
打开
复制
移动
下载
上传
删除
重命名
应该尽可能保持一致。
下一篇就可以继续完成另一种经典远程文件协议:
《FTP 文件管理器怎么实现?从 FTP 控制连接、数据连接到大文件传输架构》
下一篇会重点讨论一个非常不同的协议模型:
Control Connection
+
Data Connection
以及为什么 FTP 下载一个文件,和我们前面讲过的 HTTP/WebDAV 在底层连接模型上有明显区别。