file_system_access 0.1.0
file_system_access: ^0.1.0 copied to clipboard
Real files on Flutter Web. Save in place instead of downloading a copy, and reopen the same file after a reload with the File System Access API.
file_system_access #
Real files on Flutter Web.
final file = await FileSystemAccess.saveFile(suggestedName: 'notes.txt');
await file?.writeText('saved into the file the user picked');
// Later, after a full page reload:
final again = await FileSystemAccess.recallFile('last-document');
print(await again?.readText()); // the same file, still there
The problem #
On Flutter Web, "Save" means "download another copy". The user picks
notes.txt, you write it, and their Downloads folder gains notes (1).txt,
then notes (2).txt. There is no way to write back into the file they opened,
and no way to find it again after a reload.
The File System Access API fixes both, and Flutter has no binding for it.
package:web ships the handle types but not the pickers — they appear only in
its doc comments — and neither permissions nor directory iteration.
Saving in place #
final files = await FileSystemAccess.openFiles(
types: [FilePickerType.mime('text/plain', ['.txt', '.md'])],
);
final file = files.first;
final body = await file.readText();
await file.writeText('$body\n\nedited'); // same file, no second copy
Cancelling a picker is not an error — openFiles returns an empty list and
saveFile returns null, because the user changing their mind is not an
exceptional condition.
Coming back to the same file #
Handles are structured cloneable, so they can be kept in IndexedDB and taken out on the next visit. This is the part that is genuinely hard without the API:
await FileSystemAccess.remember('last-document', file);
// next visit
final file = await FileSystemAccess.recallFile('last-document');
if (await file?.permission(write: true) != FilePermission.granted) {
await file?.requestPermission(write: true); // needs a user gesture
}
The browser may still confirm write access on a new visit. That is its
decision, not this package's, and permission tells you which state you are in
before you try.
Ask what the browser has #
final s = FileSystemAccess.support;
s.openPicker; // Chrome, Edge
s.savePicker;
s.directoryPicker;
s.originPrivate; // far more widely available
Four questions rather than one boolean, because the answers really do differ: Safari has the origin-private file system and no pickers at all. Code that checks a single flag will show a Save button that cannot work.
Directories #
final dir = await FileSystemAccess.openDirectory(write: true);
for (final entry in await dir!.list()) {
print('${entry.name} ${entry.isDirectory ? "dir" : "file"}');
}
await dir.file('report.txt', create: true);
await dir.remove('old.txt');
Private storage, no prompt #
final root = await FileSystemAccess.originPrivateDirectory();
final scratch = await root.file('cache.bin', create: true);
The origin-private file system needs no picker and no permission, and is supported far more widely than the pickers. It is invisible to the user, so use it for scratch data rather than for anything they should be able to find on disk.
Off the web #
The package compiles everywhere. On mobile and desktop isSupported is false
and every call throws UnsupportedByBrowserException with a message pointing
at the check you should have made — so an app that also targets web can depend
on it without conditional imports of its own.
Verified #
The web path is tested in a real browser, not mocked: flutter test --platform chrome covers byte-for-byte round trips, non-ASCII text, truncation on
overwrite, directory listing, error paths, and storing a handle in IndexedDB
and reading the file back through it. The stub path is tested on the VM.
License #
MIT © K M Shahriar Hossain