Skip to content

Use a better file picker - #388

Open
nbrownus wants to merge 1 commit into
mainfrom
file-selector
Open

Use a better file picker#388
nbrownus wants to merge 1 commit into
mainfrom
file-selector

Conversation

@nbrownus

Copy link
Copy Markdown
Contributor

Trade file_picker for file_selector and drop temporary files

Comment thread lib/services/utils.dart
@@ -127,14 +129,52 @@ class Utils {
}

static Future<String?> pickFile(BuildContext context) async {

@johnmaguire johnmaguire Jul 24, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: context is unused now

Comment thread lib/services/utils.dart

// We get a copy, not the original, so don't leave a plaintext key sitting around
try {
await File(file.path).delete();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Android drops each copy in its own <cacheDir>/<uuid>/, and the plugin's targetDirectory.deleteOnExit() (FileUtils.java:130) never fires there - the javadoc attempts deletion only on normal VM termination, which an Android process kill is not. Worth adding a non-recursive .parent.delete() here; it throws harmlessly into the existing catch on iOS, where the parent is shared.

Suggested change
await File(file.path).delete();
await File(file.path).delete();
// Android drops each copy in its own <cacheDir>/<uuid>/; fails harmlessly
// on iOS where the parent is shared
await File(file.path).parent.delete();

Comment thread lib/services/utils.dart
try {
await File(file.path).delete();
} catch (err) {
// Ignoring file delete errors

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possible plaintext key material stayed on disk, maybe worth a log at least

Comment thread lib/services/utils.dart
try {
final cacheDir = await getTemporaryDirectory();
final legacyCache = Directory(p.join(cacheDir.path, 'file_picker'));
if (legacyCache.existsSync()) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

existsSync / deleteSync / listSync block the isolate during startup for no benefit, since this is already fire-and-forget. The dart:io docs say it directly at file_system_entity.dart:230: "prefer the asynchronous version to avoid blocking your program."

Suggested change
if (legacyCache.existsSync()) {
if (await legacyCache.exists()) {
await legacyCache.delete(recursive: true);
}

Comment thread lib/services/utils.dart
// Ignoring cleanup errors
}

// iOS document imports land in <tmp>/<bundle id>-Inbox. systemTemp because

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could not confirm anywhere that .import stages into <tmp>/<bundle id>-Inbox. Apple's picker docs do not say, and the archived File System guide's Documents/Inbox entry describes a different API (UIDocumentInteractionController and Mail attachments, not the picker). Worth a one-line debugPrint of the path openFile() returns on a device before merging, because if it is not under systemTemp then the whole iOS branch below matches nothing and is dead code.

Comment thread lib/services/utils.dart

// iOS document imports land in <tmp>/<bundle id>-Inbox. systemTemp because
// path_provider points getTemporaryDirectory at Library/Caches, not tmp, and it
// is only our sandbox on iOS. Inboxes only, Share is using the rest of tmp.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this premise is inverted. Share writes to getTemporaryDirectory() (lib/services/share.dart:24), which path_provider_foundation maps to NSCachesDirectory on iOS - path_provider_foundation_real.dart:41, pinned at 2.6.0 in the lockfile. Share never touches tmp, so there is nothing of ours here to protect from a wider sweep.

Comment thread lib/services/utils.dart

try {
for (final entry in Directory.systemTemp.listSync()) {
if (entry is Directory && entry.path.endsWith('-Inbox')) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking, I think. file_picker 12 does not hand back the -Inbox path - it copies to a loose file at the top of tmp. IOSFilePickerHandler.swift:208 calls copyToTemporaryDirectory, defined at :334-345, which writes to NSTemporaryDirectory()/<filename>. So this filter walks straight past the plaintext key it exists to delete.

That file is exactly what the old FilePicker.clearTemporaryFiles() used to remove, since it wiped every entry in tmp (:308-320) - which means after this upgrade the last-imported key stays on disk permanently, the failure mode the method is named for.

Suggested change
if (entry is Directory && entry.path.endsWith('-Inbox')) {
if (entry is File || (entry is Directory && entry.path.endsWith('-Inbox'))) {
entry.deleteSync(recursive: true);
}

Comment thread pubspec.yaml
file_picker: ^12.0.0-beta.7
# We only pick documents (certs, keys, configs). file_selector is first party and
# has no media picker code, so photo library permissions can never sneak into our binary
file_selector: ^1.1.0

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI: the allowed-type root narrows from public.item (file_picker IOSFilePickerHandler.swift:67) to public.data (file_selector_ios.dart:54, pinned 0.5.3+5). Should be invisible for certs, keys and YAML, but it is a real behavior change. Claude suggests testing the Files-app-via-iCloud at least.

Comment thread lib/main.dart

@override
void initState() {
// Waits until here so the binding is up, main runs before anyone has made one

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't quite follow the comment, fwiw

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants