Open source
Introducing FTPServerKit
Our second open-source release: a lightweight, read-only FTP server you embed in iOS and macOS apps, written in Swift 6.
FTPServerKit is a Swift package that runs a small, read-only FTP server inside your iOS or macOS app. Any FTP client on the local network can then browse and download the files you share: FileZilla, Cyberduck, Finder, curl, or an embedded device that fetches files over FTP. It is built on Network.framework with no dependencies and is MIT-licensed. The code is on GitHub and the package is on the Swift Package Index.
Why I built it
I built FTPServerKit for the EVBox Connect iOS app, where it’s in use today. An FTP server is a niche thing for an app to need, but when you do need one, it helps to have it as a small package you can drop in, so I decided to open-source it.
Install
FTPServerKit needs iOS 15 or macOS 12 and Swift 6 (Xcode 16 or later). Add it in Xcode with File → Add Package Dependencies…, or in Package.swift:
dependencies: [
.package(url: "https://github.com/CodeInstinctAI/FTPServerKit.git", from: "0.1.0"),
],
targets: [
.target(name: "MyApp", dependencies: ["FTPServerKit"]),
]
On iOS, add NSLocalNetworkUsageDescription to your app’s Info.plist. iOS shows its text when it asks the user for local network access.
Share a directory
Create a server with a configuration and a file provider, start it and show the user where to connect:
import FTPServerKit
let documents = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let server = FTPServer(
configuration: FTPServerConfiguration(
port: 2121,
authentication: .credentials(username: "user", password: "pass")
),
fileProvider: FTPDirectoryProvider(rootURL: documents)
)
let port = try await server.start()
let address = IPAddressHelper.getLocalIPAddress() ?? "localhost"
print("Connect to ftp://\(address):\(port)")
start() returns once clients can connect, and throws when the server can’t listen, for example because the port is already in use. Pass port 0 and the system picks a free one.
The directory provider serves subdirectories too and picks up files you add or remove while the server runs. Clients can’t reach anything outside the directory, not even through symbolic links, and hidden files stay hidden unless you ask for them. Downloads are streamed in chunks, so a large file is never loaded into memory, and clients can resume an interrupted download.
One file, or your own source
To share a single file, use FTPSingleFileProvider(fileURL:) instead. For anything else, such as files in memory or in a database, conform a type to the FTPFileProvider protocol. It answers three questions: what is at a path, what a directory contains, and the bytes of a file. You can swap the provider while the server runs, and the next command a client sends uses the new one.
Follow along
The server reports what it does as events: started, stopped, every command a client sends and every reply. You can receive them as an AsyncStream, a Combine publisher or a delegate, whichever fits your codebase:
for await event in server.events {
if case .receivedCommand(let command, let argument, _) = event {
print(command, argument) // PASS arguments are masked
}
}
There’s also a log handler that receives every log line, with a connection ID so you can tell clients apart. That’s handy to attach a client’s session to a bug report.
Older and embedded clients
Desktop clients work with the default configuration. Some clients, often in embedded devices, don’t quite follow the FTP spec: some lose the end of a file when the server closes the connection right after the last byte, and some resume a download without asking for a new data port. FTPServerKit has opt-in workarounds for both:
var configuration = FTPServerConfiguration(authentication: .anonymous)
configuration.legacyClientOptions = FTPLegacyClientOptions(
transferCompletionDelay: 30,
keepsPassiveListenerOpen: true
)
They’re off by default, because they make the server slower for clients that do follow the spec.
Good to know
- Read-only. Uploads, deletes and renames are refused. Clients have to use passive mode, which they do by default.
- Background. iOS suspends an app’s network listeners soon after it goes to the background. Stop the server when your app does, and start it again when the app becomes active.
Documentation and example app
The full documentation, with guides on getting started, observing the server, custom file providers and older clients, is on the Swift Package Index. The repository also includes a SwiftUI example app that shares the files you pick with any FTP client on the network.
Releases
- 0.1.0, October 6, 2026. The first release, with directory, single-file and custom file providers, passive mode, resume, streamed downloads, events and a log handler, and the workarounds for older clients.
The full notes are on the releases page.
Get involved
FTPServerKit is our second open-source package, after Artisan Docs. If you run into a bug or miss a feature, open an issue. Pull requests are welcome too. And if it saves you time, a star on GitHub helps other Swift developers find it.