plugins-workspace icon indicating copy to clipboard operation
plugins-workspace copied to clipboard

[bug] iOS file picker filter, selection, and security-scoped resource issues

Open velocitysystems opened this issue 2 months ago • 5 comments

Describe the problem

On iOS, when using plugin-dialog to select a file, the API does not behave as expected when filtering or selecting files. Custom file extensions (e.g. .foo) appear in the picker but cannot be selected. Additionally, unless MIME types or known extensions (like "txt") are passed, the picker defaults to an image picker instead of a file picker — two distinct modes on iOS.

Further, proper handling of security-scoped resources is missing or incomplete, which is required for persistent file access on iOS.

Repro (a)

  1. Open the file picker via the Tauri plugin on an iOS device.
  2. Provide a custom file type filter (e.g. ["foo", "bar"]).
  3. Observe that matching files appear in the picker but cannot be selected.

Repro (b)

  1. Open the file picker via the Tauri plugin on an iOS device.
  2. Try calling the picker without MIME types or standard extensions (e.g. "txt").
  3. Note that an image picker appears instead of the expected file picker.

Describe the solution you'd like

Files with custom extensions in the filter should be selectable.

  • The picker should open the UIDocumentPickerViewController (file picker), not the UIImagePickerController (image picker), when no image MIME types are provided.
  • Returned file handles should properly include security-scoped resource access, allowing safe reading and writing of files outside the app sandbox.

Alternatives considered

N/A

Related

#3029 #1578 #1596 #3716 PR 2061

velocitysystems avatar Oct 07 '25 13:10 velocitysystems

@FabianLars In addition to the change proposed for Android, we'd like to propose this enhancement to the open() method:

1. Select picker mode

Current: iOS attempts to infer which picker mode to use (document, media) based on the MIME type.

Proposed: Rather than infer the picker "mode" allow the user to specify a preferred mode:

enum PickerMode {
  Auto = 'auto',          // Automatically choose based on MIME types (default)
  Document = 'document',  // Use document picker
  Media = 'media'         // Use media picker
}

interface OpenDialogOptions {
  ...
  /**
   * Preferred picker mode for mobile platforms
   * Determines which native picker to use (e.g., document vs. media picker).
   */
  pickerMode?: PickerMode
}

Cross-platform note: This change affects all platforms. This change should also be implemented for Android to use the document picker (ACTION_OPEN_DOCUMENT) or media picker (ACTION_PICK). For desktop platforms it is a 'no-op'. However a consistent API across platforms simplifies application code.

velocitysystems avatar Oct 09 '25 17:10 velocitysystems

I like the PickerMode option - though we should also improve the default picker mode infer logic to comply with "The picker should open the UIDocumentPickerViewController (file picker), not the UIImagePickerController (image picker), when no image MIME types are provided"

I think the security-scoped resource is a separate issue - there's a pending work here #2548 but we never got to finalize it (it also breaks the API which i didn't really want to do)

lucasfernog avatar Oct 13 '25 12:10 lucasfernog

Thanks @lucasfernog. Agree with your suggestion; we'll implement the default logic per your comment above.

We have reviewed #2548 and agree it would be better to make a change which improves support for security-scoped resources while not being a breaking API change. We'll give this some further thought and propose this in a separate PR.

velocitysystems avatar Oct 13 '25 12:10 velocitysystems

@FabianLars I am currently working on an MR for the dialog plugin to support [start/stop]AccessingSecurityScopedResource. The solution I currently have working (at least for a proof of concept) has a method that allows for accessing, processing, then releasing the requested resource(s) in a single method so the client doesn't have to worry about the lifecycle, and is completely additive (no breaking changes).

Does this seem like a reasonable starting point to support startAccessingSecurityScopedResource for iOS and Mac when using the dialog plugin?

As a note, this also adds a benefit that we no longer need to copy a selected file to the local app sandbox before operating on it (unless the user wants to do that 😄).

If it's better to move this to another bug or ticket, we can definitely do that. Thanks!

c. @velocitysystems

Current starting point (just the TS API; the rest of the internals are fleshed out and working):

interface OpenAndProcessResult {
  processedPaths: string[]
  failedPaths: Record<string, string>[]
}

async function openAndProcess<T extends OpenDialogOptions>(
  options: T = {} as T,
  process: (filePath: string) => Promise<void>
): Promise<OpenAndProcessResult> {
  if (typeof options === 'object') {
    Object.freeze(options)
  }

  const file = await open(options)
  if (file === null) {
    return { processedPaths: [], failedPaths: [] }
  }

  const files: string[] = Array.isArray(file) ? file : [file]
  if (!files.length) {
    return { processedPaths: [], failedPaths: [] }
  }

  const processedFiles: string[] = []
  const failedFiles: Record<string, string>[] = []

  // Use .map instead of a for loop to avoid scoping issues with the filePath property
  const promises = files.map(async filePath => {
    const currentFilePath = String(filePath)
    const accessedFile: ProcessFileOptions = await invoke('plugin:dialog|start_file_access', { 
      options: { path: currentFilePath } 
    })
    try {
      await process(currentFilePath)
      processedFiles.push(currentFilePath)
    } catch (e) {
      failedFiles.push({ path: currentFilePath, error: e instanceof Error ? e.message : String(e) })
    } finally {
      await invoke('plugin:dialog|end_file_access', { options: { resourceId: accessedFile.resourceId } })
    }
  })

  await Promise.all(promises)
  return { processedPaths: processedFiles, failedPaths: failedFiles }
}

onehumandev avatar Oct 16 '25 20:10 onehumandev

Thanks @onehumandev. Discussed this with @lucasfernog and here is the proposed solution:

2. Select file-access mode

Current: iOS copies the file to the application sandbox which avoids the use of security-scoped bookmarks.

Proposed: Allow the user to specify a preferred file access mode:

enum FileAccessMode {
  Copy = 'copy',            // Copy the file to the sandbox
  Scoped = 'scoped'         // Use security-scoped access
}

interface OpenDialogOptions {
  ...
  /**
   * Preferred file access mode for mobile platforms
   * Determines whether to copy to the application sandbox, or use security-scoped access
   */
  fileAccessMode?: FileAccessMode
}

Cross-platform note: This change affects all platforms. For desktop platforms it is a 'no-op'. However a consistent API across platforms simplifies application code.

3. Add methods for starting/stopping access to scoped resources

Current: N/A

Proposed: Allow the user to start/stop accessing security-scoped resources via TS and Rust APIs.

/**
 * Start access to a security-scoped resource.
 *
 * @param path - The absolute file path to access.
 */
function startScopedFileAccess(path: string): Promise<void> {
...
}

/**
 * Stop access to a security-scoped resource.
 *
 * @param path - The absolute file path to access.
 */
function stopScopedFileAccess(path: string): Promise<void> {
...
}
#[command]
fn start_scoped_file_access(path: String) -> Result<(), String> {
...
}

#[command]
fn stop_scoped_file_access(path: String) -> Result<(), String> {
...
}

impl Drop for ScopedFileAccess {
    fn drop(&mut self) {
        #[cfg(target_os = "ios")]
        {
            // TODO: Call stop_scoped_file_access() for the path
        }
    }
}

Cross-platform note: This API would initially be implemented for iOS only. For desktop platforms it is a 'no-op'; however it could be later implemented for macOS. However a consistent API across platforms simplifies application code.

velocitysystems avatar Oct 24 '25 09:10 velocitysystems