A modern I/O library for Android, Java, and Kotlin Multiplatform
—
This document covers Okio's cross-platform file system APIs, including path manipulation, file I/O operations, directory management, and metadata handling.
Path represents a hierarchical address on a file system, supporting both UNIX and Windows path conventions.
expect class Path {
// Core properties
val root: Path? // Root path (null for relative paths)
val segments: List<String> // Path components
val isAbsolute: Boolean // True if path is absolute
val isRelative: Boolean // True if path is relative
val volumeLetter: Char? // Windows volume letter (e.g., 'C')
// Path components
val name: String // Last segment (file/directory name)
val parent: Path? // Parent path (null for roots)
val isRoot: Boolean // True if this equals root
// Path operations
operator fun div(child: String): Path
operator fun div(child: ByteString): Path
operator fun div(child: Path): Path
fun resolve(child: String, normalize: Boolean = false): Path
fun resolve(child: ByteString, normalize: Boolean = false): Path
fun resolve(child: Path, normalize: Boolean = false): Path
fun relativeTo(other: Path): Path
fun normalized(): Path
// Comparison and string representation
override fun equals(other: Any?): Boolean
override fun hashCode(): Int
override fun toString(): String
companion object {
val DIRECTORY_SEPARATOR: String
}
}// String to Path conversion
fun String.toPath(normalize: Boolean = false): Path// Creating paths
val absolutePath = "/Users/john/documents/file.txt".toPath()
val relativePath = "documents/file.txt".toPath()
// Path properties
println("Is absolute: ${absolutePath.isAbsolute}") // true
println("Root: ${absolutePath.root}") // "/"
println("Name: ${absolutePath.name}") // "file.txt"
println("Parent: ${absolutePath.parent}") // "/Users/john/documents"
println("Segments: ${absolutePath.segments}") // ["Users", "john", "documents", "file.txt"]
// Path manipulation
val documentsDir = "/Users/john".toPath()
val fileInDocs = documentsDir / "documents" / "file.txt"
val backupFile = fileInDocs.parent!! / "backup" / fileInDocs.name
// Relative paths
val base = "/Users/john".toPath()
val target = "/Users/john/documents/file.txt".toPath()
val relative = target.relativeTo(base) // "documents/file.txt"
// Normalization
val messyPath = "/Users/john/../john/./documents/../documents/file.txt".toPath()
val cleanPath = messyPath.normalized() // "/Users/john/documents/file.txt"FileSystem provides read and write access to a hierarchical collection of files and directories.
expect abstract class FileSystem {
// Path resolution
abstract fun canonicalize(path: Path): Path
// Metadata operations
fun metadata(path: Path): FileMetadata
abstract fun metadataOrNull(path: Path): FileMetadata?
fun exists(path: Path): Boolean
// Directory listing
abstract fun list(dir: Path): List<Path>
abstract fun listOrNull(dir: Path): List<Path>?
open fun listRecursively(dir: Path, followSymlinks: Boolean = false): Sequence<Path>
// File I/O
abstract fun openReadOnly(file: Path): FileHandle
abstract fun openReadWrite(
file: Path,
mustCreate: Boolean = false,
mustExist: Boolean = false
): FileHandle
abstract fun source(file: Path): Source
inline fun <T> read(file: Path, readerAction: BufferedSource.() -> T): T
abstract fun sink(file: Path, mustCreate: Boolean = false): Sink
inline fun <T> write(
file: Path,
mustCreate: Boolean = false,
writerAction: BufferedSink.() -> T
): T
abstract fun appendingSink(file: Path, mustExist: Boolean = false): Sink
// Directory operations
abstract fun createDirectory(dir: Path, mustCreate: Boolean = false)
fun createDirectories(dir: Path, mustCreate: Boolean = false)
// File management
abstract fun atomicMove(source: Path, target: Path)
open fun copy(source: Path, target: Path)
abstract fun delete(path: Path, mustExist: Boolean = false)
open fun deleteRecursively(fileOrDirectory: Path, mustExist: Boolean = false)
// Symbolic links
abstract fun createSymlink(source: Path, target: Path)
companion object {
val SYSTEM_TEMPORARY_DIRECTORY: Path
val SYSTEM: FileSystem
}
}// Basic file operations
val fs = FileSystem.SYSTEM
val filePath = "/tmp/example.txt".toPath()
// Write to file
fs.write(filePath) {
writeUtf8("Hello, Okio FileSystem!")
writeUtf8("\n")
writeUtf8("This is a test file.")
}
// Read from file
val content = fs.read(filePath) {
readUtf8()
}
println("File content: $content")
// Check if file exists
if (fs.exists(filePath)) {
val metadata = fs.metadata(filePath)
println("File size: ${metadata.size} bytes")
println("Last modified: ${metadata.lastModifiedAtMillis}")
}
// Directory operations
val dirPath = "/tmp/test-directory".toPath()
fs.createDirectory(dirPath)
// List directory contents
val files = fs.list(dirPath)
files.forEach { path ->
println("Found: ${path.name}")
}
// Recursive directory listing
val homeDir = System.getProperty("user.home").toPath()
fs.listRecursively(homeDir / "Documents")
.take(10)
.forEach { path ->
println("File: $path")
}
// Copy and move operations
val sourceFile = "/tmp/source.txt".toPath()
val targetFile = "/tmp/target.txt".toPath()
fs.write(sourceFile) { writeUtf8("Source content") }
fs.copy(sourceFile, targetFile)
fs.atomicMove(targetFile, "/tmp/moved.txt".toPath())
// Cleanup
fs.delete(sourceFile)
fs.delete("/tmp/moved.txt".toPath())
fs.deleteRecursively(dirPath)FileHandle provides both streaming and random access I/O for open files.
abstract class FileHandle : Closeable {
// Properties
val readWrite: Boolean
// Random access I/O
fun read(fileOffset: Long, array: ByteArray, arrayOffset: Int = 0, byteCount: Int = array.size): Int
fun read(fileOffset: Long, sink: Buffer, byteCount: Long): Long
fun write(fileOffset: Long, array: ByteArray, arrayOffset: Int = 0, byteCount: Int = array.size)
fun write(fileOffset: Long, source: Buffer, byteCount: Long)
// File properties
fun size(): Long
fun resize(size: Long)
fun flush()
// Streaming I/O
fun source(fileOffset: Long = 0L): Source
fun sink(fileOffset: Long = 0L): Sink
fun appendingSink(): Sink
// Position management
fun position(source: Source): Long
fun reposition(source: Source, position: Long)
fun position(sink: Sink): Long
fun reposition(sink: Sink, position: Long)
override fun close()
}// Random access file I/O
val fs = FileSystem.SYSTEM
val file = "/tmp/random-access.txt".toPath()
// Create file with some content
fs.write(file) {
writeUtf8("0123456789ABCDEF")
}
fs.openReadWrite(file).use { handle ->
// Read from specific position
val buffer = Buffer()
val bytesRead = handle.read(5L, buffer, 5L) // Read 5 bytes starting at offset 5
println("Read: ${buffer.readUtf8()}") // "56789"
// Write at specific position
val writeBuffer = Buffer().writeUtf8("HELLO")
handle.write(10L, writeBuffer, 5L) // Write "HELLO" at offset 10
// Get file size
println("File size: ${handle.size()}")
// Flush changes
handle.flush()
}
// Streaming with position control
fs.openReadOnly(file).use { handle ->
val source = handle.source(0L) // Start from beginning
val bufferedSource = source.buffer()
// Read first part
val firstPart = bufferedSource.readUtf8(5)
println("First part: $firstPart")
// Change position
handle.reposition(source, 10L)
val secondPart = bufferedSource.readUtf8(5)
println("Second part: $secondPart")
}FileMetadata contains information about files and directories.
class FileMetadata(
val isRegularFile: Boolean = false,
val isDirectory: Boolean = false,
val symlinkTarget: Path? = null,
val size: Long? = null,
val createdAtMillis: Long? = null,
val lastModifiedAtMillis: Long? = null,
val lastAccessedAtMillis: Long? = null,
val extras: Map<KClass<*>, Any> = emptyMap()
) {
fun <T : Any> extra(type: KClass<out T>): T?
fun copy(
isRegularFile: Boolean = this.isRegularFile,
isDirectory: Boolean = this.isDirectory,
symlinkTarget: Path? = this.symlinkTarget,
size: Long? = this.size,
createdAtMillis: Long? = this.createdAtMillis,
lastModifiedAtMillis: Long? = this.lastModifiedAtMillis,
lastAccessedAtMillis: Long? = this.lastAccessedAtMillis,
extras: Map<KClass<*>, Any> = this.extras
): FileMetadata
}val fs = FileSystem.SYSTEM
val file = "/tmp/metadata-test.txt".toPath()
// Create a test file
fs.write(file) {
writeUtf8("Test content for metadata")
}
// Get file metadata
val metadata = fs.metadata(file)
println("Is regular file: ${metadata.isRegularFile}")
println("Is directory: ${metadata.isDirectory}")
println("File size: ${metadata.size} bytes")
metadata.lastModifiedAtMillis?.let { timestamp ->
val date = java.util.Date(timestamp)
println("Last modified: $date")
}
metadata.createdAtMillis?.let { timestamp ->
val date = java.util.Date(timestamp)
println("Created: $date")
}
// Check for symbolic links
if (metadata.symlinkTarget != null) {
println("Symbolic link target: ${metadata.symlinkTarget}")
}
// Directory metadata
val dirPath = "/tmp".toPath()
val dirMetadata = fs.metadata(dirPath)
println("Is directory: ${dirMetadata.isDirectory}")ForwardingFileSystem is an abstract decorator for applying monitoring, encryption, compression, or filtering to file operations.
abstract class ForwardingFileSystem(
protected val delegate: FileSystem
) : FileSystem {
// All methods delegate to the wrapped FileSystem by default
// Override specific methods to add custom behavior
override fun canonicalize(path: Path): Path = delegate.canonicalize(path)
override fun metadataOrNull(path: Path): FileMetadata? = delegate.metadataOrNull(path)
override fun list(dir: Path): List<Path> = delegate.list(dir)
// ... other delegating methods
}// Example: Logging FileSystem
class LoggingFileSystem(delegate: FileSystem) : ForwardingFileSystem(delegate) {
override fun source(file: Path): Source {
println("Opening source for: $file")
return delegate.source(file)
}
override fun sink(file: Path, mustCreate: Boolean): Sink {
println("Opening sink for: $file (mustCreate: $mustCreate)")
return delegate.sink(file, mustCreate)
}
override fun delete(path: Path, mustExist: Boolean) {
println("Deleting: $path")
delegate.delete(path, mustExist)
}
}
// Usage
val loggingFs = LoggingFileSystem(FileSystem.SYSTEM)
loggingFs.write("/tmp/logged-file.txt".toPath()) {
writeUtf8("This write operation will be logged")
}// Working with temporary directory
val tempDir = FileSystem.SYSTEM_TEMPORARY_DIRECTORY
val tempFile = tempDir / "okio-temp-${System.currentTimeMillis()}.txt"
FileSystem.SYSTEM.write(tempFile) {
writeUtf8("Temporary file content")
}
// Cleanup temporary file
FileSystem.SYSTEM.delete(tempFile)val fs = FileSystem.SYSTEM
val sourceFile = "/tmp/source.txt".toPath()
val targetFile = "/tmp/target.txt".toPath()
// Create source file
fs.write(sourceFile) {
writeUtf8("Important data that must be moved atomically")
}
// Atomic move ensures no partial state
fs.atomicMove(sourceFile, targetFile)
// Source file no longer exists, target file has the content
println("Source exists: ${fs.exists(sourceFile)}") // false
println("Target exists: ${fs.exists(targetFile)}") // trueval fs = FileSystem.SYSTEM
val baseDir = "/tmp/recursive-test".toPath()
// Create directory structure
fs.createDirectories(baseDir / "level1" / "level2" / "level3")
// Create files at various levels
fs.write(baseDir / "root-file.txt") { writeUtf8("Root level") }
fs.write(baseDir / "level1" / "level1-file.txt") { writeUtf8("Level 1") }
fs.write(baseDir / "level1" / "level2" / "level2-file.txt") { writeUtf8("Level 2") }
// List all files recursively
println("All files in directory tree:")
fs.listRecursively(baseDir).forEach { path ->
val metadata = fs.metadataOrNull(path)
val type = when {
metadata?.isDirectory == true -> "[DIR]"
metadata?.isRegularFile == true -> "[FILE]"
else -> "[OTHER]"
}
println("$type $path")
}
// Delete entire directory tree
fs.deleteRecursively(baseDir)File system operations can throw various exceptions:
expect open class IOException : Exception
expect class FileNotFoundException : IOException
expect class FileAlreadyExistsException : IOException
expect class DirectoryNotEmptyException : IOException
expect class AccessDeniedException : IOExceptionval fs = FileSystem.SYSTEM
val nonExistentFile = "/path/that/does/not/exist.txt".toPath()
try {
// This will throw FileNotFoundException
fs.source(nonExistentFile)
} catch (e: FileNotFoundException) {
println("File not found: ${e.message}")
}
try {
// This will throw DirectoryNotEmptyException if directory has contents
fs.delete("/tmp".toPath())
} catch (e: DirectoryNotEmptyException) {
println("Directory not empty: ${e.message}")
}
// Safer operations using null-returning variants
val metadata = fs.metadataOrNull(nonExistentFile)
if (metadata != null) {
println("File exists with size: ${metadata.size}")
} else {
println("File does not exist")
}
val files = fs.listOrNull(nonExistentFile)
if (files != null) {
println("Directory contains ${files.size} items")
} else {
println("Directory does not exist or is not accessible")
}/ as path separator// Check platform capabilities
val fs = FileSystem.SYSTEM
val testDir = "/tmp/platform-test".toPath()
fs.createDirectory(testDir)
try {
// Symbolic links (POSIX systems)
val linkTarget = testDir / "target.txt"
val linkPath = testDir / "symlink.txt"
fs.write(linkTarget) { writeUtf8("Target content") }
fs.createSymlink(linkPath, linkTarget)
val linkMetadata = fs.metadata(linkPath)
if (linkMetadata.symlinkTarget != null) {
println("Symbolic link supported, points to: ${linkMetadata.symlinkTarget}")
}
} catch (e: Exception) {
println("Symbolic links not supported: ${e.message}")
} finally {
fs.deleteRecursively(testDir)
}Install with Tessl CLI
npx tessl i tessl/maven-com-squareup-okio--okio