Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,26 @@ jobs:
- name: 🧪 Run macOS Tests
run: ./gradlew macosArm64Test --no-daemon

test-linux:
name: 🐧 Test Linux
needs: build
runs-on: ubuntu-latest
steps:
- name: 🛎️ Check out repository
uses: actions/checkout@v6

- name: 🍉 Configure JDK 21
uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: '21'

- name: 🐘 Setup Gradle
uses: gradle/actions/setup-gradle@v6

- name: 🧪 Run Linux Native Tests
run: ./gradlew linuxX64Test

lint:
name: 🚨 Lint
runs-on: ubuntu-latest
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class KotlinMultiplatformLibraryConventionPlugin : Plugin<Project> {
addMacosTargets = true,
addWatchosTargets = path == ":filekit-core",
addMingwTargets = path == ":filekit-core" || path == ":filekit-dialogs",
addLinuxTargets = path == ":filekit-core",
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ internal fun Project.configureKotlinMultiplatform(
addMacosTargets: Boolean,
addWatchosTargets: Boolean,
addMingwTargets: Boolean = false,
addLinuxTargets: Boolean = false,
) = extension.apply {
// Force visibility of public API
explicitApi()
Expand Down Expand Up @@ -47,6 +48,12 @@ internal fun Project.configureKotlinMultiplatform(
mingwX64()
}

// Linux native targets
if (addLinuxTargets) {
linuxX64()
linuxArm64()
}

// If one day we need to disable watchOS tests
//
// if (addWatchosTargets) {
Expand Down
2 changes: 1 addition & 1 deletion docs/cookbook/test-resources.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,6 @@ val file = FileKit.projectDir / "src" / "commonTest" / "resources" / "data.json"
| iOS | ✅ |
| macOS | ✅ |
| JVM | ✅ |
| Kotlin/Native Linux (`linuxX64`, `linuxArm64`) | ✅ |
| JS | ❌ |
| WASM | ❌ |

2 changes: 1 addition & 1 deletion docs/core/bookmark-data.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ FileKit abstracts away the details, but here's what happens on each platform:

- **JVM macOS**: Uses the same versioned native bookmark format through CoreFoundation. A `PlatformFile` restored from a security-scoped bookmark retains its access capability, including for children inside a bookmarked directory.

- **JVM Linux and Windows**: Stores the file path. These platforms keep their existing bookmark representation.
- **Kotlin/Native Linux, JVM Linux, and Windows**: Stores the file path. These platforms use a path-based bookmark representation.

New macOS bookmark data is wrapped in a versioned FileKit format. Existing unwrapped Kotlin/Native bookmarks and JVM path bytes remain readable. Bookmark bytes are platform-specific and must not be treated as portable between operating systems or applications.

Expand Down
11 changes: 8 additions & 3 deletions docs/core/file-utils.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import DownloadFilesWeb from '/snippets/download-files-web.mdx'

## Standard Directories

<Check>Supported on Android, iOS, macOS, JVM targets</Check>
<Check>Supported on Android, iOS, macOS, JVM, and Kotlin/Native Linux targets</Check>

FileKit provides access to standard platform-specific directories:

Expand Down Expand Up @@ -54,13 +54,18 @@ Each platform maps these standard directories to different locations according t
- Windows: `%LOCALAPPDATA%/<app-id>/Cache/`
- **databasesDir**: Maps to a `databases` subdirectory within filesDir

**Kotlin/Native Linux (`linuxX64`, `linuxArm64`)**
- **filesDir**: Maps to `$XDG_DATA_HOME/<app-id>/`, or `~/.local/share/<app-id>/` when `XDG_DATA_HOME` is unset
- **cacheDir**: Maps to `$XDG_CACHE_HOME/<app-id>/`, or `~/.cache/<app-id>/` when `XDG_CACHE_HOME` is unset
- **databasesDir**: Maps to a `databases` subdirectory within filesDir

<Note>
On JVM and macOS platforms, you must initialize FileKit with an application ID before accessing these directories. See the [Setup guide](/core/setup) for details.
On JVM, macOS, and Kotlin/Native Linux platforms, you must initialize FileKit with an application ID before accessing these directories. See the [Setup guide](/core/setup) for details.
</Note>

### Additional Directories

On desktop platforms (JVM and macOS), FileKit provides access to common user directories:
On desktop platforms (JVM, macOS, and Kotlin/Native Linux), FileKit provides access to common user directories:

```kotlin
// Typed API
Expand Down
5 changes: 3 additions & 2 deletions docs/core/platform-file.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ title: 'PlatformFile'
description: 'Cross-platform file representation for Kotlin Multiplatform'
---

<Check>Supported on Android, iOS, macOS, JVM, JS and WASM targets</Check>
<Check>Supported on Android, iOS, macOS, JVM, Kotlin/Native Linux, JS and WASM targets</Check>

## Introduction

Expand Down Expand Up @@ -165,7 +165,8 @@ Use `mimeType()` to ask the underlying platform for the best-known media type. I

- **Apple platforms**: FileKit first queries provider metadata (e.g., from the Files app or iCloud Drive) and falls back to the filename extension when needed, so even extension-less documents can resolve to a MIME type.
- **Android**: The resolver consults the `ContentResolver` for `content://` URIs and then falls back to `MimeTypeMap` if necessary.
- **Desktop & Web**: The lookup is derived from the file extension.
- **JVM Desktop & Web**: The lookup is derived from the file extension or the desktop platform's MIME registry.
- **Kotlin/Native Linux**: FileKit matches the filename against the system shared MIME database, with `/etc/mime.types` as a fallback.

This utility is helpful when you need to validate file types, populate upload headers, or customise previews based on content.

Expand Down
6 changes: 3 additions & 3 deletions docs/core/read-file.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ FileKit Core provides a consistent API for reading files across all platforms. T

## Reading as ByteArray

<Check>Supported on Android, iOS, macOS, JVM, JS and WASM targets</Check>
<Check>Supported on Android, iOS, macOS, JVM, Kotlin/Native Linux, JS and WASM targets</Check>

The most common way to read a file is to use the `readBytes()` suspend function, which returns the file contents as a `ByteArray`:

Expand All @@ -22,7 +22,7 @@ val bytes: ByteArray = file.readBytes()

## Reading as String

<Check>Supported on Android, iOS, macOS, JVM, JS and WASM targets</Check>
<Check>Supported on Android, iOS, macOS, JVM, Kotlin/Native Linux, JS and WASM targets</Check>

For text files, you can use the `readString()` suspend function, which returns the file contents as a `String`:

Expand All @@ -36,7 +36,7 @@ println(text)

## Using Source

<Check>Supported on Android, iOS, macOS, JVM targets</Check>
<Check>Supported on Android, iOS, macOS, JVM and Kotlin/Native Linux targets</Check>

For more advanced use cases or when working with large files, you can use the `source()` method from [kotlinx-io](https://github.com/Kotlin/kotlinx-io) to get a raw source:

Expand Down
12 changes: 12 additions & 0 deletions docs/core/setup.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,18 @@ class MainActivity : ComponentActivity() {

<SetupJVMCore/>

### Linux Native setup

For `linuxX64` and `linuxArm64`, initialize FileKit with your application ID before accessing the standard application directories:

```kotlin
fun main() {
FileKit.init(appId = "my.application.id")
}
```

FileKit uses the XDG data and cache directories by default. You can instead pass explicit `PlatformFile` values for `filesDir` and `cacheDir`.

### iOS, macOS, JS, WASM setup

No additional setup is needed for iOS, macOS, JS, and WASM targets. FileKit Core works out of the box on these platforms.
2 changes: 1 addition & 1 deletion docs/core/write-file.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: 'Write files with FileKit Core in Kotlin Multiplatform'

import DownloadFilesWeb from '/snippets/download-files-web.mdx'

<Check>Supported on Android, iOS, macOS, JVM targets</Check>
<Check>Supported on Android, iOS, macOS, JVM and Kotlin/Native Linux targets</Check>

FileKit Core provides a consistent API for writing files across all platforms. The `PlatformFile` class offers multiple methods to write data to files, from simple strings to binary data and streaming operations.

Expand Down
3 changes: 3 additions & 0 deletions docs/installation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@ FileKit supports the following targets:
- Android
- iOS, macOS
- JVM (Windows, macOS, Linux)
- Kotlin/Native Linux (`linuxX64`, `linuxArm64`) in FileKit Core
- JS, WASM

FileKit Dialogs does not yet support Kotlin/Native Linux or Aurora OS. Those integrations are separate from the FileKit Core support described here.

## FileKit Core

FileKit Core provides the fundamental file operations with the `PlatformFile` abstraction. It allows you to work with files in a platform-agnostic way. You can create, read, write, and delete files using the `PlatformFile` API.
Expand Down
1 change: 1 addition & 0 deletions filekit-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ kotlin {
jvmMain.get().dependsOn(desktopMain)
macosMain.get().dependsOn(desktopMain)
mingwX64Main.get().dependsOn(desktopMain)
linuxMain.get().dependsOn(desktopMain)
jvmMain.get().dependsOn(jvmAndNativeMain)
nativeMain.get().dependsOn(jvmAndNativeMain)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package io.github.vinceglb.filekit

import io.github.vinceglb.filekit.utils.div
import io.github.vinceglb.filekit.utils.toPath
import kotlinx.io.files.Path

internal fun resolveLinuxUserDirectoryPath(
type: FileKitUserDirectory,
home: String?,
envProvider: (String) -> String?,
linuxUserDirsConfigProvider: () -> String?,
): Path? {
val safeHome = home
?.takeIf(String::isNotBlank)
?: return null
val envValue = envProvider(type.xdgEnvKey)
?.takeIf(String::isNotBlank)
?.let { expandHomeVariable(it, safeHome) }
if (envValue != null) {
return envValue.toPath()
}

val configuredValue = linuxUserDirsConfigProvider()
?.takeIf(String::isNotBlank)
?.let(::parseXdgUserDirsConfig)
?.get(type.xdgEnvKey)
?.takeIf(String::isNotBlank)
?.let { expandHomeVariable(it, safeHome) }
if (configuredValue != null) {
return configuredValue.toPath()
}

return safeHome.toPath() / type.linuxFallbackDirName
}

internal fun parseXdgUserDirsConfig(config: String): Map<String, String> =
buildMap {
config
.lineSequence()
.map(String::trim)
.filter(String::isNotBlank)
.filterNot { it.startsWith("#") }
.forEach { line ->
val key = line.substringBefore("=", missingDelimiterValue = "").trim()
val rawValue = line.substringAfter("=", missingDelimiterValue = "").trim()

if (key.isBlank() || rawValue.isBlank()) {
return@forEach
}

val value = rawValue
.removeSurrounding("\"")
.replace("\\\"", "\"")
.replace("\\\\", "\\")

put(key, value)
}
}

private fun expandHomeVariable(path: String, home: String): String =
path
.replace("\${HOME}", home)
.replace("\$HOME", home)

private val FileKitUserDirectory.xdgEnvKey: String
get() = when (this) {
FileKitUserDirectory.Downloads -> "XDG_DOWNLOAD_DIR"
FileKitUserDirectory.Pictures -> "XDG_PICTURES_DIR"
FileKitUserDirectory.Videos -> "XDG_VIDEOS_DIR"
FileKitUserDirectory.Music -> "XDG_MUSIC_DIR"
FileKitUserDirectory.Documents -> "XDG_DOCUMENTS_DIR"
}

private val FileKitUserDirectory.linuxFallbackDirName: String
get() = when (this) {
FileKitUserDirectory.Downloads -> "Downloads"
FileKitUserDirectory.Pictures -> "Pictures"
FileKitUserDirectory.Videos -> "Videos"
FileKitUserDirectory.Music -> "Music"
FileKitUserDirectory.Documents -> "Documents"
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ internal fun resolveJvmUserDirectoryPath(
): Path? = when (platform) {
Platform.Linux -> resolveLinuxUserDirectoryPath(
type = type,
home = envProvider("HOME"),
envProvider = envProvider,
linuxUserDirsConfigProvider = linuxUserDirsConfigProvider,
)
Expand Down Expand Up @@ -59,58 +60,6 @@ internal fun resolveKnownFolderPath(type: FileKitUserDirectory): String? =
)
}.getOrNull()

internal fun parseXdgUserDirsConfig(config: String): Map<String, String> =
buildMap {
config
.lineSequence()
.map(String::trim)
.filter(String::isNotBlank)
.filterNot { it.startsWith("#") }
.forEach { line ->
val key = line.substringBefore("=", missingDelimiterValue = "").trim()
val rawValue = line.substringAfter("=", missingDelimiterValue = "").trim()

if (key.isBlank() || rawValue.isBlank()) {
return@forEach
}

val value = rawValue
.removeSurrounding("\"")
.replace("\\\"", "\"")
.replace("\\\\", "\\")

put(key, value)
}
}

private fun resolveLinuxUserDirectoryPath(
type: FileKitUserDirectory,
envProvider: (String) -> String?,
linuxUserDirsConfigProvider: () -> String?,
): Path? {
val home = envProvider("HOME")
?.takeIf(String::isNotBlank)
?: return null
val envValue = envProvider(type.xdgEnvKey)
?.takeIf(String::isNotBlank)
?.let { expandHomeVariable(it, home) }
if (envValue != null) {
return envValue.toPath()
}

val configuredValue = linuxUserDirsConfigProvider()
?.takeIf(String::isNotBlank)
?.let(::parseXdgUserDirsConfig)
?.get(type.xdgEnvKey)
?.takeIf(String::isNotBlank)
?.let { expandHomeVariable(it, home) }
if (configuredValue != null) {
return configuredValue.toPath()
}

return (home.toPath() / type.linuxFallbackDirName)
}

private fun resolveMacUserDirectoryPath(type: FileKitUserDirectory, home: String?): Path? {
val safeHome = home?.takeIf(String::isNotBlank) ?: return null
return (safeHome.toPath() / type.macFallbackDirName)
Expand All @@ -133,29 +82,6 @@ private fun resolveWindowsUserDirectoryPath(
return (userProfile.toPath() / type.windowsFallbackDirName)
}

private fun expandHomeVariable(path: String, home: String): String =
path
.replace("\${HOME}", home)
.replace("\$HOME", home)

private val FileKitUserDirectory.xdgEnvKey: String
get() = when (this) {
FileKitUserDirectory.Downloads -> "XDG_DOWNLOAD_DIR"
FileKitUserDirectory.Pictures -> "XDG_PICTURES_DIR"
FileKitUserDirectory.Videos -> "XDG_VIDEOS_DIR"
FileKitUserDirectory.Music -> "XDG_MUSIC_DIR"
FileKitUserDirectory.Documents -> "XDG_DOCUMENTS_DIR"
}

private val FileKitUserDirectory.linuxFallbackDirName: String
get() = when (this) {
FileKitUserDirectory.Downloads -> "Downloads"
FileKitUserDirectory.Pictures -> "Pictures"
FileKitUserDirectory.Videos -> "Videos"
FileKitUserDirectory.Music -> "Music"
FileKitUserDirectory.Documents -> "Documents"
}

private val FileKitUserDirectory.macFallbackDirName: String
get() = when (this) {
FileKitUserDirectory.Downloads -> "Downloads"
Expand Down
Loading