Skip to content

Commit

Permalink
Feat: Add Kotlin file system API
Browse files Browse the repository at this point in the history
  • Loading branch information
zhanghai committed Mar 27, 2024
1 parent 11003f3 commit 39b1f4a
Show file tree
Hide file tree
Showing 41 changed files with 1,806 additions and 0 deletions.
1 change: 1 addition & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ dependencies {
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])

implementation 'org.jetbrains.kotlinx:kotlinx-io-core:0.3.2'
// kotlinx-coroutines-android depends on kotlin-stdlib-jdk8
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
def kotlinx_coroutines_version = '1.8.0'
Expand Down
7 changes: 7 additions & 0 deletions app/src/main/java/me/zhanghai/kotlin/filesystem/AccessMode.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package me.zhanghai.kotlin.filesystem

public enum class AccessMode {
READ,
WRITE,
EXECUTE
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package me.zhanghai.kotlin.filesystem

public enum class BasicCopyFileOption : CopyFileOption {
REPLACE_EXISTING,
COPY_METADATA,
ATOMIC_MOVE
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package me.zhanghai.kotlin.filesystem

public enum class BasicDirectoryStreamOption : DirectoryStreamOption {
READ_METADATA
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package me.zhanghai.kotlin.filesystem

public enum class BasicFileContentOption : FileContentOption {
READ,
WRITE,
APPEND,
TRUNCATE_EXISTING,
CREATE,
CREATE_NEW
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package me.zhanghai.kotlin.filesystem

public interface CopyFileOption
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package me.zhanghai.kotlin.filesystem

public interface CreateFileOption : FileContentOption
11 changes: 11 additions & 0 deletions app/src/main/java/me/zhanghai/kotlin/filesystem/DirectoryEntry.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package me.zhanghai.kotlin.filesystem

import kotlinx.io.bytestring.ByteString

public interface DirectoryEntry {
public val name: ByteString

public val type: FileType

public val metadata: FileMetadata?
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package me.zhanghai.kotlin.filesystem

import me.zhanghai.kotlin.filesystem.io.AsyncCloseable

public interface DirectoryStream : AsyncCloseable {
public suspend fun read(): DirectoryEntry?
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package me.zhanghai.kotlin.filesystem

public interface DirectoryStreamOption
80 changes: 80 additions & 0 deletions app/src/main/java/me/zhanghai/kotlin/filesystem/FileContent.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package me.zhanghai.kotlin.filesystem

import kotlinx.io.Buffer
import kotlinx.io.IOException
import me.zhanghai.kotlin.filesystem.io.AsyncCloseable
import me.zhanghai.kotlin.filesystem.io.Sink
import me.zhanghai.kotlin.filesystem.io.Source
import kotlin.coroutines.cancellation.CancellationException

public interface FileContent : AsyncCloseable {
@Throws(CancellationException::class, IOException::class)
public suspend fun readAtMostTo(position: Long, sink: Buffer, byteCount: Long): Long

@Throws(CancellationException::class, IOException::class)
public suspend fun write(position: Long, source: Buffer, byteCount: Long)

@Throws(CancellationException::class, IOException::class) public suspend fun getSize()

@Throws(CancellationException::class, IOException::class) public suspend fun setSize(size: Long)

@Throws(CancellationException::class, IOException::class) public suspend fun sync()
}

public fun FileContent.openSource(position: Long = 0): Source = FileContentSource(this, position)

private class FileContentSource(private val fileContent: FileContent, private var position: Long) :
Source {
private var closed: Boolean = false

@Throws(CancellationException::class, IOException::class)
override suspend fun readAtMostTo(sink: Buffer, byteCount: Long): Long {
checkNotClosed()
val read = fileContent.readAtMostTo(position, sink, byteCount)
if (read != -1L) {
position += read
}
return read
}

private fun checkNotClosed() {
if (!closed) {
throw IOException("Source is closed")
}
}

@Throws(CancellationException::class, IOException::class)
override suspend fun close() {
closed = true
}
}

public fun FileContent.openSink(position: Long = 0): Sink = FileContentSink(this, position)

private class FileContentSink(private val fileContent: FileContent, private var position: Long) :
Sink {
private var closed: Boolean = false

@Throws(CancellationException::class, IOException::class)
override suspend fun write(source: Buffer, byteCount: Long) {
checkNotClosed()
fileContent.write(position, source, byteCount)
position += byteCount
}

@Throws(CancellationException::class, IOException::class)
override suspend fun flush() {
checkNotClosed()
}

private fun checkNotClosed() {
if (!closed) {
throw IOException("Sink is closed")
}
}

@Throws(CancellationException::class, IOException::class)
override suspend fun close() {
closed = true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package me.zhanghai.kotlin.filesystem

public interface FileContentOption
15 changes: 15 additions & 0 deletions app/src/main/java/me/zhanghai/kotlin/filesystem/FileMetadata.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package me.zhanghai.kotlin.filesystem

public interface FileMetadata {
public val id: Any

public val type: FileType

public val size: Long

public val lastModificationTimeMillis: Long

public val lastAccessTimeMillis: Long?

public val creationTimeMillis: Long?
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package me.zhanghai.kotlin.filesystem

public interface FileMetadataOption
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package me.zhanghai.kotlin.filesystem

import me.zhanghai.kotlin.filesystem.io.AsyncCloseable

public interface FileMetadataView : AsyncCloseable {
public suspend fun readMetadata(): FileMetadata

public suspend fun setTimes(
lastModificationTimeMillis: Long? = null,
lastAccessTimeMillis: Long? = null,
creationTimeMillis: Long? = null
)
}
7 changes: 7 additions & 0 deletions app/src/main/java/me/zhanghai/kotlin/filesystem/FileStore.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package me.zhanghai.kotlin.filesystem

import me.zhanghai.kotlin.filesystem.io.AsyncCloseable

public interface FileStore : AsyncCloseable {
public suspend fun readMetadata(): FileStoreMetadata
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package me.zhanghai.kotlin.filesystem

import kotlinx.io.bytestring.ByteString

public interface FileStoreMetadata {
public val type: ByteString

public val blockSize: Long

public val totalSpace: Long

public val freeSpace: Long

public val availableSpace: Long
}
118 changes: 118 additions & 0 deletions app/src/main/java/me/zhanghai/kotlin/filesystem/FileSystem.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package me.zhanghai.kotlin.filesystem

import kotlinx.io.IOException
import kotlinx.io.bytestring.ByteString
import me.zhanghai.kotlin.filesystem.io.AsyncCloseable
import me.zhanghai.kotlin.filesystem.io.Sink
import me.zhanghai.kotlin.filesystem.io.Source
import me.zhanghai.kotlin.filesystem.io.use
import me.zhanghai.kotlin.filesystem.io.withCloseable
import kotlin.coroutines.cancellation.CancellationException

public interface FileSystem : AsyncCloseable {
@Throws(CancellationException::class, IOException::class)
public suspend fun openMetadataView(
file: Path,
vararg options: FileMetadataOption
): FileMetadataView

@Throws(CancellationException::class, IOException::class)
public suspend fun readMetadata(file: Path, vararg options: FileMetadataOption): FileMetadata =
openMetadataView(file, *options).use { it.readMetadata() }

@Throws(CancellationException::class, IOException::class)
public suspend fun openContent(file: Path, vararg options: FileContentOption): FileContent

@Throws(CancellationException::class, IOException::class)
public suspend fun openSource(file: Path, vararg options: FileContentOption): Source {
require(BasicFileContentOption.WRITE !in options) { BasicFileContentOption.WRITE }
require(BasicFileContentOption.APPEND !in options) { BasicFileContentOption.APPEND }
return openContent(file, *options).let { it.openSource().withCloseable(it) }
}

@Throws(CancellationException::class, IOException::class)
public suspend fun openSink(
file: Path,
vararg options: FileContentOption = OPEN_SINK_OPTIONS_DEFAULT
): Sink {
require(BasicFileContentOption.READ !in options) { BasicFileContentOption.READ }
require(
BasicFileContentOption.WRITE in options || BasicFileContentOption.APPEND in options
) {
"Missing ${BasicFileContentOption.WRITE} or ${BasicFileContentOption.APPEND}"
}
return openContent(file, *options).let { it.openSink().withCloseable(it) }
}

@Throws(CancellationException::class, IOException::class)
public suspend fun openDirectoryStream(
directory: Path,
vararg options: DirectoryStreamOption
): DirectoryStream

@Throws(CancellationException::class, IOException::class)
public suspend fun readDirectory(
directory: Path,
vararg options: DirectoryStreamOption
): List<Path> =
openDirectoryStream(directory, *options).use { directoryStream ->
buildList {
while (true) {
val directoryEntry = directoryStream.read() ?: break
this += directory.resolve(directoryEntry.name)
}
}
}

@Throws(CancellationException::class, IOException::class)
public suspend fun createDirectory(directory: Path, vararg options: CreateFileOption)

@Throws(CancellationException::class, IOException::class)
public suspend fun readSymbolicLink(link: Path): ByteString

@Throws(CancellationException::class, IOException::class)
public suspend fun createSymbolicLink(
link: Path,
target: ByteString,
vararg options: CreateFileOption
)

@Throws(CancellationException::class, IOException::class)
public suspend fun createHardLink(link: Path, existing: Path)

@Throws(CancellationException::class, IOException::class) public suspend fun delete(path: Path)

@Throws(CancellationException::class, IOException::class)
public suspend fun deleteIfExists(path: Path): Boolean =
try {
delete(path)
true
} catch (e: NoSuchFileException) {
false
}

@Throws(CancellationException::class, IOException::class)
public suspend fun copy(source: Path, target: Path, vararg options: CopyFileOption)

@Throws(CancellationException::class, IOException::class)
public suspend fun move(source: Path, target: Path, vararg options: CopyFileOption)

@Throws(CancellationException::class, IOException::class)
public suspend fun isSameFile(path1: Path, path2: Path): Boolean

@Throws(CancellationException::class, IOException::class)
public suspend fun checkAccess(path: Path, vararg modes: AccessMode)

@Throws(CancellationException::class, IOException::class)
public suspend fun openFileStore(path: Path): FileStore

public companion object {
@PublishedApi
internal val OPEN_SINK_OPTIONS_DEFAULT: Array<FileContentOption> =
arrayOf(
BasicFileContentOption.WRITE,
BasicFileContentOption.TRUNCATE_EXISTING,
BasicFileContentOption.CREATE
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package me.zhanghai.kotlin.filesystem

import kotlinx.io.IOException

public open class FileSystemException(
public val file: Path?,
public val otherFile: Path? = null,
message: String? = null,
cause: Throwable? = null
) : IOException(message, cause)
Loading

0 comments on commit 39b1f4a

Please sign in to comment.