persistent_file_access 0.1.1
persistent_file_access: ^0.1.1 copied to clipboard
Persist and restore access to user-selected files and directories across app restarts.
Persistent File Access #
persistent_file_access keeps a durable collection of files and directories
selected by the user and restores access to them when the application starts
again.
Application code uses the same API on macOS and Windows. Open a named
collection, add picker results, inspect its recovery state, and perform file I/O
inside withAccess. The package selects and manages the appropriate access
mechanism for the current platform.
It provides:
- persistent collections containing both files and directories;
- automatic recovery when a collection is reopened;
- available and temporarily unavailable states for each saved item;
- duplicate prevention and persistent insertion order;
- whole-collection reloads and targeted retries;
- removal even when an item is currently unavailable; and
- immutable snapshots plus a stream for reactive UI.
Choosing between similar packages #
persistent_file_access and
directory_bookmarks both
restore access to user-selected filesystem locations, but they are designed
for different application shapes. This comparison reflects the published
directory_bookmarks 0.1.2 API.
| Need | persistent_file_access |
directory_bookmarks |
|---|---|---|
| Supported platforms | macOS and Windows | macOS; Android is documented as in development, while Windows is planned |
| Saved selections | Named, ordered collections containing both files and directories | A directory-centered bookmark workflow |
| Filesystem operations | Any dart:io work inside a scoped withAccess callback |
Package-provided helpers for reading, writing, listing, and creating items below the bookmarked directory |
| Recovery model | Per-entry available/unavailable state, automatic recovery, retry, reload, and removal | Resolve the current bookmark and use permission or operation results |
| Reactive UI | Immutable collection snapshots and a snapshot stream | No collection snapshot model in the documented API |
Choose persistent_file_access when the application owns a collection of
individually selected files or directories, targets macOS and Windows, or needs
to keep unavailable entries visible and retryable. Choose directory_bookmarks
when a directory-centric API and its built-in relative-path file helpers are a
closer match, especially if its developing Android support is relevant.
Getting started #
Add the package to a Flutter desktop application:
flutter pub add persistent_file_access
1. Open a collection #
Use a stable key for each collection in the application. The package restores
its saved entries as part of open.
import 'dart:io';
import 'package:flutter/widgets.dart';
import 'package:persistent_file_access/persistent_file_access.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
final recentItems = await PersistentFileAccessSet.open(
'recent-filesystem-items',
);
// Pass recentItems to the part of the application that owns it.
}
No platform check or backend selection is needed for normal macOS and Windows usage.
2. Add files and directories selected by the user #
This package accepts File and Directory objects. Use any desktop file picker
to obtain the user's selection, then add it to the collection:
final selectedFile = File(filePathFromPicker);
final selectedDirectory = Directory(directoryPathFromPicker);
await recentItems.add(selectedFile);
await recentItems.add(selectedDirectory);
add returns true when the collection or the item's availability changed,
and false when the same available item was already present. Use
addDetailed when the UI needs to distinguish an insertion, replacement,
recovery, or unchanged item:
final result = await recentItems.addDetailed(selectedFile);
print(result.disposition);
print(result.entry.status);
3. Display restored entries #
The current snapshot contains every retained item. Temporarily unavailable
items remain in the collection so they can recover later, for example when a
removable or network volume reconnects.
for (final entry in recentItems.snapshot.entries) {
print('${entry.lastKnownPath}: ${entry.status}');
}
final ready = recentItems.snapshot.availableEntries;
final unavailable = recentItems.snapshot.unavailableEntries;
Each entry has an opaque id, its file or directory kind, a
lastKnownPath suitable for display, and a status. Use the ID as a UI key or
to target collection operations; do not parse or persist it separately.
4. Read or write through withAccess #
Perform all filesystem work inside a withAccess callback:
final entry = recentItems.snapshot.availableEntries.firstWhere(
(entry) => entry.kind == PersistentAccessEntityKind.file,
);
final contents = await recentItems.withAccess(entry, (entity) async {
final file = entity as File;
return file.readAsString();
});
The callback boundary is part of the API contract. Finish asynchronous reads
and writes, consume lazy streams, and close open handles before the callback
returns. Do not retain or return the supplied FileSystemEntity for later use.
withAccessById is useful when application state stores the entry ID:
await recentItems.withAccessById(entry.id, (entity) async {
// Complete filesystem work here.
});
Access is checked again when the callback starts, so an operation may still throw if the item or filesystem changed after the latest snapshot.
5. Retry or remove unavailable entries #
Temporary recovery failures do not delete an item. Retry all unavailable
entries with reload, or retry just one entry:
await recentItems.reload();
final unavailableEntry = recentItems.snapshot.unavailableEntries.first;
final refreshed = await recentItems.retryEntry(unavailableEntry);
By default, reload retries unavailable entries without probing entries that
are already available. To refresh every entry's availability, use:
await recentItems.reload(revalidateAvailable: true);
Revalidation can wait for slow or disconnected volumes. Use it when the UI needs a fresh availability check rather than on every rebuild.
Remove an available item by entity, or remove any retained entry by its public identity:
await recentItems.remove(selectedFile);
await recentItems.removeEntry(unavailableEntry);
// Equivalent when only the ID is available:
await recentItems.removeById(unavailableEntry.id);
Prefer removeEntry or removeById for UI actions because they still work when
the file or directory cannot currently be reached.
6. Observe changes and close the collection #
Read snapshot for the initial state, then listen to snapshots for accepted
adds, removals, reloads, and retries:
final initial = recentItems.snapshot;
final subscription = recentItems.snapshots.listen((snapshot) {
// Rebuild application state from snapshot.
});
// When the owner is disposed:
await subscription.cancel();
await recentItems.close();
close waits for already admitted withAccess callbacks to finish, closes the
snapshot stream, and rejects later operations. Do not call close from inside a
withAccess callback because it would wait for that same callback.
Recovery behavior #
Opening a collection attempts to recover every saved item. A successful item is
available; an item that may recover later is temporarilyUnavailable and
remains saved. A stored entry is discarded only when its data is proven to be
permanently invalid.
The collection reports discarded entries in snapshot.discardedEntries.
snapshot.discardedEntriesRemoved tells whether their removal was successfully
saved. Pass onIssue to open when diagnostics are needed for failures that the
collection handled safely:
final recentItems = await PersistentFileAccessSet.open(
'recent-filesystem-items',
onIssue: (issue) {
debugPrint('Entry ${issue.id} failed during ${issue.phase}: ${issue.error}');
},
);
Paths and filesystem errors may contain sensitive user information. Avoid
sending lastKnownPath, entry.error, or raw issue details to telemetry unless
the user expects it.
The same application flow applies on both supported platforms. Operating-system filesystem behavior can still affect recovery: a saved item may become unavailable after it is deleted, disconnected, or moved somewhere the operating system cannot resolve. In every case, use the snapshot status and retry/remove APIs above rather than adding platform-specific application logic.
macOS application setup #
Windows requires no package-specific runner configuration. A sandboxed macOS application must enable user-selected read/write access and app-scoped bookmarks in its entitlement files:
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
<key>com.apple.security.files.bookmarks.app-scope</key>
<true/>
Only add a file or directory after the user has authorized it through an operating-system picker, drag and drop, or another user-approved interaction. The application owns these entitlements; a Dart package cannot add them to the macOS runner.
Before releasing a sandboxed macOS application, verify the complete user flow:
- Select a file and a directory outside the application container.
- Quit and relaunch the application.
- Read both restored entries through
withAccess. - Make an entry unavailable and confirm the UI offers retry or removal.
- Remove an entry and confirm it stays removed after another restart.
Advanced configuration #
The default PersistentFileAccessSet.open(key) configuration is recommended
for new macOS and Windows collections.
Import the storage library to inject a PersistentSetStorage, usually for tests
or an application-specific transactional store:
import 'package:persistent_file_access/persistent_file_access_storage.dart';
final items = await PersistentFileAccessSet.open(
'recent-filesystem-items',
storage: customStorage,
);
Import the backend library only when configuring or implementing a backend:
import 'package:persistent_file_access/persistent_file_access_backend.dart';
MacOSPersistentAccessBackend.forEntityKind and legacyHandleKind exist for
applications migrating older, untagged macOS bookmark stores. New collections
do not need them. WindowsPathPersistentAccessBackend(caseSensitivePaths: true)
is available for applications that deliberately store selections on a
case-sensitive Windows filesystem.
Implement PersistentAccessBackend only when supporting another platform or a
custom access mechanism. A backend owns handle creation, recovery, identity,
and the bounded lifecycle used by withAccess; applications using the built-in
macOS and Windows support should not interact with handles or backends directly.
Example #
The example directory contains a desktop application that selects files and
directories, displays their recovery state, reads them through withAccess,
retries unavailable entries, and removes saved entries.