Skip to content

Allow to select files with a "persistent" link/path without creating a temporary copy #2037

Description

@ekuleshov

When working with large files, especially media files from user's gallery it is not desirable to make copies for large media files.

It would be really great to have an option for selecting files that would to return a "persistent" link/path that could use used at any time later to access selected file content.

On MacOS, Windows, Linux

Should be okay to return the full file path and have the original file:// url in the PlatformFile.identifier
Though

On iOS

If a user picks a file outside app's sandbox, the OS grants a temporary security token. To access it later without copying it, the native file URL need to be converted to into a Security-Scoped Bookmark:

  1. Create a Bookmark (Save Access). Convert selected file URL.
func createSecurityScopedBookmark(from fileURL: URL) -> String? {
    // MUST explicitly declare intent to access the file resource
    guard fileURL.startAccessingSecurityScopedResource() else {
        print("Failed to start accessing security-scoped resource.")
        return nil
    }
    
    // Ensure resource access is released when the function completes
    defer {
        fileURL.stopAccessingSecurityScopedResource()
    }
    
    do {
        // Generate security-scoped data bytes
        let bookmarkData = try fileURL.bookmarkData(
            options: .withSecurityScope, // Required for sandboxed apps
            includingResourceValuesForKeys: nil,
            relativeTo: nil
        )
        
        // convert to a Base64 string for storing it
        return bookmarkData.base64EncodedString()
        
    } catch {
        print("Error creating bookmark: \(error.localizedDescription)")
        return nil
    }
}
  1. Resolve a Bookmark (Regain Access Later). Call this on a future app launch using the base64-encoded bookmark string.
func resolveSecurityScopedBookmark(from base64String: String) -> URL? {
    guard let bookmarkData = Data(base64Encoded: base64String) else {
        print("Invalid base64 string.")
        return nil
    }
    
    do {
        var isStale = false
        
        // Resolve data back into a valid security-scoped URL
        let resolvedURL = try URL(
            resolvingBookmarkData: bookmarkData,
            options: .withSecurityScope,
            relativeTo: nil,
            bookmarkDataIsStale: &isStale
        )
        
        if isStale {
            print("Bookmark data is stale. You may need to ask the user to pick the file again.")
        }
        
        return resolvedURL
        
    } catch {
        print("Error resolving bookmark: \(error.localizedDescription)")
        return nil
    }
}

// How to read the file contents using the resolved URL
func printFileContents(at url: URL) {
    if url.startAccessingSecurityScopedResource() {
        defer { url.stopAccessingSecurityScopedResource() }
        
        // Do your file reading operations safely here
        if let data = try? Data(contentsOf: url) {
            print("Successfully read \(data.count) bytes directly from the original file source!")
        }
    } else {
        print("Permission denied to access the resolved URL.")
    }
}

On Android

You cannot use raw file paths (/storage/emulated/0/...). Instead, can use the native storage access framework to capture the content:// URI and request long-term persistence permissions. See shared_storage or more recently updated saf_util and saf_stream packages for Android.

The PlatformFile.identifier property already containing the content:// URI. Though there is no API to read data or create a PlatformFile for a previously saved URI.

Also it seem like there is no option to not create a temp file when there is content:// resource selected. Perhaps it could be added to AndroidSAFOptions.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions