- 后端
- 网络
【免费下载链接】swift-nio
Event-driven network application framework for high performance protocol servers & clients, non-blocking.
本文基于 swift-nio 仓库中_NIOFileSystem模块的官方 DocC 扩展文档 FileSystemProtocol.md,系统梳理FileSystemProtocol协议的六大 API 分组:带作用域生命周期管理的文件打开(withFileHandle系列)、手动打开文件、文件信息查询、符号链接、文件管理(复制/删除/移动/替换/建目录)以及系统目录访问。读完后你可以直接使用FileSystem.shared完成异步非阻塞的本地文件操作,也能理解每个选项(OpenOptions、CopyStrategy、RemovalStrategy)的默认值与底层映射,甚至实现一个自己的文件系统后端。
1. 协议定位:所有文件 I/O 的统一抽象
FileSystemProtocol定义在 FileSystemProtocol.swift,是整个模块的核心接口。模块总览文档 index.md 指出:_NIOFileSystem提供了具体的FileSystem用于操作本地文件系统,同时通过一组协议允许你创建其他文件系统实现。
协议本身是一个Sendable协议,并声明了 4 个关联类型(FileSystemProtocol.swift#L19-L34):
public protocol FileSystemProtocol: Sendable { /// Opens a file for reading -> this handle type associatedtype ReadFileHandle: ReadableFileHandleProtocol /// for writing associatedtype WriteFileHandle: WritableFileHandleProtocol /// for reading and writing associatedtype ReadWriteFileHandle: ReadableAndWritableFileHandleProtocol /// for directories associatedtype DirectoryFileHandle: DirectoryFileHandleProtocol where DirectoryFileHandle.ReadFileHandle == ReadFileHandle, DirectoryFileHandle.ReadWriteFileHandle == ReadWriteFileHandle, DirectoryFileHandle.WriteFileHandle == WriteFileHandle }关联类型之间的where约束保证了目录句柄打开子文件时返回的句柄类型与文件系统本身保持一致——这是实现自定义后端时最容易踩的坑。
官方实现FileSystem是一个基于NIOThreadPool的结构体:所有阻塞的系统调用都被提交到线程池执行,避免阻塞 Swift 并发运行时(FileSystem.swift#L30-L89):
let handle = try await self.threadPool.runIfActive { let handle = try self._openFile(forReadingAt: path, options: options).get() // 刚刚创建,安全地转移到调用者所在的 Task return UnsafeTransfer(handle) }获取实例的三种方式:
FileSystem.shared:全局共享实例,默认 2 个工作线程,可通过SWIFT_FILE_SYSTEM_THREAD_COUNT环境变量调整;FileSystem(threadPool:):传入你自己的NIOThreadPool(不由FileSystem负责关闭);withFileSystem(numberOfThreads:) { fileSystem in ... }:创建带独立线程池的实例,闭包结束后自动shutdownGracefully()(FileSystem.swift#L774-L788)。
错误模型方面,模块抛出的顶层错误只有FileSystemError(及 Swift 的CancellationError),FileSystemError提供detailedDescription()输出结构化多行诊断信息(FileSystemError.swift)。官方给出的整体使用示例见 NIOFileSystemTour.swift 片段,下文各节会拆解其中涉及的 API。
2. 带生命周期管理的文件打开:withFileHandle/withDirectoryHandle
官方文档将这一组 API 归为 "Opening files with managed lifecycles"。它们封装了"打开 → 执行 → 关闭"的完整流程,文件在execute闭包的生命周期内保持打开,闭包返回前自动关闭,无需调用者担心资源泄漏。实现位于 FileSystemProtocol.swift#L321-L432:
// 只读打开 func withFileHandle<Result>( forReadingAt path: FilePath, options: OpenOptions.Read = OpenOptions.Read(), execute: (_ read: ReadFileHandle) async throws -> Result ) async throws -> Result // 只写打开(默认:新建文件,不替换已有文件) func withFileHandle<Result>( forWritingAt path: FilePath, options: OpenOptions.Write = .newFile(replaceExisting: false), execute: (_ write: WriteFileHandle) async throws -> Result ) async throws -> Result // 读写打开(默认同上) func withFileHandle<Result>( forReadingAndWritingAt path: FilePath, options: OpenOptions.Write = .newFile(replaceExisting: false), execute: (_ readWrite: ReadWriteFileHandle) async throws -> Result ) async throws -> Result // 打开目录 func withDirectoryHandle<Result>( atPath path: FilePath, options: OpenOptions.Directory = OpenOptions.Directory(), execute: (_ directory: DirectoryFileHandle) async throws -> Result ) async throws -> Result几个值得注意的实现细节:
- 句柄禁止逃逸闭包。所有方法的文档都标注了 "The handle passed to
executemust not escape the closure"——关闭由外层统一管理,句柄一旦逃逸会导致重复关闭或使用已关闭描述符。 - 失败路径上的关闭语义。写句柄的关闭使用
withUncancellableTearDown区分成败:成功时close()正常落地(例如提交事务性创建);失败时close(makeChangesVisible: false)丢弃未提交的变更(FileSystemProtocol.swift#L366-L382)。 - 关闭是不可取消的(
withUncancellableTearDown),即使调用方 Task 被取消,close()也会执行完,保证描述符不泄漏。
官方示例中用它写入一个文件并顺带读目录(NIOFileSystemTour.swift#L40-L60):
try await fileSystem.withFileHandle( forWritingAt: "/Users/hal9000/demise-of-dave.txt", options: .newFile(replaceExisting: false) ) { file in let plan = ByteBuffer(string: "TODO...") try await file.write(contentsOf: plan.readableBytesView, toAbsoluteOffset: 0) } let path: FilePath? = try await fileSystem.withDirectoryHandle(atPath: "/Users/hal9000/Music") { directory in for try await entry in directory.listContents() { if entry.name == "daisy.mp3" { return entry.path // 提前 return 也会自动关闭句柄 } } return nil }3. 手动打开文件/目录:openFile与openDirectory
文档 "Opening files" 分组的四个方法是withFileHandle系列的底层原语,区别在于关闭责任完全交给调用者,适用于句柄需要跨越多个逻辑阶段、或需要在结构化闭包之外持有的场景:
func openFile(forReadingAt path: FilePath, options: OpenOptions.Read) async throws -> ReadFileHandle func openFile(forWritingAt path: FilePath, options: OpenOptions.Write) async throws -> WriteFileHandle func openFile(forReadingAndWritingAt path: FilePath, options: OpenOptions.Write) async throws -> ReadWriteFileHandle func openDirectory(atPath path: FilePath, options: OpenOptions.Directory) async throws -> DirectoryFileHandle协议约定(FileSystemProtocol.swift#L38-L85):
- 打开的文件必须已存在,否则抛出
FileSystemError,错误码为.notFound; - 打开目录时目录必须已存在,否则抛错;创建目录应使用
createDirectory(at:withIntermediateDirectories:permissions:); - 便捷重载
openFile(forReadingAt:)、openDirectory(atPath:)(不带 options)等价于传入默认的OpenOptions.Read()/OpenOptions.Directory()(FileSystemProtocol.swift#L434-L462)。
本地实现FileSystem的这四个方法都走open(2)系统调用,先在NIOThreadPool中同步执行,再把句柄以UnsafeTransfer移交回调用方 Task(FileSystem.swift#L91-L200)。
3.1OpenOptions参数详解
三类 options 都位于 OpenOptions.swift#L18-L151,公共字段:
| 字段 | 类型 | 默认值 | 含义 |
|---|---|---|---|
followSymbolicLinks | Bool | true | 末段路径是符号链接时是否跟随;为false且遇到符号链接则抛错 |
closeOnExec | Bool | false | 将描述符标记为 close-on-exec(O_CLOEXEC) |
OpenOptions.Write额外包含两个字段:
existingFile: OpenOptions.ExistingFile:对已存在文件的处理策略,取值.none(存在即报错,O_EXCL)、.open(直接打开)、.truncate(截断,等价O_TRUNC);newFile: OpenOptions.NewFile?:是否允许创建(nil表示不创建),NewFile含permissions(nil时用默认权限)和transactionalCreation(默认true:新建文件在close()且无异常时才真正落盘,仅在existingFile == .none时生效)。
两个最常用工厂方法(OpenOptions.swift#L112-L149):
// 新建文件;replaceExisting=true 时替换同名文件(truncate) static func newFile(replaceExisting: Bool, permissions: FilePermissions? = nil) -> Self // 修改已有文件;createIfNecessary=true 时不存在则创建 static func modifyFile(createIfNecessary: Bool, permissions: FilePermissions? = nil) -> Self底层映射关系可从descriptorOptions属性看到(OpenOptions.swift#L209-L234):followSymbolicLinks == false → .noFollow、closeOnExec == true → .closeOnExec、newFile != nil → .create、existingFile == .none → .exclusiveCreate、.truncate → .truncate。目录 options 还会额外带上.directory标志。
默认权限常量(OpenOptions.swift#L283-L298):
- 常规文件
defaultsForRegularFile:属主读写(rw-)、组/其他只读(r--); - 目录
defaultsForDirectory:属主读写执行(rwx)、组/其他读执行(r-x)。
4. 文件信息:info(forFileAt:infoAboutSymbolicLink:)
func info(forFileAt path: FilePath, infoAboutSymbolicLink: Bool) async throws -> FileInfo?返回路径处的文件信息;文件不存在时返回nil而不是抛错——因此它是"文件是否存在"的标准检查手段。infoAboutSymbolicLink为true时返回链接本身的信息,为false时返回链接目标的信息(FileSystemProtocol.swift#L136-L147)。便捷重载info(forFileAt:)固定传入false(FileSystemProtocol.swift#L464-L473)。
FileInfo中type(FileType)、permissions、时间戳等字段在平台间存在差异,模块文档 index.md 明确说明:不同平台的FileInfo表示不同,需查阅FileInfo本身的文档。示例用法(NIOFileSystemTour.swift#L15-L19):
if let info = try await fileSystem.info(forFileAt: "/Users/hal9000/demise-of-dave.txt") { print("demise-of-dave.txt has type '\(info.type)'") } else { print("demise-of-dave.txt doesn't exist") }5. 符号链接
协议提供两个符号链接操作(FileSystemProtocol.swift#L151-L169):
// 在 path 处创建指向 destinationPath 的符号链接;path 处已有文件/目录则抛错 func createSymbolicLink(at path: FilePath, withDestination destinationPath: FilePath) async throws // 读取符号链接的目标路径 func destinationOfSymbolicLink(at path: FilePath) async throws -> FilePath官方 Tour 示例(NIOFileSystemTour.swift#L76-L81):
try await fileSystem.createSymbolicLink(at: "/Users/hal9000/Backup", withDestination: "/Volumes/Tardis") // 打开符号链接默认就打开其目标,多数场景无需读取 destination try await fileSystem.withDirectoryHandle(atPath: "/Users/hal9000/Backup") { directory in ... }注意打开行为与OpenOptions中followSymbolicLinks的联动:默认跟随链接;置为false时末段组件若是符号链接会抛错,可用destinationOfSymbolicLink(at:)显式解析目标。
6. 文件管理:复制、删除、移动、替换、建目录
这是文档 "Managing files" 分组,也是协议中最复杂的部分,完整实现语义都写在协议方法的文档注释中。
6.1 复制:copyItem全家族
完整签名(FileSystemProtocol.swift#L236-L251):
func copyItem( at sourcePath: FilePath, to destinationPath: FilePath, strategy copyStrategy: CopyStrategy, replaceExisting: Bool, shouldProceedAfterError: @escaping @Sendable (_ source: DirectoryEntry, _ error: Error) async throws -> Void, shouldCopyItem: @escaping @Sendable (_ source: DirectoryEntry, _ destination: FilePath) async -> Bool ) async throws语义要点(均来自 FileSystemProtocol.swift#L173-L235 的文档注释):
- 可能抛出的错误码:
sourcePath不存在 →.notFound;replaceExisting == false且destinationPath已存在、或其父目录不存在 →.invalidArgument;其他错误也可能发生; - 若
sourcePath是符号链接,只复制链接本身;复制结果保留权限与扩展属性(在文件系统支持时); - 错误回调契约:实现方在抛错前必须先调用
shouldProceedAfterError;若闭包正常返回视为"继续",该错误被吞掉;闭包抛错则copyItem抛错并停止复制。实现方 MUST 对每个出错项恰好调用一次、且不得持锁;MAY 并发多次调用(sequential策略除外)。抛出错误后destinationPath内的状态是未定义的,实现方无义务清理; - 过滤回调契约:
shouldCopyItemMUST 在每个项(含sourcePath本身)被复制前恰好调用一次、不得持锁、且必须先于父目录检查子项——父目录被过滤则其内部所有项都不再检查;sequential策略下同一时刻只会有一个回调在执行。
CopyStrategy定义了目录级复制的并发度(IOStrategy.swift#L58-L103):
| 工厂 | 含义 |
|---|---|
.platformDefault | 平台合理默认;假设同一时刻只有一次复制、且复制不是设备主要活动 |
.sequential | 异步执行但一次只有一项操作,保证shouldCopyItem回调串行 |
.parallel(maxDescriptors:) | 限制复制中并发打开的描述符数量,必须 ≥ 2,否则抛.invalidArgument |
便捷重载都在扩展里(FileSystemProtocol.swift#L475-L645):
// 最简:平台默认策略,出错即中止,全部项都复制 try await fileSystem.copyItem(at: "/Users/hal9000/Music", to: "/Volumes/Tardis/Music") // 带回调:策略默认 .platformDefault,replaceExisting 固定 false try await fileSystem.copyItem(at: src, to: dst, shouldProceedAfterError: { ... }, shouldCopyItem: { ... })另有一个标记@available(*, deprecated)的旧重载copyItem(at:to:shouldProceedAfterError:shouldCopyFile:),其shouldCopyFile闭包参数是(FilePath, FilePath);官方提示迁移为接收DirectoryEntry的新版本,实现内部用.sequential策略保持旧版串行回调语义。
6.2 删除:removeItem(at:strategy:recursively:)
@discardableResult func removeItem(at path: FilePath, strategy removalStrategy: RemovalStrategy, recursively removeItemRecursively: Bool) async throws -> Int(FileSystemProtocol.swift#L253-L277)
- 目标必须是常规文件、符号链接或目录;路径不存在时返回 0 而非抛错,返回值是被删除的项数;
recursively == true等价rm -r,false等价rmdir(对非目录无效);符号链接只删链接不删目标;removalStrategy与复制策略同族(IOStrategy.swift#L125-L168):.platformDefault/.sequential/.parallel(maxDescriptors:),区别是删除最少只需1个描述符(只扫描目录时占用),maxDescriptors < 1才抛.invalidArgument;
便捷重载(FileSystemProtocol.swift#L647-L716):removeItem(at:)等价于.platformDefault + recursively: true;removeItem(at:recursively:)与removeItem(at:strategy:)分别补全其余默认参数。目录删除的并行扫描实现在 ParallelRemoval.swift。
6.3 移动与替换:moveItem/replaceItem
func moveItem(at sourcePath: FilePath, to destinationPath: FilePath) async throws func replaceItem(at destinationPath: FilePath, withItemAt existingPath: FilePath) async throwsmoveItem:sourcePath不存在 →.notFound;destinationPath已存在或父目录不存在 →.invalidArgument;源是符号链接时只移动链接(FileSystemProtocol.swift#L279-L293);replaceItem:行为与moveItem相同,但允许替换已存在的destinationPath——替换完成后existingPath被移除,因此原路径不再存在。destinationPath不必存在,且允许文件与目录互相替换;若复制到destinationPath成功但从existingPath移除失败,错误码为.io(FileSystemProtocol.swift#L295-L316)。
6.4 创建目录:createDirectory
func createDirectory(at path: FilePath, withIntermediateDirectories createIntermediateDirectories: Bool, permissions: FilePermissions?) async throws- 对应
mkdir(2);path处已有目录(或文件)则抛错; createIntermediateDirectories == false时path的完整前缀必须已存在,为true时自动创建全部中间目录;- 便捷重载
createDirectory(at:withIntermediateDirectories:)固定使用permissions: .defaultsForDirectory(rwx/r-x/r-x,FileSystemProtocol.swift#L718-L741)。
7. 系统目录:当前目录、临时目录与作用域临时目录
文档 "System directories" 分组列出三个成员(FileSystemProtocol.swift#L106-L132):
var currentWorkingDirectory: FilePath { get async throws } // 当前工作目录 var temporaryDirectory: FilePath { get async throws } // 系统临时目录(协议中还另有 homeDirectory)以及核心的作用域 APIwithTemporaryDirectory(FileSystemProtocol.swift#L743-L777):
func withTemporaryDirectory<Result>( prefix: FilePath? = nil, options: OpenOptions.Directory = OpenOptions.Directory(), execute: (_ directory: DirectoryFileHandle, _ path: FilePath) async throws -> Result ) async throws -> Result实现要点:
prefix为nil时以temporaryDirectory为前缀;- 在模板尾部追加 8 个
X("XXXXXXXX"),调用createTemporaryDirectory(template:)由系统替换为唯一组合生成真实目录名——协议要求模板至少以 3 个X结尾,且模板中的中间目录若不存在会被创建; - 通过
withUncancellableTearDown保证:无论execute成功还是抛错,退出时都会以.platformDefault策略递归删除整个临时目录。
这是写"测试数据沙箱""下载暂存区"这类逻辑的推荐模式,无需defer或 finally 清理。
8. 实现一个自定义文件系统
模块文档 index.md 的 "Creating a File System" 一节指出:实现FileSystemProtocol即可创建自定义文件系统,它依赖以下句柄协议族:
FileSystemProtocol ├── FileHandleProtocol // 所有句柄的基础 ├── ReadableFileHandleProtocol // ReadFileHandle 关联类型 ├── WritableFileHandleProtocol // WriteFileHandle 关联类型 ├── ReadableAndWritableFileHandleProtocol // ReadWriteFileHandle 关联类型 └── DirectoryFileHandleProtocol // DirectoryFileHandle 关联类型这些协议分别定义在 FileHandleProtocol.swift、ReadableFileHandleProtocol.md 等对应文档中。实现时的关键点:
- 协议要求的"必须实现"方法只有第二节列出的四个
openFile/openDirectory,加上createDirectory、三个目录属性、createTemporaryDirectory、info、两个符号链接方法和四个文件管理方法;withFileHandle系列、各类便捷openFile/copyItem/removeItem重载都是基于必须实现方法的默认扩展,自动获得; - 关联类型
where约束(DirectoryFileHandle.ReadFileHandle == ReadFileHandle等)要求目录句柄打开子文件时返回你定义的句柄类型; copyItem的错误/过滤回调契约(每次恰好一次、不持锁、父先于子)对实现方是强制语义,目录级并行扫描可参考 ParallelDirCopy.swift 的实现方式;- 由于协议是
Sendable的,你的文件系统类型及句柄类型都应满足并发安全要求。
9. 行为验证:测试用例指引
若需验证以上语义,可参考仓库中的测试:
- FileSystemTests.swift:复制/移动/替换/删除、临时目录等协议级行为的集成验证;
- FileHandleTests.swift:句柄读写字节偏移、事务性创建等;
- DirectoryEntriesTests.swift:目录列举(
listContents()返回的DirectoryEntry序列); - FileSystemTests+SPI.swift:通过
@_spi(Testing)入口验证OpenOptions默认权限等内部约定。
10. 小结:API 速查表
| 分组(对应文档 Topics) | API | 生命周期责任 | 关键默认值 |
|---|---|---|---|
| 管理式打开 | withFileHandle(forReadingAt:options:execute:) | 框架自动关闭 | OpenOptions.Read() |
| 管理式打开 | withFileHandle(forWritingAt:options:execute:) | 框架自动关闭,失败时makeChangesVisible: false | .newFile(replaceExisting: false) |
| 管理式打开 | withFileHandle(forReadingAndWritingAt:options:execute:) | 框架自动关闭 | .newFile(replaceExisting: false) |
| 管理式打开 | withDirectoryHandle(atPath:options:execute:) | 框架自动关闭 | OpenOptions.Directory() |
| 手动打开 | openFile(forReadingAt:)/openFile(forWritingAt:options:)/openFile(forReadingAndWritingAt:options:)/openDirectory(atPath:options:) | 调用者必须关闭 | 各OpenOptions |
| 文件信息 | info(forFileAt:infoAboutSymbolicLink:) | — | 不存在返回nil |
| 符号链接 | createSymbolicLink(at:withDestination:)/destinationOfSymbolicLink(at:) | — | — |
| 文件管理 | copyItem(4 个重载) | — | 最简版.platformDefault、replaceExisting: false、出错即停 |
| 文件管理 | removeItem(at:)/(at:recursively:)/(at:strategy:)/(at:strategy:recursively:) | — | .platformDefault+ 递归;不存在返回 0 |
| 文件管理 | moveItem(at:to:)/replaceItem(at:withItemAt:) | — | 替换版允许目标已存在 |
| 文件管理 | createDirectory(at:withIntermediateDirectories:permissions:) | — | 便捷版用目录默认权限 |
| 系统目录 | currentWorkingDirectory/temporaryDirectory | — | — |
| 系统目录 | withTemporaryDirectory(prefix:options:execute:) | 自动创建+递归删除 | 前缀默认temporaryDirectory,模板 8 个X |
本地文件系统的具体实现(FileSystem+ 线程池 +open(2)等系统调用)、跨平台差异(Apple 平台复制走 clone、扩展属性可能在部分系统不可用、路径格式差异等)见 index.md 与 FileSystem.swift,可作为深入阅读的入口。
- 后端
- 网络
【免费下载链接】swift-nio
Event-driven network application framework for high performance protocol servers & clients, non-blocking.
相关推荐
Shiro文件系统:文件操作与管理指南
Shiro文件系统:文件操作与管理指南 Shiro作为一个极简主义的个人网站主题,虽然在设计上追求简洁,但其文件系统架构却十分完善。本文将为您详细介绍Shiro
前端快速上手Goose AI Agent:从一句自然语言到自动化部署的完整路径
快速上手Goose AI Agent:从一句自然语言到自动化部署的完整路径 Goose(Goose AI Agent)是一个开源的本地AI代理:它跑在你自己的机
人工智能大模型AI AgentAI 应用本地部署MCP ClientsMCP 服务工具调用桌面应用CLI掌握Carbon语言文件系统:高效路径处理与文件操作全指南
掌握Carbon语言文件系统:高效路径处理与文件操作全指南 Carbon语言作为一种实验性的系统级编程语言,其文件系统API设计融合了现代安全性与跨平台兼容性。
编程语言编译器标准库
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考