WebDAV 文件管理是如何实现的?从 HTTP 方法到跨存储 FileProvider 架构

31 阅读13分钟

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 在底层连接模型上有明显区别。