
Unified file-system abstraction with stream-first APIs for discovering, reading, and writing files, recursive extension-filtered discovery, storage access framework support, stack-based traversal, and lightweight platform detection.
A Kotlin Multiplatform file-system abstraction for working with files and directories across JVM, Android, and iOS through a consistent API.
filesystem provides a common way to perform file operations while
allowing each platform to use its native storage mechanisms. On Android,
this includes regular filesystem paths and Storage Access Framework
(SAF) documents. On iOS, it uses Foundation file APIs.
FileOperation wrapper for common file metadata and operations.FileSystemUtil abstraction for platform-specific filesystem
implementations.kotlinx-io.Add the published artifact for the platform you are targeting. Check the project's Maven Central page or repository README for the current coordinates and version:
For example, in a Gradle Kotlin DSL dependency block, use the appropriate published artifact:
dependencies {
implementation("io.github.sifisofakude.filesystem:filesystem:1.0.0")
}The library exposes a current platform filesystem through
FileSystems.current. The platform implementation is expected to
initialize this property; applications generally should not assign it
themselves.
import io.github.sifisofakude.filesystem.FileOperation
import io.github.sifisofakude.filesystem.FileSystems
val fs = FileSystems.current
val file = FileOperation("documents/hello.txt")
file.parent
file.name
file.extension
file.length()
file.writeText("Hello from filesystem")
val contents = file.readText()The exact path interpretation depends on the platform and filesystem configuration. On Android SAF and iOS, configure a selected directory when you want relative paths to be rooted in a user-selected or application-selected directory.
FileOperation provides a convenient wrapper around a path and exposes
common metadata and operations.
val file = FileOperation("documents/notes.txt")
val fileName = file.name
val nameWithoutExtension = file.nameWithoutExtension
val extension = file.extension
val parent = file.parent
val size = file.length()
val modified = file.lastModified()
if (file.exists()) {
println("File exists: $fileName")
}Commonly used properties and operations include:
path --- the path represented by the operation.name --- the final name component.nameWithoutExtension --- the name without its final extension.extension --- the file extension.parent --- the parent path, when available.length() --- the file length.lastModified() --- last-modified timestamp as provided by the
platform implementation.exists() --- whether the target exists.isFile() --- whether the target is a file.isDirectory() --- whether the target is a directory.mkdir() --- create a directory when its parent already exists.mkdirs() --- create the directory and missing parent directories.delete() --- delete the target.copy(dst) --- copy the target to a destination.move(dst) --- move the target to a destination.renameTo(...) --- rename the target.listFiles() --- list child entries.val directory = FileOperation("documents/reports")
directory.mkdirs()
val report = FileOperation("documents/reports/summary.txt")
report.writeText("Report contents")mkdir() is intended for creating a directory whose parent already
exists. Use mkdirs() when the parent directories may also need to be
created.
val source = FileOperation("documents/source.txt")
source.copy("documents/copy.txt")
source.move("documents/archive/source.txt")Do not assume that copy or move is atomic across storage providers.
FileSystemUtil defines the common filesystem abstraction implemented
by platform-specific filesystem classes.
import io.github.sifisofakude.filesystem.FileSystems
val fs = FileSystems.currentUse the current implementation for operations that need to work with the active platform's storage provider. The implementation may use regular filesystem paths, or platform-native APIs.
Responsibilities of the filesystem abstraction include:
kotlinx-io sources and sinks.Not every platform or storage provider has identical capabilities. For example, Android SAF providers may impose restrictions that do not apply to ordinary filesystem paths.
FileOperation is the convenient wrapper; FileSystemUtil is the lower-level API when you want to call the filesystem implementation directly.
import io.github.sifisofakude.filesystem.FileSystems
import io.github.sifisofakude.filesystem.FileSystemUtil
val fs: FileSystemUtil = FileSystems.currentFileSystems.current is initialized by the platform-specific implementation. In normal application code, retrieve it rather than replacing it.
val directoryCreated = fs.createDirectory("project/src")
val fileCreated = fs.createFile("project/src/Main.kt")Use the return values to check whether the operation succeeded. For nested paths, use the directory-creation behavior provided by the implementation.
val success = fs.writeText(
outputFile = "project/README.txt",
text = "# My project"
)
val appended = fs.appendText(
outputFile = "project/README.txt",
text = "\nMore information"
)
val contents = fs.readText("project/README.txt")These methods operate on paths understood by the active implementation. On Android, a path may refer to a SAF-backed document; on JVM it normally refers to a conventional filesystem path.
The JVM-compatible stream API exposes openInputStream and openOutputStream:
val input = fs.openInputStream("project/input.bin")
val output = fs.openOutputStream("project/output.bin")
if (input != null && output != null) {
input.use { source ->
output.use { destination ->
fs.copy(source, destination)
}
}
}The API signatures include:
openInputStream(path: String): InputStream?
openOutputStream(path: String, append: Boolean = false): OutputStream?
copy(input: InputStream, output: OutputStream)These Java stream methods are for compatible platform/source sets; do not call InputStream APIs from common code that must compile for iOS.
val copied = fs.copy(
src = "project/README.txt",
dst = "project/README-backup.txt"
)
val moved = fs.move(
src = "project/README-backup.txt",
dst = "archive/README-backup.txt"
)
val renamed = fs.rename(
src = "project/README.txt",
target = "README-old.txt"
)
val deleted = fs.delete("project/README-old.txt")val exists = fs.exists("project/src/Main.kt")
val isFile = fs.isFile("project/src/Main.kt")
val isDirectory = fs.isDirectory("project/src")
val name = fs.getName("project/src/Main.kt")
val extension = fs.getExtension("project/src/Main.kt")
val parent = fs.getParentFile("project/src/Main.kt")
val size = fs.size("project/src/Main.kt")
val modified = fs.lastModified("project/src/Main.kt")Use these calls when you need metadata or checks without wrapping every path in FileOperation.
For direct discovery, the API includes findFiles and resolveFiles:
val kotlinFiles = fs.findFiles(
directory = "project/src",
extensions = setOf("kt")
)
val sources = fs.resolveFiles(
inputFiles = listOf("project/src"),
extensions = setOf("kt", "java")
)
sources.forEach { source ->
println("Relative: ${source.relativePath}")
println("Absolute: ${source.absolutePath}")
}findFiles returns matching paths. resolveFiles returns FileSource entries, which preserve relative and
absolute path information for later processing.
In common/platform-neutral code, prefer the Source/Sink API rather than Java streams:
val source = fs.openSource("project/input.txt")
val sink = fs.openSink("project/output.txt")
if (source != null && sink != null) {
source.use { input ->
sink.use { output ->
input.transferTo(output)
}
}
}The lower-level API includes these declarations:
abstract fun openSource(path: String): Source?
abstract fun openSink(path: String, append: Boolean = false): Sink?The Android SAF implementation expose helpers for materializing SAF content into app-managed local storage
when a downstream API requires a conventional file path. The available API also includes materialization cleanup
helper such as clearMaterialized. Refer to the Android implementation's actual
method signatures for the destination and cleanup behavior.
FileSource represents a discovered file and carry both a relative
path and an absolute path.
// obtain FileSource instances from resolveFiles().
val relativePath = fileSource.relativePath
val absolutePath = fileSource.absolutePathThe relative path is useful when preserving a directory structure, while the absolute path identifies the resolved location for the current filesystem implementation.
A discovery workflow looks like this:
val root = FileOperation("documents")
val entries = root.listFiles()
entries.forEach { entry ->
println(entry.name)
}Use the common stream APIs when you want to process content without loading an entire file into memory.
val file = FileOperation("documents/data.txt")
file.writeText("Some content")
file.openSource()?.use {
// read from source
}The JVM implementation also provides JVM-specific InputStream and
OutputStream helpers for code that needs Java stream interoperability.
These APIs are platform-specific and should be used only from compatible
source sets.
For large files, prefer streaming rather than readText().
The Android implementation can work with ordinary paths and SAF-backed documents. SAF is useful when users choose files or directories through Android's document picker, including storage locations that the application cannot access through ordinary filesystem paths.
The Android SAF implementation supports configuring a selected directory using a tree URI:
// treeUri is the URI returned by the Android directory picker.
// Persist the URI permission when appropriate before using it.
val safFileSystem = AndroidSafFileSystem(context)
safFileSystem.changeSelectedDirectory(treeUri)Once a directory is selected, relative paths can be resolved against that selected directory.
The implementation use an internal representation such as:
treeUri||relative/path/to/file.txt
This representation associates a relative path with a selected document-tree URI. Treat it as an implementation detail rather than a user-facing path format.
Use the library's common operations where possible rather than manually
repeating DocumentFile or DocumentsContract handling throughout your
application. The backing provider may still restrict operations such as
renaming, deleting, or creating files.
Some workflows need a regular local file instead of a SAF URI---for example, when a third-party API requires a filesystem path. The Android implementation includes materialization support for copying accessible SAF content into app-managed storage.
Remember that materializing a file creates a local copy; it does not make that copy automatically synchronize with the original document.
The iOS implementation uses Foundation filesystem APIs and supports a selected directory as the root for relative paths.
Configure the selected directory before using relative paths:
// Example using the iOS filesystem implementation.
val fs = IosFileSystem()
fs.changeSelectedDirectory("/path/to/directory")Use the actual public API and path format exposed by the iOS artifact.
Absolute paths are handled separately from paths resolved against the
selected root. Passing null to clear the selected directory is
supported only if the version you use explicitly implements that
behavior.
The implementation integrates with kotlinx-io sources and sinks for
stream-oriented operations.
The JVM implementation works with conventional filesystem paths and can
expose Java stream interoperability helpers such as InputStream and
OutputStream.
val file = FileOperation("documents/example.txt")
file.openInputStream()?.use {
// read stream
}
file.openOutputStream()?.use {
// write to stream
}The library has a PlatformDetector helper for determining platform
information where that API is available. Platform-specific behavior
should generally be isolated in the appropriate source set instead of
spreading platform checks throughout shared business logic.
See the repository's LICENSE file for the license terms.
A Kotlin Multiplatform file-system abstraction for working with files and directories across JVM, Android, and iOS through a consistent API.
filesystem provides a common way to perform file operations while
allowing each platform to use its native storage mechanisms. On Android,
this includes regular filesystem paths and Storage Access Framework
(SAF) documents. On iOS, it uses Foundation file APIs.
FileOperation wrapper for common file metadata and operations.FileSystemUtil abstraction for platform-specific filesystem
implementations.kotlinx-io.Add the published artifact for the platform you are targeting. Check the project's Maven Central page or repository README for the current coordinates and version:
For example, in a Gradle Kotlin DSL dependency block, use the appropriate published artifact:
dependencies {
implementation("io.github.sifisofakude.filesystem:filesystem:1.0.0")
}The library exposes a current platform filesystem through
FileSystems.current. The platform implementation is expected to
initialize this property; applications generally should not assign it
themselves.
import io.github.sifisofakude.filesystem.FileOperation
import io.github.sifisofakude.filesystem.FileSystems
val fs = FileSystems.current
val file = FileOperation("documents/hello.txt")
file.parent
file.name
file.extension
file.length()
file.writeText("Hello from filesystem")
val contents = file.readText()The exact path interpretation depends on the platform and filesystem configuration. On Android SAF and iOS, configure a selected directory when you want relative paths to be rooted in a user-selected or application-selected directory.
FileOperation provides a convenient wrapper around a path and exposes
common metadata and operations.
val file = FileOperation("documents/notes.txt")
val fileName = file.name
val nameWithoutExtension = file.nameWithoutExtension
val extension = file.extension
val parent = file.parent
val size = file.length()
val modified = file.lastModified()
if (file.exists()) {
println("File exists: $fileName")
}Commonly used properties and operations include:
path --- the path represented by the operation.name --- the final name component.nameWithoutExtension --- the name without its final extension.extension --- the file extension.parent --- the parent path, when available.length() --- the file length.lastModified() --- last-modified timestamp as provided by the
platform implementation.exists() --- whether the target exists.isFile() --- whether the target is a file.isDirectory() --- whether the target is a directory.mkdir() --- create a directory when its parent already exists.mkdirs() --- create the directory and missing parent directories.delete() --- delete the target.copy(dst) --- copy the target to a destination.move(dst) --- move the target to a destination.renameTo(...) --- rename the target.listFiles() --- list child entries.val directory = FileOperation("documents/reports")
directory.mkdirs()
val report = FileOperation("documents/reports/summary.txt")
report.writeText("Report contents")mkdir() is intended for creating a directory whose parent already
exists. Use mkdirs() when the parent directories may also need to be
created.
val source = FileOperation("documents/source.txt")
source.copy("documents/copy.txt")
source.move("documents/archive/source.txt")Do not assume that copy or move is atomic across storage providers.
FileSystemUtil defines the common filesystem abstraction implemented
by platform-specific filesystem classes.
import io.github.sifisofakude.filesystem.FileSystems
val fs = FileSystems.currentUse the current implementation for operations that need to work with the active platform's storage provider. The implementation may use regular filesystem paths, or platform-native APIs.
Responsibilities of the filesystem abstraction include:
kotlinx-io sources and sinks.Not every platform or storage provider has identical capabilities. For example, Android SAF providers may impose restrictions that do not apply to ordinary filesystem paths.
FileOperation is the convenient wrapper; FileSystemUtil is the lower-level API when you want to call the filesystem implementation directly.
import io.github.sifisofakude.filesystem.FileSystems
import io.github.sifisofakude.filesystem.FileSystemUtil
val fs: FileSystemUtil = FileSystems.currentFileSystems.current is initialized by the platform-specific implementation. In normal application code, retrieve it rather than replacing it.
val directoryCreated = fs.createDirectory("project/src")
val fileCreated = fs.createFile("project/src/Main.kt")Use the return values to check whether the operation succeeded. For nested paths, use the directory-creation behavior provided by the implementation.
val success = fs.writeText(
outputFile = "project/README.txt",
text = "# My project"
)
val appended = fs.appendText(
outputFile = "project/README.txt",
text = "\nMore information"
)
val contents = fs.readText("project/README.txt")These methods operate on paths understood by the active implementation. On Android, a path may refer to a SAF-backed document; on JVM it normally refers to a conventional filesystem path.
The JVM-compatible stream API exposes openInputStream and openOutputStream:
val input = fs.openInputStream("project/input.bin")
val output = fs.openOutputStream("project/output.bin")
if (input != null && output != null) {
input.use { source ->
output.use { destination ->
fs.copy(source, destination)
}
}
}The API signatures include:
openInputStream(path: String): InputStream?
openOutputStream(path: String, append: Boolean = false): OutputStream?
copy(input: InputStream, output: OutputStream)These Java stream methods are for compatible platform/source sets; do not call InputStream APIs from common code that must compile for iOS.
val copied = fs.copy(
src = "project/README.txt",
dst = "project/README-backup.txt"
)
val moved = fs.move(
src = "project/README-backup.txt",
dst = "archive/README-backup.txt"
)
val renamed = fs.rename(
src = "project/README.txt",
target = "README-old.txt"
)
val deleted = fs.delete("project/README-old.txt")val exists = fs.exists("project/src/Main.kt")
val isFile = fs.isFile("project/src/Main.kt")
val isDirectory = fs.isDirectory("project/src")
val name = fs.getName("project/src/Main.kt")
val extension = fs.getExtension("project/src/Main.kt")
val parent = fs.getParentFile("project/src/Main.kt")
val size = fs.size("project/src/Main.kt")
val modified = fs.lastModified("project/src/Main.kt")Use these calls when you need metadata or checks without wrapping every path in FileOperation.
For direct discovery, the API includes findFiles and resolveFiles:
val kotlinFiles = fs.findFiles(
directory = "project/src",
extensions = setOf("kt")
)
val sources = fs.resolveFiles(
inputFiles = listOf("project/src"),
extensions = setOf("kt", "java")
)
sources.forEach { source ->
println("Relative: ${source.relativePath}")
println("Absolute: ${source.absolutePath}")
}findFiles returns matching paths. resolveFiles returns FileSource entries, which preserve relative and
absolute path information for later processing.
In common/platform-neutral code, prefer the Source/Sink API rather than Java streams:
val source = fs.openSource("project/input.txt")
val sink = fs.openSink("project/output.txt")
if (source != null && sink != null) {
source.use { input ->
sink.use { output ->
input.transferTo(output)
}
}
}The lower-level API includes these declarations:
abstract fun openSource(path: String): Source?
abstract fun openSink(path: String, append: Boolean = false): Sink?The Android SAF implementation expose helpers for materializing SAF content into app-managed local storage
when a downstream API requires a conventional file path. The available API also includes materialization cleanup
helper such as clearMaterialized. Refer to the Android implementation's actual
method signatures for the destination and cleanup behavior.
FileSource represents a discovered file and carry both a relative
path and an absolute path.
// obtain FileSource instances from resolveFiles().
val relativePath = fileSource.relativePath
val absolutePath = fileSource.absolutePathThe relative path is useful when preserving a directory structure, while the absolute path identifies the resolved location for the current filesystem implementation.
A discovery workflow looks like this:
val root = FileOperation("documents")
val entries = root.listFiles()
entries.forEach { entry ->
println(entry.name)
}Use the common stream APIs when you want to process content without loading an entire file into memory.
val file = FileOperation("documents/data.txt")
file.writeText("Some content")
file.openSource()?.use {
// read from source
}The JVM implementation also provides JVM-specific InputStream and
OutputStream helpers for code that needs Java stream interoperability.
These APIs are platform-specific and should be used only from compatible
source sets.
For large files, prefer streaming rather than readText().
The Android implementation can work with ordinary paths and SAF-backed documents. SAF is useful when users choose files or directories through Android's document picker, including storage locations that the application cannot access through ordinary filesystem paths.
The Android SAF implementation supports configuring a selected directory using a tree URI:
// treeUri is the URI returned by the Android directory picker.
// Persist the URI permission when appropriate before using it.
val safFileSystem = AndroidSafFileSystem(context)
safFileSystem.changeSelectedDirectory(treeUri)Once a directory is selected, relative paths can be resolved against that selected directory.
The implementation use an internal representation such as:
treeUri||relative/path/to/file.txt
This representation associates a relative path with a selected document-tree URI. Treat it as an implementation detail rather than a user-facing path format.
Use the library's common operations where possible rather than manually
repeating DocumentFile or DocumentsContract handling throughout your
application. The backing provider may still restrict operations such as
renaming, deleting, or creating files.
Some workflows need a regular local file instead of a SAF URI---for example, when a third-party API requires a filesystem path. The Android implementation includes materialization support for copying accessible SAF content into app-managed storage.
Remember that materializing a file creates a local copy; it does not make that copy automatically synchronize with the original document.
The iOS implementation uses Foundation filesystem APIs and supports a selected directory as the root for relative paths.
Configure the selected directory before using relative paths:
// Example using the iOS filesystem implementation.
val fs = IosFileSystem()
fs.changeSelectedDirectory("/path/to/directory")Use the actual public API and path format exposed by the iOS artifact.
Absolute paths are handled separately from paths resolved against the
selected root. Passing null to clear the selected directory is
supported only if the version you use explicitly implements that
behavior.
The implementation integrates with kotlinx-io sources and sinks for
stream-oriented operations.
The JVM implementation works with conventional filesystem paths and can
expose Java stream interoperability helpers such as InputStream and
OutputStream.
val file = FileOperation("documents/example.txt")
file.openInputStream()?.use {
// read stream
}
file.openOutputStream()?.use {
// write to stream
}The library has a PlatformDetector helper for determining platform
information where that API is available. Platform-specific behavior
should generally be isolated in the appropriate source set instead of
spreading platform checks throughout shared business logic.
See the repository's LICENSE file for the license terms.