Type-safe file system operations for Swift with kernel-assisted I/O and async streaming. APFS clones, pull-based batched directory iteration, and configurable durability. Swift 6 strict concurrency with actor-based I/O executor.
- Why swift-file-system?
- Overview
- Design Principles
- Design Guarantees
- Features
- Installation
- Quick Start
- Usage Examples
- Testing Your Code
- Performance
- Architecture
- Platform Support
- Non-goals
- Testing
- Related Packages
- Contributing
- License
Foundation's FileManager and URL APIs are designed for simplicity, not performance or async correctness. This library exists to solve problems Foundation cannot:
| Problem | Foundation | swift-file-system |
|---|---|---|
| Async iteration | Blocks thread per entry | Pull-based batching (~0.6ms per 1000 files) |
| File copy | Always reads+writes bytes | APFS clone, kernel-assisted paths (~7.5ms for 1MB) |
| Durability control | No control over fsync | .full, .dataOnly, .none modes |
| Thread pool starvation | Blocking I/O starves cooperative pool | Dedicated executor option isolates I/O |
| Concurrency safety | Requires manual synchronization | Sendable throughout, actor-isolated state |
| Symlink handling | Implicit, inconsistent | Explicit followSymlinks option everywhere |
| Error handling | Untyped throws, catch-all required | Typed throws with precise error types |
If you need predictable async behavior, kernel-level optimizations, or fine-grained durability control, this library provides what Foundation cannot.
swift-file-system provides a modern Swift interface for file system operations with focus on performance, safety, and async-first design. Built on POSIX and Windows APIs with platform-specific optimizations like APFS cloning on macOS and copy_file_range/sendfile on Linux.
- Typed throws: Every function declares its error type. Catch precisely, not broadly.
- Nested types: API reads naturally:
File.Directory.Contents.Async, notFileDirectoryContentsAsync. - Foundation-free: No URL, no Data, no String bridging.
- Pull-based async: Bounded memory via batched iteration. No unbounded queues.
- Sync and async parity: Same method names, Swift picks based on context.
- Atomic writes: Write-sync-rename pattern ensures no partial writes on crash (when durability is
.fullor.dataOnly) - Bounded memory: Async executor queue is bounded; backpressure suspends callers when full
- Fallback correctness: Copy operations try fast paths first, fall back gracefully to manual loop
- No data races: Full Swift 6 strict concurrency compliance with
Sendabletypes throughout - Graceful shutdown: In-flight operations complete; pending operations fail with explicit error
- Cross-process consistency: No file locking primitives (yet); concurrent access from multiple processes requires external coordination
- Path normalization:
File.Pathvalidates structure (no NUL bytes, no empty components) but does not resolve.., symlinks, or canonicalize case - Windows path semantics: UNC paths (
\\server\share) and drive-relative paths (C:foo) are not fully validated - Security sandbox: This is not a security boundary; path traversal prevention is best-effort, not hardened
Use File.Path for structured path manipulation, but do not rely on it as a security mechanism.
- Kernel-assisted file copy: APFS cloning,
copyfile()on Darwin,copy_file_range/sendfileon Linux (~7.5ms for 1MB) - Batched directory iteration: Pull-based async with configurable batch sizes (~0.6ms per 1000 files)
- Async streaming:
AsyncSequencefor file bytes, directory entries, and recursive walks - Atomic writes: Crash-safe write-sync-rename pattern with configurable durability modes
- Dedicated I/O executor: Actor-based thread pool that doesn't starve Swift's cooperative pool
- Validated paths:
File.Pathrejects NUL bytes, empty components, and embedded newlines - Swift 6 strict concurrency: Full
Sendablecompliance, no data races - Cross-platform: macOS, iOS, Linux, and Windows support
Add swift-file-system to your Package.swift:
dependencies: [
.package(url: "https://github.com/coenttb/swift-file-system.git", from: "0.5.0")
]Add to your target:
.target(
name: "YourTarget",
dependencies: [
.product(name: "File System", package: "swift-file-system"),
]
)- Swift 6.2+
- macOS 26.0+, iOS 26.0+, or Linux
- Xcode 26.0+ (for Apple platforms)
import File_System
// Create a file reference
let file: File = "/tmp/data.txt"
// Read and write
let data = try file.read() // sync
let data = try await file.read() // async
try file.write("Hello, World!") // sync
try await file.write("Hello, World!") // async
// Append
try file.append(" More content")
// Check properties
if file.exists && file.isFile {
print("Size: \(try file.size)")
print("Empty: \(try file.isEmpty)")
}
// File operations
try file.copy(to: otherFile)
try file.move(to: newLocation)
try file.delete()// Scoped handle access with automatic cleanup
try file.open { handle in
let chunk = try handle.read(count: 1024)
}
try file.open.write { handle in
try handle.write(bytes)
}
// Static API works too
try File.open(path).readWrite { handle in
try handle.seek(to: 100)
try handle.write(data)
}let dir: File.Directory = "/tmp/mydir"
// Create and delete
try dir.create(withIntermediates: true)
try dir.delete(recursive: true)
// Contents
for entry in try dir.contents() {
print(entry.name)
}
// Subscript access
let readme = dir[file: "README.md"]
let subdir = dir[directory: "src"]
// Async iteration (batched, 48x faster)
for try await entry in File.Directory.entries(at: dir.path) {
print(entry.name)
}
// Recursive walk
for entry in try File.Directory.Walk(at: dir.path) {
print("\(entry.depth): \(entry.path)")
}The same method names work for both sync and async - Swift picks based on context:
// Convenience API
let data = try file.read() // sync
let data = try await file.read() // async
// Primitive API (for advanced options)
try File.System.Write.Atomic.write(data, to: path, options: .init(durability: .full))
try await File.System.Write.Atomic.write(data, to: path, options: .init(durability: .full))// Async byte streaming with backpressure
for try await chunk in File.System.Read.bytes(from: path) {
process(chunk)
}let file: File = "/tmp/config.json"
// Simple read/write (uses safe defaults)
try file.write(jsonString)
let content = try file.readString()
// Copy and move
let backup = File("/tmp/config.backup.json")
try file.copy(to: backup)
try file.move(to: File("/tmp/new-location.json"))Use the primitive API when you need fine-grained control:
// Full durability (F_FULLFSYNC on Darwin, fsync on Linux)
try File.System.Write.Atomic.write(
data,
to: path,
options: .init(durability: .full)
)
// Data-only sync (faster, metadata may be stale after crash)
try File.System.Write.Atomic.write(
data,
to: path,
options: .init(durability: .dataOnly)
)
// No sync (fastest, for temporary files)
try File.System.Write.Atomic.write(
data,
to: path,
options: .init(durability: .none)
)// Copy with attributes (permissions, timestamps)
try File.System.Copy.copy(
from: source,
to: destination,
options: .init(copyAttributes: true, overwrite: true)
)
// Handle symlinks explicitly
try File.System.Copy.copy(
from: source,
to: destination,
options: .init(followSymlinks: false) // Copy symlink itself
)let projectDir: File.Directory = "/Users/me/project"
// Create directory structure
try projectDir.create(withIntermediates: true)
let srcDir = projectDir[directory: "src"]
try srcDir.create()
// List contents
let files = try projectDir.files()
let subdirs = try projectDir.subdirectories()
// Check if empty
if try projectDir.isEmpty {
try projectDir.delete()
}
// Copy/move directories
try projectDir.copy(to: File.Directory("/tmp/backup"))
try projectDir.move(to: File.Directory("/Users/me/new-project"))// Walk with options
let options = File.Directory.Walk.Options(
followSymlinks: false,
skipHidden: true,
maxDepth: 5
)
for entry in try File.Directory.Walk(at: rootPath, options: options)
where entry.type == .regular && entry.name.hasSuffix(".swift") {
print("Found Swift file: \(entry.path)")
}// For heavy I/O, use a custom executor with more workers
let io = File.IO.Executor(.init(workers: 4))
// Pass custom executor to any operation
let data = try await File.System.Read.Full.read(from: path, io: io)
for try await entry in File.Directory.entries(at: path, io: io) { ... }
// Explicit executors must be shut down when done
await io.shutdown()Add the test support library to your test target:
.testTarget(
name: "YourTests",
dependencies: [
.product(name: "File System", package: "swift-file-system"),
.product(name: "File System Test Support", package: "swift-file-system"),
]
)Use Temporary.Scope for automatic cleanup:
import File_System_Test_Support
try File.Directory.temporary { dir in
// dir is a File.Path to a fresh temp directory
let testFile = File(dir, appending: "test.txt")
try testFile.write("test data")
// directory automatically deleted when closure exits
}Benchmarks from swift test -c release on Apple Silicon.
| Operation | Median | Throughput | Notes |
|---|---|---|---|
| Read 1MB | 5.94ms | ~168 MB/s | File.System.Read.Full.read |
| Write 1MB (atomic) | 6.03ms | ~166 MB/s | Write-sync-rename pattern |
| Write 1MB (direct) | 1.87ms | ~535 MB/s | No atomicity guarantees |
| Copy 1MB | 7.51ms | ~133 MB/s | APFS clone or kernel-assisted |
| Read 10MB | 17.80ms | ~562 MB/s | Large file throughput |
| Write 10MB | 17.01ms | ~588 MB/s | Large file throughput |
Measured with 1000 files × 100 loops (100,000 total iterations):
| Approach | Total Time | Per 1000 Files | Notes |
|---|---|---|---|
| Sync iterator | 32.90ms | 0.33ms | Blocking, fastest |
| Async batch 64 | 78.00ms | 0.78ms | Pull-based batching |
| Async batch 128 | 57.34ms | 0.57ms | Optimal batch size |
| Async batch 256 | 63.77ms | 0.64ms | Diminishing returns |
Async iteration trades ~1.7x overhead for non-blocking execution. The batching architecture reduces executor hops from N to N/batch_size, avoiding cooperative pool starvation.
| Operation | Median | Notes |
|---|---|---|
| Stream 1MB (64KB chunks) | 14.20ms | AsyncSequence with backpressure |
| Stream 1MB (4KB chunks) | 17.22ms | Smaller chunks, more overhead |
| Early termination | 16.29ms | Clean resource cleanup on break |
| Operation | Median | Notes |
|---|---|---|
| Submit 1000 jobs | 8.10ms | Job queue throughput |
| Sequential execution | 6.32ms | Per-job overhead |
| Handle registration | 7.37ms | 1000 handles |
Sync-to-sync benchmarks against Foundation's FileManager and Data APIs. This isolates syscall and API overhead from async scheduling:
| Operation | swift-file-system | Foundation | Difference |
|---|---|---|---|
| Read 1MB | 47µs | 53µs | 1.1x faster |
| Read 10MB | 2.36ms | 2.43ms | ~same |
| Read 100MB | 23.8ms | 24.4ms | ~same |
| Write 100MB | 24ms | 26ms | ~same |
| Iterate 100 files | 59µs | 367µs | 6x faster |
| Iterate 1000 files | 508µs | 1.81ms | 3.5x faster |
| Stat file | 2.5µs | 44µs | 17x faster |
| File exists | 4.2µs | 3.6µs | ~same |
| Copy 100MB | 316µs | 218µs | ~same (both use clonefile) |
swift-file-system provides substantial advantages for directory iteration (direct readdir vs Foundation's object creation) and metadata operations (direct stat vs attributesOfItem dictionary allocation).
Async benchmarks against swift-nio's _NIOFileSystem:
| Operation | swift-file-system | NIO FileSystem | Difference |
|---|---|---|---|
| Read 1MB | 6.05ms | 6.36ms | 1.05x faster |
| Write 1MB | 5.38ms | 5.08ms | 0.94x |
| Stat | 5.83ms | 7.80ms | 1.34x faster |
| Directory list (100 files) | 19.85ms | 91.36ms | 4.6x faster |
| Directory walk (100 entries) | 50.75ms | 91.41ms | 1.8x faster |
| Copy 1MB | 7.23ms | 7.07ms | ~same |
swift-file-system's pull-based batching architecture provides significant advantages for directory operations. File I/O performance is comparable, with slight variations depending on operation type.
Note: NIO benchmarks include async scheduling overhead. The Foundation comparison above isolates raw API performance.
The async executor maintains bounded memory regardless of workload:
- Queue limit prevents unbounded accumulation
- Batched iteration reduces intermediate allocations
- Handle store tracks open files without leaks
- Zero-allocation stat operations verified
Performance numbers are indicative, not guarantees. Measured on:
- Apple Silicon (M-series), macOS 26.0, Swift 6.2
- Release builds (
swift test -c release) - Warm filesystem cache (files read once before timing)
- APFS filesystem on internal NVMe SSD
- Statistical analysis: min, median, mean, p95, p99, stddev
Your results will vary based on hardware, filesystem, and workload characteristics.
┌─────────────────────────────────────────────┐
│ File System │ ← Public API: File, File.Directory
├─────────────────────────────────────────────┤
│ File System Primitives + Async (internal) │ ← Sync/async ops, platform abstraction
├─────────────────────────────────────────────┤
│ POSIX / Windows / Darwin │ ← System calls
└─────────────────────────────────────────────┘
| Level | Types | Use Case |
|---|---|---|
| Convenience | File, File.Directory |
Most common operations |
| Open/Handle | file.open, File.Handle |
Scoped low-level access |
| Primitive | File.System.* |
Advanced options (durability, symlinks) |
| Platform | Fallback Order |
|---|---|
| Darwin | copyfile(CLONE_FORCE) → copyfile(ALL/DATA) → manual loop |
| Linux | copy_file_range → sendfile → manual loop |
| Windows | CopyFileW → manual loop |
- Dedicated thread pool: Blocking I/O runs on separate threads, never starves Swift's cooperative pool
- Backpressure: Bounded queue with suspension when full
- Graceful shutdown: Completes in-flight work, fails pending jobs
| Platform | Status | Optimizations |
|---|---|---|
| macOS | Full | APFS cloning, copyfile(), kernel-assisted copy |
| iOS | Full | Same as macOS |
| Linux | Full | copy_file_range, sendfile |
| Windows | Build ✅ | CopyFileW, see note below |
| Platform | Status | Notes |
|---|---|---|
| macOS | Full | Cooperative and dedicated thread models |
| iOS | Full | Same as macOS |
| Linux | Full | Same as macOS |
| Windows | Build ✅ | Executor architecture ready |
| Platform | Status | Notes |
|---|---|---|
| macOS | Planned | FSEvents integration pending |
| iOS | Planned | FSEvents integration pending |
| Linux | Planned | inotify integration pending |
| Windows | Planned | ReadDirectoryChangesW pending |
The library builds successfully on Windows with Swift 6.2. However, tests are currently skipped on Windows CI due to a Swift compiler crash when compiling swift-syntax (a transitive dependency used for test macros).
| Aspect | Status |
|---|---|
| Library build | ✅ Passes |
| Tests | ⏸️ Skipped (swift-syntax compiler crash) |
| Core functionality | Ready for use |
The underlying issue is in the Swift toolchain on Windows, not this library. Once Swift on Windows matures, full test coverage will be enabled. Contributions welcome.
This library intentionally does not provide:
- Virtual filesystem abstraction: No pluggable backends or mock filesystems. Use protocol abstraction at a higher layer if needed.
- Security sandbox: Path validation is structural, not a security boundary. Do not use
File.Pathto prevent directory traversal attacks in untrusted input. - Database replacement: For transactional semantics across multiple files, use SQLite or a proper database.
- Watcher-first design: File watching is planned but not the primary focus. For complex watching needs, consider dedicated solutions.
- Complete Windows parity: Windows support covers core operations; advanced features (ACLs, alternate data streams) are not prioritized.
# All tests (1143 tests in 381 suites)
swift test
# Specific test suites
swift test --filter "File.System.Copy"
swift test --filter "File.IO.Executor"
swift test --filter "EdgeCase"
# Performance tests
swift test --filter "Performance"Test coverage includes:
- Copy semantics (attributes, symlinks, overwrite)
- Atomic write durability modes
- Async batching and cancellation
- Executor thread model isolation
- Edge cases (empty files, long paths, unicode)
- Cross-platform behavior
- apple/swift-system: Low-level system types
- apple/swift-async-algorithms: Async sequence algorithms
- swift-standards: Binary serialization, time types
Contributions welcome. Please:
- Add tests - maintain coverage for new features
- Follow conventions - Swift 6, strict concurrency, no force-unwraps
- Update docs - inline documentation and README updates
Areas for contribution:
- Windows feature parity
- File watching implementation (FSEvents, inotify)
- Performance optimizations
- Additional edge case coverage
Apache 2.0 - See LICENSE for details.