FileKit

repository·main·Indexed 23 days ago

https://github.com/vinceglb/filekit

A cross-platform file operations library for Kotlin Multiplatform and Compose Multiplatform. FileKit provides a unified API for picking files, saving documents, and managing directories using platform-native pickers. It includes modules for core operations, dialogs, Compose integration, and Coil image loading, as well as utilities for image compression, persistent storage via databasesDir, and secure file access using BookmarkData on sandboxed platforms.

Tokens
34.2K
Snippets
117
Records
146
Agent score
81%

What's inside FileKit

  1. Introduction to FileKit

    main

    FileKit is a lightweight library designed to simplify file operations in Kotlin Multiplatform (KMP) and Compose Multiplatform (CMP) projects. It provides a consistent API for picking files, selecting directories, and saving documents across Android, iOS, macOS, JVM (Windows, macOS, Linux), JS, and WASM. It uses platform-native pickers to ensure a native user experience.

    // Pick an image file
    val imageFile = FileKit.openFilePicker(type = FileKitType.Image)
    
    // Pick multiple files
    val files = FileKit.openFilePicker(mode = FileKitMode.Multiple())
    
    // Pick a directory
    val directory = FileKit.openDirectoryPicker()
    
    // Save a file
    PlatformFile(directory, "image.png").write(imageFile)
  2. What is PlatformFile?

    main

    PlatformFile is the core class in FileKit that provides a unified, cross-platform representation of files. It abstracts away platform-specific file implementations, allowing you to use a consistent API across Android, iOS, macOS, JVM, Kotlin/Native Linux, JS, and WASM targets.

    Key benefits include:

    • Unified API: Perform file operations without writing platform-specific code.
    • Cross-platform support: Works seamlessly across mobile, desktop, and web.
    • Integration: Built-in support for kotlinx-io and platform-specific types like Android Uri or iOS NSURL.
  3. Using PlatformFile for macOS sandboxed access

    main

    The PlatformFile abstraction is used to manage file access capabilities. When a file is restored from a security-scoped bookmark, the resulting PlatformFile retains its security scope.

    Key behaviors:

    • Inheritance: Descendants of a directory accessed via a bookmarked directory inherit the security scope.
    • Resource Management: FileKit manages scoped access based on actual resource usage to balance performance and security.
    • API Stability: Because PlatformFile carries these capabilities, you do not need to switch to a separate 'access-session' type when working with sandboxed files.
  4. Understand the PlatformFile abstraction

    main

    PlatformFile is the core Kotlin Multiplatform abstraction for file operations. It provides a consistent API across all platforms and is interoperable with kotlinx-io.

    You can obtain a PlatformFile via dialogs, or by using the division operator on standard directory paths.

    Common Properties and Methods:

    • name: The file name.
    • extension: The file extension.
    • path: The file path.
    • size(): Returns the size in bytes.
    • absolutePath(): Returns the absolute path.
    • exists(): Checks if the file exists.
    • isRegularFile(): Checks if it is a file.
    • isDirectory(): Checks if it is a directory.
    • parent(): Returns the parent PlatformFile.

    File Operations:

    • createDirectories(): Creates necessary directories.
    • copyTo(destinationFile): Copies the file.
    • atomicMove(destinationFile): Moves the file atomically.
    • delete(): Deletes the file.
    // Pick a file
    val file = FileKit.openFilePicker()
    
    // Get a file reference
    val file = FileKit.filesDir / "document.pdf"
    
    // Get the file properties
    val name: String = file.name
    val extension: String = file.extension
    val path: String = file.path
    val size: Long = file.size()
    val absolutePath: String = file.absolutePath()
    val parent: PlatformFile? = file.parent()
    val exists: Boolean = file.exists()
    val isFile: Boolean = file.isRegularFile()
    val isDirectory: Boolean = file.isDirectory()
    
    // File operations
    file.createDirectories()
    file.copyTo(destinationFile)
    file.atomicMove(destinationFile)
    file.delete()
  5. Prevent macOS JVM Runnable-Class Collisions

    main

    This record describes the technical resolution for Issue #626, which addresses crashes on macOS/JVM when multiple dependencies attempt to register an Objective-C class named IdeaRunnable. Because Objective-C class names occupy a process-wide namespace, collisions with other libraries (like IntelliJ/JBR) cause objc_allocateClassPair to return Nil or a zero-valued ID.

    To resolve this, FileKit implements the following safety measures:

    1. Namespace Isolation: Uses a unique, prefixed name FileKitMainThreadRunnable instead of the generic IdeaRunnable.
    2. Defensive Allocation: Explicitly checks for both Kotlin null and zero-valued ID results using Foundation.isNil. If allocation fails, it throws an IllegalStateException immediately.
    3. Safe Lifecycle Management: Follows the strict Apple-recommended sequence: allocate $\rightarrow$ add method (run:) $\rightarrow$ register. If class_addMethod fails, the unregistered class is cleaned up using objc_disposeClassPair to prevent memory leaks or incomplete class states.
    4. Explicit Ownership: Instead of performing a global getObjcClass(name) lookup (which could return a foreign class), FileKit stores the specific class ID and the JNA Callback reference in a private support value during a single, locked initialization phase.
  6. Understand FileKit's Bookmark and Access Concepts

    main

    FileKit uses a platform-specific abstraction called Bookmark Data to provide cross-platform references to user-selected files while respecting platform security models (like macOS sandboxing).

    Key concepts to understand for managing file references:

    • Bookmark Data: An opaque, platform-specific persistent reference. It is not portable between platforms and should not be treated as a simple saved path or serialized file. It may preserve access grants.
    • Security-Scoped Bookmark: Specifically for macOS, this preserves sandbox access to a resource across application launches.
    • Bookmark Resolution: The process of interpreting stored bookmark data to recover a PlatformFile. Note that successful resolution identifies the resource but does not guarantee that subsequent file operations will succeed.
    • Access Capability: The authority granted by the platform to a PlatformFile to access a resource. For directories, this capability extends to files within that directory.
    • Scoped Access: The bounded period during which an application must actively use/activate the access represented by a security-scoped resource.
    • Stale Bookmark: A bookmark that is still resolvable but should be replaced with newly created bookmark data according to the platform. This is different from an invalid bookmark.
    • Legacy Bookmark Data: Data created by older FileKit behaviors that may not preserve current access guarantees. FileKit attempts to resolve these permissively.
    • Bookmark Refresh: An advisory action to replace successfully resolved bookmark data when the platform reports it as stale or FileKit identifies it as legacy. This preserves or upgrades the reference but cannot recover access already revoked by the OS.
  7. Maintain persistent file access with BookmarkData

    main

    On platforms with sandboxing (Android, iOS, macOS), standard file paths may become invalid after an app restart. BookmarkData provides a way to create a persistent, secure reference to a file that can be saved (e.g., to preferences or a database) and used later to reliably regain access.

    Basic Workflow:

    1. Create: Call .bookmarkData() on a PlatformFile to get the bytes representing the bookmark.
    2. Save: Store these bytes in your application's persistent storage.
    3. Load: Retrieve the bytes from storage.
    4. Restore: Use PlatformFile.fromBookmarkData(bytes) to recreate the PlatformFile object.
    // 1. User picks a file
    val userPickedFile: PlatformFile = // ...from a file picker
    
    // 2. Create and save its bookmark data
    val bookmark = userPickedFile.bookmarkData()
    MyPreferences.save("last_file_bookmark", bookmark.bytes)
    
    // --- App restarts ---
    
    // 3. Load the saved bookmark data
    val savedBytes = MyPreferences.load("last_file_bookmark")
    
    // 4. Restore the PlatformFile from the bookmark
    if (savedBytes != null) {
        val restoredFile = PlatformFile.fromBookmarkData(savedBytes)
        // Now you can work with the restoredFile
    }
  8. How macOS bookmark persistence works in FileKit

    main

    FileKit uses a versioned envelope to store native macOS bookmarks. This ensures that file access can be persisted across application launches, which is critical for sandboxed macOS applications.

    When creating bookmarks, FileKit automatically selects the appropriate type based on the environment:

    • App Sandbox enabled: Uses a security-scoped bookmark to maintain access permissions.
    • No App Sandbox: Uses a regular native bookmark.

    Legacy (unwrapped) bookmark data is still supported via platform-specific resolution. If a legacy bookmark is successfully resolved, FileKit recommends refreshing it into the new versioned envelope format.

  9. Use FileKit.databasesDir for persistent storage

    main

    The FileKit.databasesDir property provides a platform-specific directory suitable for storing persistent application data like databases or preference files.

    PlatformLocation
    AndroidApp's internal databases directory
    iOSNSDocumentDirectory
    macOS~/Library/Application Support/{bundle-id}
    JVMUser's app data directory
  10. Understand the FileKit modular structure

    main

    FileKit is organized into several modules so you can include only the dependencies your project requires:

    • FileKit Core: Contains basic file operations and the PlatformFile abstraction.
    • FileKit Dialogs: Provides file pickers and save dialogs without any UI framework dependencies.
    • FileKit Dialogs Compose: Adds Compose Multiplatform integration for file operations.
    • FileKit Coil: Provides integration with the Coil library for easy image loading from files.
  11. Determine MIME types

    main

    Use mimeType() to retrieve the best-known media type for a file. It returns null for directories or unknown types.

    Platform Behavior:

    • Apple platforms: Queries provider metadata (Files app/iCloud) and falls back to filename extension.
    • Android: Uses ContentResolver for content:// URIs, falling back to MimeTypeMap.
    • JVM Desktop & Web: Derived from file extension or platform MIME registry.
    • Kotlin/Native Linux: Matches against the system shared MIME database (e.g., /etc/mime.types).
  12. Understand FileKit's macOS JVM Main-Thread Dispatch terminology

    main

    When working with FileKit on macOS JVM, specific terminology is used to distinguish between work and state owned by a FileKit runtime versus state owned by other parts of the process. Use these terms to avoid confusion when debugging or extending the library:

    • Runnable Adapter: A FileKit-owned bridge that carries a JVM runnable ticket onto the macOS application thread. (Do not confuse with IdeaRunnable or global helpers).
    • Runnable Ticket: An opaque identifier that connects a scheduled main-thread callback to its corresponding JVM work. (Do not confuse with Runnable pointer or callback state).
    • Foreign Adapter: A main-thread bridge owned by code outside the current FileKit runtime owner, even if it seems compatible. (Do not confuse with Reusable adapter or shared helper).
    • Runtime Owner: The specific FileKit runtime instance that owns one runnable adapter and its pending runnable tickets as a single lifecycle. (Do not confuse with Global helper or shared adapter).