在 macOS 上使用 FileProvider 同步文件到云端
Sync files to the cloud with FileProvider on macOS
2021年6月10日
一句话判断
如果你在做 macOS 云存储客户端,FileProvider 框架是你和 Finder 深度集成的唯一正确路径——别再自己写 Finder Sync Extension 了,那是个维护噩梦。
这场 Session 讲了什么
Session 介绍了 macOS Monterey 中 FileProvider 框架的重大更新。FileProvider 是 Apple 为云存储服务商设计的系统级框架,让你的云存储服务可以在 Finder、Open/Save 对话框、Spotlight 搜索中无缝呈现,就像 iCloud Drive 一样。
今年的更新重点包括:改进的遥测(Telemetry)API 让你可以监控同步状态、新的 NSFileProviderDomain 管理能力、以及 NSFileProviderReplicatedExtension——一个全新的扩展子类,支持更细粒度的同步控制。Session 还详细讲解了”按需下载”(On-demand Download)的实现:文件在 Finder 中显示但内容还在云端,用户双击时才触发下载。这个机制对用户体验至关重要——想象一下用户的 100GB 云盘里有 10 万个文件,你不可能在启动时全部下载。
值得深挖的点
Replicated Extension vs Non-Replicated Extension 的选择
FileProvider 提供两种扩展类型:NSFileProviderExtension(Non-Replicated,文件只存在于云端)和 NSFileProviderReplicatedExtension(Replicated,文件可以同时在本地和云端存在)。大多数云存储服务应该选择 Replicated 模式,因为它支持离线访问、本地缓存和增量同步。Non-Replicated 模式适合纯云端的文件浏览器。Replicated Extension 的实现更复杂,需要管理本地镜像(local mirror)和云端状态的双向同步。
NSFileProviderUserInteractions 的错误处理
同步过程中不可避免地会遇到冲突——用户在两台设备上同时修改了同一个文件。NSFileProviderUserInteractions 允许你定义用户可见的冲突解决界面。你可以通过 NSFileProviderUserInteractions 的 actionIdentifier 注册自定义的处理动作,Finder 会在冲突发生时展示你的解决方案(比如”保留云端版本”、“保留本地版本”或”保留两者”)。
代码片段
// 注册 FileProvider 域(Domain)
func setupProviderDomain() {
let domain = NSFileProviderDomain(
identifier: NSFileProviderDomainIdentifier("com.example.cloud"),
displayName: "My Cloud Storage",
pathRelativeToDocumentStorage: "MyCloud"
)
// 向系统注册域
NSFileProviderManager.add(domain) { error in
if let error = error {
print("注册域失败: \(error)")
} else {
print("域注册成功,Finder 中将出现 My Cloud Storage")
}
}
}
// 实现 NSFileProviderReplicatedExtension 的核心方法
class CloudFileProvider: NSFileProviderReplicatedExtension {
// 提供目录内容列表
func enumerator(for containerItemIdentifier: NSFileProviderItemIdentifier) throws -> NSFileProviderEnumerator {
return CloudEnumerator(containerItemIdentifier: containerItemIdentifier)
}
// 创建文件或目录
func createItem(basedOn itemTemplate: NSFileProviderItem,
fields: NSFileProviderItemFields,
contents url: URL?,
options: NSFileProviderCreateItemOptions = [],
completionHandler: @escaping (NSFileProviderItem?, NSFileProviderItemFields, Bool, Error?) -> Void) {
Task {
do {
// 在云端创建文件
let remoteItem = try await CloudAPI.createItem(
name: itemTemplate.filename,
parentID: itemTemplate.parentItemIdentifier.rawValue,
localURL: url
)
// 返回创建后的文件元数据
let providerItem = CloudFileItem(remoteItem: remoteItem)
completionHandler(providerItem, [], false, nil)
} catch {
completionHandler(nil, [], false, error)
}
}
}
// 获取文件内容(按需下载)
func fetchContents(for itemIdentifier: NSFileProviderItemIdentifier,
version requestedVersion: NSFileProviderItemVersion?,
completionHandler: @escaping (URL?, NSFileProviderItem?, Error?) -> Void) {
Task {
do {
// 从云端下载文件到临时 URL
let (localURL, metadata) = try await CloudAPI.downloadFile(
id: itemIdentifier.rawValue,
version: requestedVersion
)
completionHandler(localURL, metadata, nil)
} catch {
completionHandler(nil, nil, error)
}
}
}
}
// 枚举器:列出目录内容
class CloudEnumerator: NSObject, NSFileProviderEnumerator {
private let containerID: NSFileProviderItemIdentifier
init(containerItemIdentifier: NSFileProviderItemIdentifier) {
self.containerID = containerItemIdentifier
}
func enumerateItems(for observer: NSFileProviderEnumerationObserver,
startingAt anchor: NSFileProviderSyncAnchor) {
Task {
do {
// 从云端获取目录列表
let items = try await CloudAPI.listDirectory(
id: containerID == .rootContainer ? "root" : containerID.rawValue
)
let providerItems = items.map { CloudFileItem(remoteItem: $0) }
observer.didEnumerate(providerItems)
observer.finishEnumerating(upTo: NSFileProviderSyncAnchor(data: Data()))
} catch {
observer.finishEnumerating(error: error)
}
}
}
// 监控变更:当云端文件发生变化时通知系统
func startProvidingItem(at url: URL, completionHandler: @escaping (Error?) -> Void) {
// 确保本地文件已下载
// 如果文件已在本地缓存,直接返回
completionHandler(nil)
}
}
最佳实践
- 初始同步时只拉取文件元数据(名称、大小、修改日期),不要下载文件内容。用户双击或通过 Quick Look 查看时才触发下载。
- 同步冲突的默认策略应该是”保留最新修改的版本”,并允许用户通过 Finder 的冲突解决界面手动选择。
NSFileProviderManager的signalEnumerator(for:)方法是你告诉 Finder”这个目录的内容变了,请刷新”的通道——任何云端变更后都要调用它。
还有什么值得关注
- FileProvider 的 thumbnail 支持可以让 Finder 显示云端文件的缩略图,不需要下载完整文件。
- Spotlight 通过 FileProvider 自动索引云端文件名——用户可以在 Spotlight 中搜索到未下载的云端文件。
- macOS Monterey 的
NSFileProviderDomain支持多个域,适合云存储服务中的”个人空间”和”团队空间”分离。