dbas_filesystem 3.1.4
dbas_filesystem: ^3.1.4 copied to clipboard
Flutter plugin for cross-platform file system operations with streaming, byte array, and directory support across Android, iOS, macOS, Linux, Windows, and Web (OPFS).
dbas_filesystem #
A Flutter plugin for cross-platform file system operations with streaming, byte array, and directory support across Android, iOS, macOS, Linux, Windows, and Web.
Features #
- File operations — read, write, append, copy, move, rename, delete, and existence check.
- File metadata — get file size and last modified timestamp.
- Overwrite protection —
writeFile,writeFileStream,copyFile,moveFile,renameFile, andcopyDirectorydefault tooverwrite: false, preventing accidental data loss. - Append support —
appendFileandappendFileStreamappend bytes to existing files (or create them if missing). - Stream support — stream-based read and write for memory-efficient large file handling.
- Atomic bulk operations —
writeFilesandwriteFilesStreamdefault to atomic mode: snapshot existing files, write all, rollback on failure. Setatomic: falsefor independent writes with per-fileonError. - Directory operations — create, list (with typed entries), copy, move, delete, rename, and existence check.
- Cross-device move — automatic copy+delete fallback when source and destination are on different devices.
- File change notifications — optional
onFileChangedcallback fires after every mutation with a map of affected entries and their before/after state. - Progress callbacks — optional
onProgresson all operations, with per-entry and overall progress reporting. - Hierarchical thread safety — mutating file operations acquire a shared lock on the parent directory + exclusive lock on the file. Read-only queries (
fileExists,getFileSize,getLastModified) use shared locks for minimal contention. Directory-destructive operations acquire exclusive locks that block concurrent child file ops. Different paths proceed in parallel. - Typed exceptions —
FileNotFoundException,FileAlreadyExistsException,DirectoryNotFoundException,DirectoryNotEmptyException,PermissionDeniedException,MultiException,AtomicOperationException. - Web worker pool — configurable pool of OPFS Web Workers for true parallel I/O on web, with automatic restart on crash (exponential backoff).
- Configurable chunking — streamed reads use a configurable chunk size (default 64 KB).
- Path normalization —
listDirectoryandgetAppFilePathreturn forward-slash paths on all platforms. - Side-effect free path resolution —
getAppFilePathresolves a platform path without creating directories or accessing the file system. - Lifecycle management —
dispose({timeout})gives in-flight operations a configurable grace period, then forces teardown.isDisposedreflects whether an instance has been disposed. - Cancellation —
CancellationTokenwithaddListener/removeListenerfor reactive cancellation in long-running operations.
Platform Support #
| Platform | Implementation |
|---|---|
| Android | dart:io |
| iOS | dart:io |
| macOS | dart:io |
| Linux | dart:io |
| Windows | dart:io |
| Web | OPFS via Web Worker pool |
Web storage uses the Origin Private File System (OPFS)
exclusively. There is no fallback to IndexedDB, LocalStorage, or Cache API.
OPFS is the modern browser standard for persistent, origin-scoped binary file
storage with byte-level random access and true background-thread I/O.
Applications targeting older browsers should catch the DbasFileSystemException
thrown by getInstance() and display an appropriate upgrade notice.
Installation #
Add to your pubspec.yaml:
dependencies:
dbas_filesystem: ^3.1.4
Or install with the Dart CLI:
flutter pub add dbas_filesystem
Usage #
import 'dart:typed_data';
import 'package:dbas_filesystem/dbas_filesystem.dart';
final fs = await DbasFileSystem.getInstance();
Write and read a file #
final path = await fs.getAppFilePath('example.txt');
// Write (throws FileAlreadyExistsException if file exists)
await fs.writeFile(path, Uint8List.fromList(utf8.encode('Hello, world!')));
// Write with explicit overwrite
await fs.writeFile(path, Uint8List.fromList(utf8.encode('updated')), overwrite: true);
// Read (returns Uint8List)
final bytes = await fs.readFile(path);
print(utf8.decode(bytes));
Note:
getAppFilePathonly resolves a path — it does not create directories. Parent directories are created automatically when you callwriteFile,writeFileStream, orappendFile.
Append to a file #
// Append bytes (creates file if it doesn't exist)
await fs.appendFile(path, Uint8List.fromList(utf8.encode('more data')));
// Append from a stream
await fs.appendFileStream(path, myStream);
File metadata #
final size = await fs.getFileSize(path); // bytes
final modified = await fs.getLastModified(path); // DateTime (UTC)
Stream a large file #
// Write from stream
Stream<List<int>> chunks() async* {
for (int i = 0; i < 100; i++) {
yield Uint8List(65536); // 64 KB chunks
}
}
await fs.writeFileStream(path, chunks());
// Read as stream (returns Stream<Uint8List>)
await for (final chunk in fs.readFileStream(path)) {
// process chunk
}
Bulk operations #
// Atomic write (default) — all-or-nothing with rollback
await fs.writeFiles(fileMap, maxConcurrency: 10);
// Non-atomic write — skip failures, continue
final errors = <String, Object>{};
await fs.writeFiles(fileMap, atomic: false, onError: (path, e) => errors[path] = e);
// Read multiple files concurrently
final results = await fs.readFiles(paths, maxConcurrency: 10);
// Read with partial results on failure
final results = await fs.readFiles(
paths,
onError: (path, error) => errors[path] = error,
);
// results contains only successful reads; errors contains the failures
Memory note:
readFilesandwriteFileshold all file contents in memory simultaneously, bounded bymaxConcurrency. For large files, preferreadFileStream/writeFileStreamindividually to control memory usage.
Rename, copy, and move #
await fs.renameFile(oldPath, newPath); // atomic on native
await fs.renameDirectory(oldDirPath, newDirPath); // atomic on native
await fs.copyFile(sourcePath, destPath);
await fs.moveFile(sourcePath, destPath); // cross-device safe
await fs.moveDirectory(sourceDirPath, destDirPath); // cross-device safe
// Overwrite explicitly (default is overwrite: false)
await fs.copyFile(sourcePath, destPath, overwrite: true);
await fs.moveFile(sourcePath, destPath, overwrite: true);
await fs.renameFile(oldPath, newPath, overwrite: true);
Directory operations #
await fs.createDirectory(dirPath);
// List returns typed entries with path and type (file or directory)
final entries = await fs.listDirectory(dirPath);
for (final entry in entries) {
print('${entry.path} is a ${entry.type}'); // FileSystemEntityType.file or .directory
}
final allEntries = await fs.listDirectory(dirPath, recursive: true); // entire tree
// Copy a directory (merge — overwrites conflicting files with overwrite: true)
await fs.copyDirectory(sourceDirPath, destDirPath, overwrite: true);
// Move a directory (atomic rename on native, copy+delete fallback across devices)
await fs.moveDirectory(sourceDirPath, destDirPath);
await fs.deleteDirectory(dirPath, recursive: true);
File change notifications #
final fs = await DbasFileSystem.getInstance(
onFileChanged: (changes) {
for (final entry in changes.entries) {
final change = entry.value;
print('${change.type}: ${change.path}');
// FileChangeType.created, .modified, or .deleted
}
},
);
// Or set/update the callback later:
fs.onFileChanged = (changes) { /* ... */ };
// Notifications include before/after state:
// change.oldEntry — null if created, set if modified/deleted
// change.newEntry — null if deleted, set if created/modified
Progress callbacks #
await fs.copyFile(source, dest, overwrite: true, onProgress: (progress) {
print('File: ${progress.current.entry.path}');
print('Current: ${(progress.current.progress * 100).toInt()}%');
print('Overall: ${(progress.overall * 100).toInt()}%');
});
await fs.writeFiles(fileMap, onProgress: (progress) {
print('Writing ${progress.current.entry.path} — overall ${(progress.overall * 100).toInt()}%');
});
Lifecycle management #
// Release all resources (terminates web workers, resets singleton)
await fs.dispose();
// With custom timeout (default 30 seconds)
await fs.dispose(timeout: const Duration(seconds: 10));
// After dispose, getInstance() creates a fresh instance
final freshFs = await DbasFileSystem.getInstance();
// Calling methods on a disposed instance throws StateError
Cancellation #
final token = CancellationToken();
// Start a bulk write in the background
final future = fs.writeFiles(largeFileMap, cancellationToken: token);
// Cancel later — tasks not yet started are skipped, in-flight tasks complete
token.cancel();
try {
await future;
} on OperationCancelledException {
print('Write was cancelled');
}
Error handling #
try {
await fs.readFile('/nonexistent');
} on FileNotFoundException catch (e) {
print(e.path); // '/nonexistent'
} on PermissionDeniedException catch (e) {
print('Access denied: ${e.path}');
} on OperationCancelledException {
print('Operation was cancelled');
} on DbasFileSystemException catch (e) {
print(e.message);
}
// Atomic bulk writes roll back on failure
try {
await fs.writeFiles({
'path/a.bin': bytesA,
'path/b.bin': bytesB, // if this fails, a.bin is rolled back
});
} on AtomicOperationException catch (e) {
print('Primary error: ${e.error}');
if (e.secondaryError != null) {
print('Rollback also failed: ${e.secondaryError}');
// Some files may not have been rolled back — verify state manually.
}
}
// Non-atomic bulk writes with error handling
await fs.writeFiles(fileMap, atomic: false, onError: (path, error) {
print('Failed to write $path: $error');
});
Storage persistence (web) #
final fs = await DbasFileSystem.getInstance();
// Check if the browser granted persistent storage
if (!fs.isPersistentStorage) {
print('Warning: data may be evicted under storage pressure');
}
API Reference #
| Method | Description |
|---|---|
getInstance({workerPoolSize, onFileChanged}) |
Returns the singleton DbasFileSystem instance. |
dispose({timeout}) |
Releases all resources. Gives in-flight operations up to timeout (default 30s) to finish, then forces teardown. |
isDisposed |
true after dispose() has been called. Subsequent operations throw StateError. |
isPersistentStorage |
Whether storage is persistent (always true on native; reflects browser grant on web). |
onFileChanged |
Getter/setter for the file change notification callback. |
getAppFilePath(fileName) |
Resolves a platform-specific path (forward-slash normalized). Does not create directories. |
writeFile(path, bytes, {overwrite, onProgress}) |
Writes a Uint8List to a file. |
writeFileStream(path, stream, {overwrite, onProgress}) |
Writes a stream of byte chunks to a file. |
appendFile(path, bytes, {onProgress}) |
Appends bytes to a file (creates it if missing). |
appendFileStream(path, stream, {onProgress}) |
Appends a stream of byte chunks to a file (creates it if missing). |
readFile(path, {onProgress}) |
Reads a file as a Uint8List. |
readFileStream(path, {chunkSize}) |
Reads a file as a Stream<Uint8List>. Lock is held for the stream's lifetime. |
deleteFile(path, {onProgress}) |
Deletes a file (no-op if missing). |
fileExists(path) |
Checks whether a file exists. |
copyFile(source, dest, {overwrite, onProgress}) |
Copies a file with byte-level progress on native. |
moveFile(source, dest, {overwrite, onProgress}) |
Moves a file with cross-device fallback. |
renameFile(oldPath, newPath, {overwrite, onProgress}) |
Renames a file (atomic on native, copy+delete on web). |
getFileSize(path) |
Returns the file size in bytes. |
getLastModified(path) |
Returns the last modified timestamp (UTC). |
writeFiles(files, {maxConcurrency, cancellationToken, onProgress, atomic, onError}) |
Writes multiple files. Atomic by default (rollback on failure). Set atomic: false + onError for independent writes. |
writeFilesStream(files, {maxConcurrency, cancellationToken, onProgress, atomic, onError}) |
Writes multiple files from streams. Same atomic/non-atomic semantics as writeFiles. |
readFiles(paths, {maxConcurrency, cancellationToken, onProgress, onError}) |
Reads multiple files. Without onError, throws MultiException on any failure. With onError, returns partial results. |
createDirectory(path, {recursive, onProgress}) |
Creates a directory. |
directoryExists(path) |
Checks whether a directory exists. |
listDirectory(path, {recursive}) |
Lists typed entries (FileSystemEntry with path and type). |
deleteDirectory(path, {recursive, onProgress}) |
Deletes a directory. |
renameDirectory(oldPath, newPath, {onProgress}) |
Renames a directory (atomic on native, recursive copy+delete on web). |
copyDirectory(source, dest, {overwrite, onProgress}) |
Copies a directory (merge semantics). |
moveDirectory(source, dest, {onProgress}) |
Moves a directory with cross-device fallback. |
Thread Safety #
All operations are routed through a hierarchical per-path read-write lock (PathLock).
- Mutating file operations acquire a shared lock on the parent directory and an exclusive lock on the file path. Two writes to different files in the same directory proceed in parallel. A directory delete blocks until child file operations complete.
- Read-only file queries (
fileExists,getFileSize,getLastModified) acquire a shared lock on the file path only, allowing multiple concurrent reads without blocking each other or contending with sibling paths. - Non-destructive directory operations (
listDirectory,directoryExists,createDirectory) acquire a shared lock on the directory path, allowing concurrent access. - Destructive directory operations (
deleteDirectory,renameDirectory,moveDirectory) acquire an exclusive lock that blocks all concurrent file and directory operations within that path. - Multi-path operations (
copyFile,moveFile,renameFile,renameDirectory,copyDirectory,moveDirectory) lock all involved paths in sorted order to prevent deadlocks. Exclusive overrides shared when the same path appears in both lock sets. - Bulk operations (
writeFiles,readFiles) run through the locked single-file methods with a configurable concurrency limit (maxConcurrency, default 10). - Web worker pool distributes work across N workers (default 4) for true parallel I/O via OPFS.
- Writer-priority: When both readers and writers are waiting for a lock, writers are woken first to prevent starvation.
Different paths -> parallel (shared parent locks coexist)
Same path (writes) -> serialized (exclusive file lock queues)
Same path (reads) -> parallel (shared file lock allows concurrent queries)
Dir delete + child op -> serialized (exclusive dir lock blocks shared parent)
Bulk operations -> bounded parallelism (ConcurrencyPool) + per-path locking
Stream lock behavior:
readFileStreamholds the file path lock for the entire stream lifetime. A slow consumer will block other operations on that path until the stream completes or is cancelled.
Bulk Operation Semantics #
Atomic mode (default) #
writeFiles and writeFilesStream with atomic: true (default):
- Snapshot: Existing file contents are saved for rollback.
- Write: All files are written concurrently.
- On failure: All successfully written files are rolled back (restored or deleted). Throws
AtomicOperationExceptionwith the primaryerrorand optionalsecondaryErrorif rollback itself partially failed.
The onError callback is ignored in atomic mode.
Non-atomic mode #
writeFiles and writeFilesStream with atomic: false:
- If
onErroris provided, individual failures invoke the callback and the operation continues. - If
onErrorisnull, throws on the first error. - No rollback is performed on partial failure.
Read operations #
readFiles without onError collects all errors and throws MultiException. With onError, failed files are omitted from the result map.
Performance Tuning #
| Parameter | Default | When to adjust |
|---|---|---|
workerPoolSize |
4 | Increase for web apps doing heavy parallel I/O. Each worker is a Web Worker thread. On native, this is ignored. |
maxConcurrency |
10 | Lower if bulk operations cause memory pressure (many large files). Raise if I/O is the bottleneck and files are small. |
chunkSize |
64 KB | Increase for large file streaming (e.g. 256 KB or 1 MB). Decrease for memory-constrained environments. Affects readFileStream on all platforms. |
Building #
JS Worker minification #
The OPFS Web Worker source is at web/libs/src/dbas_filesystem_worker.js. CI automatically minifies it via esbuild and commits the output to web/libs/. To build locally:
# Linux / macOS / Git Bash
bash scripts/build/minify-js-worker.sh
# Windows PowerShell
.\scripts\build\minify-js-worker.ps1
Both scripts install esbuild automatically if not found.
Migrating from v3.x to v4.x #
Breaking changes #
-
overwritedefaults tofalse: All write, copy, move, and rename operations now throwFileAlreadyExistsExceptionif the destination exists. Addoverwrite: truewhere you intentionally replace files.// v3.x — silently overwrites await fs.writeFile(path, bytes); // v4.x — throws if file exists await fs.writeFile(path, bytes); // throws FileAlreadyExistsException await fs.writeFile(path, bytes, overwrite: true); // explicit overwrite -
writeFiles/writeFilesStreamare atomic by default: Setatomic: falseto restore the previous behavior withonError.// v3.x await fs.writeFiles(files, onError: (p, e) => log(e)); // v4.x — atomic by default await fs.writeFiles(files); // atomic, throws AtomicOperationException await fs.writeFiles(files, atomic: false, onError: (p, e) => log(e)); // non-atomic with onError -
readFileswithoutonErrorthrowsMultiException: Previously threw the first error. Now collects all errors.// v4.x — collect partial results final results = await fs.readFiles(paths, onError: (p, e) => log(e));
Migrating from v1.x to v2.x #
Breaking changes #
-
Uint8ListAPI: All byte parameters and return types changed fromList<int>toUint8List.// v1.x await fs.writeFile(path, [1, 2, 3]); final List<int> bytes = await fs.readFile(path); // v2.x await fs.writeFile(path, Uint8List.fromList([1, 2, 3])); final Uint8List bytes = await fs.readFile(path); -
dispose()added: Instances must be disposed when no longer needed. Using a disposed instance throwsStateError. -
Bulk operations removed from native interface: Now handled exclusively by the platform layer. If you extended the native interface, remove bulk method overrides.
Troubleshooting #
Web: "OPFS is not supported in this browser" #
OPFS requires a modern browser (Chrome 102+, Firefox 111+, Safari 15.2+). Ensure your users are on a supported browser. OPFS is not available in:
- Older browsers
- Some privacy-focused browsers
- Web views that don't support the Storage Foundation API
Web: isPersistentStorage is false #
The browser denied the persistent storage request. This means data may be evicted under storage pressure (e.g. low disk space). Common causes:
- The site isn't bookmarked or frequently visited (Chrome heuristic)
- The user denied the permission prompt
- Private/incognito browsing mode
Your app should handle this gracefully — check isPersistentStorage after initialization and warn the user if needed.
Web: Worker crashes #
Workers automatically restart on crash with exponential backoff (first 3 retries are immediate, then 1s, 2s, 4s... up to 60s, max 5 retries per worker slot). If all workers permanently fail, operations throw DbasFileSystemException. Call dispose() and re-initialize with getInstance(). Common causes:
- Browser memory pressure killing Web Workers
- OPFS quota exceeded
Web: No IndexedDB or LocalStorage fallback #
dbas_filesystem intentionally does not fall back to IndexedDB or LocalStorage when OPFS is unavailable. OPFS is the only storage API that supports the binary, streaming, and parallel I/O semantics this library provides. If OPFS is unavailable, getInstance() throws DbasFileSystemException — catch it and guide the user to a supported browser.
Native: Cross-device move fails #
moveFile and moveDirectory automatically fall back to copy+delete when source and destination are on different filesystems. If the fallback also fails, the partial destination is cleaned up. Check disk space and permissions.
Minimum Platform Versions #
| Platform | Minimum Version |
|---|---|
| Android | API 35 |
| iOS | 16.0 |
| macOS | 13.0 (Ventura) |
| Linux | x86_64 |
| Windows | x86_64 |
| Web | Modern browsers with OPFS support |
License #
Copyright 2025-2026 Daily Software Systems LTDA. Licensed under the Apache License, Version 2.0. See LICENSE for details.