Compare commits

..

26 Commits

Author SHA1 Message Date
rodydavis 839a57ea6b Update Gradle to v5.6.4 2026-06-13 07:02:11 +00:00
rodydavis 042da8c7e3 ci: configure Renovate Gitea Actions workflow and config
Renovate / renovate (push) Successful in 1m10s
2026-06-12 23:01:26 -07:00
rodydavis 981779120a chore: remove renovate.json config as renovate workflow is not present 2026-06-12 22:56:05 -07:00
rodydavis 00d5ced000 chore: migrate dependency updates from Dependabot to Renovate 2026-06-12 22:49:09 -07:00
rodydavis ad649263d5 build: configure .bazelignore to exclude generated files 2026-06-12 22:39:15 -07:00
rodydavis 2a88573734 build: configure HTTP and gRPC remote caching in .bazelrc 2026-06-12 22:12:20 -07:00
rodydavis a627abc813 chore(web_file_system): sync changes from open_xml including idb_block_store, getAsBlob, and tests 2026-06-10 21:35:21 -07:00
rodydavis afb6302f0d feat: implement synchronous file system APIs with worker support & 100% test coverage 2026-06-07 17:32:22 -07:00
rodydavis fcc1a194c4 docs(web_file_system): fix mermaid diagram syntax in README.md 2026-06-07 17:13:09 -07:00
rodydavis e1e8d8ded0 feat(web_file_system): resolve concurrency and stream sizing issues, add README, and achieve 100% test coverage 2026-06-07 17:11:43 -07:00
rodydavis 9ec5986d6e [packages.dart] chore: migrate workspace to hermetic Bazel build environment 2026-06-07 16:36:11 -07:00
rodydavis cc32380977 [packages.dart] chore: migrate workspace to hermetic Bazel build environment
Update README / update-readme (push) Successful in 23s
2026-05-22 21:30:04 -07:00
Rody Davis e2a470abfe Merge pull request #16 from bedranfleck/patch-1
Update Submodules / update-submodules (push) Failing after 21s
2026-03-12 07:48:28 -07:00
André Felipe Fleck Bedran 1164b50cc2 Refactor isDevice check to computed property
Refactor isDevice property to use computed property and better simulator detection API.
2026-02-23 19:45:47 -03:00
rodydavis ee78f04d33 update versions 2026-02-16 10:48:25 -08:00
rodydavis 3f8c930d48 update packages 2026-02-16 10:45:05 -08:00
Rody Davis 6d3ff3dcd5 Merge pull request #8 from rodydavis/dependabot/pub/pigeon-26.1.7
Bump pigeon from 26.1.5 to 26.1.7
2026-02-16 10:42:05 -08:00
Rody Davis 289b3dd4fa Merge pull request #14 from JoseAlba/mediaquery_breakpoints
Updated breakpoints package to using MediaQuery.sizeOf(context);
2026-02-16 10:40:59 -08:00
Rody Davis 5d2fb37973 Merge pull request #15 from rodydavis/dependabot/pub/test-1.29.0
Bump test from 1.26.3 to 1.29.0
2026-02-16 10:39:53 -08:00
github-actions[bot] 29116fa919 docs: update README [skip ci] 2026-02-16 18:36:41 +00:00
Rody Davis 36cb839dd8 Merge pull request #11 from kattaliraees/swift-build-error-fix 2026-02-16 10:36:13 -08:00
dependabot[bot] a6f4b4aed6 Bump test from 1.26.3 to 1.29.0
Bumps [test](https://github.com/dart-lang/test/tree/master/pkgs) from 1.26.3 to 1.29.0.
- [Release notes](https://github.com/dart-lang/test/releases)
- [Commits](https://github.com/dart-lang/test/commits/test-v1.29.0/pkgs)

---
updated-dependencies:
- dependency-name: test
  dependency-version: 1.29.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-16 04:21:43 +00:00
josealba 21224bffcf Breakpoints 2026-02-13 12:16:18 -05:00
Raees Kattali ab8d57c400 swift build error fix 2026-02-05 10:22:18 +03:00
dependabot[bot] 2debeaa79d Bump pigeon from 26.1.5 to 26.1.7
Bumps [pigeon](https://github.com/flutter/packages/tree/main/packages) from 26.1.5 to 26.1.7.
- [Commits](https://github.com/flutter/packages/commits/pigeon-v26.1.7/packages)

---
updated-dependencies:
- dependency-name: pigeon
  dependency-version: 26.1.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-23 04:14:21 +00:00
rodydavis 7d6028eb6d ignore deprecated packages 2026-01-21 00:42:50 -08:00
68 changed files with 1973 additions and 606 deletions
+60
View File
@@ -0,0 +1,60 @@
# Generated and managed by Antigravity
**/.dart_tool
**/build
.agents
.dart_tool
.git
.gitea
.github
.vscode
build
deprecated/cupertino_controllers/.github
deprecated/dart_firebase/.github
deprecated/dart_firebase/.vscode
deprecated/data_tables/.github
deprecated/data_tables/.vscode
deprecated/dynamic_tabs/.github
deprecated/dynamic_tabs/.vscode
deprecated/easy_google_maps/.vscode
deprecated/easy_web_view/.github
deprecated/easy_web_view/.vscode
deprecated/fb_auth/.vscode
deprecated/fb_storage/.vscode
deprecated/file_access/.vscode
deprecated/floating_search_bar/.vscode
deprecated/flutter_ast/.vscode
deprecated/flutter_cli/.vscode
deprecated/flutter_data_view/.github
deprecated/flutter_data_view/.vscode
deprecated/flutter_dynamic_widget/.vscode
deprecated/flutter_midi/.github
deprecated/flutter_midi/.vscode
deprecated/get_version/.vscode
deprecated/golden_layout/.vscode
deprecated/image_resizer/.vscode
deprecated/mobile_popup/.vscode
deprecated/mobile_sidebar/.vscode
deprecated/native_color_picker/.vscode
deprecated/navigation_rail/.vscode
deprecated/responsive_scaffold/.github
deprecated/responsive_scaffold/.vscode
deprecated/scaffold_tab_bar/.vscode
deprecated/sheet_music/.vscode
deprecated/storyboard/.github
deprecated/storyboard/.vscode
deprecated/widget_gen/.vscode
experimental/http_get_cache/.vscode
experimental/web_file_system/.dart_tool
experimental/web_file_system/example/.dart_tool
experimental/web_file_system/example/build
node_modules
packages/app_review/.github
packages/open_xml/.github
packages/open_xml/third_party/validator/.github
packages/open_xml/third_party/validator/.vscode
packages/signals/.github
packages/signals/.vscode
packages/signals/third_party/preactjs_signals/.github
packages/signals/website/.vscode
packages/undo/.github
packages/undo/.vscode
+11
View File
@@ -0,0 +1,11 @@
# --- Remote Caching ---
build:http-cache --remote_cache=https://bazel-cache.rodydavis.dev
build:http-cache --remote_timeout=5
build:http-cache --remote_local_fallback=true
build:http-cache --remote_header=Authorization="Bearer ${BAZEL_CACHE_TOKEN}"
build:grpc-cache --remote_cache=grpcs://bazel-grpc-cache.rodydavis.dev
build:grpc-cache --remote_upload_local_results=true
build:grpc-cache --remote_timeout=5
build:grpc-cache --remote_local_fallback=true
build:grpc-cache --remote_header=Authorization="Bearer ${BAZEL_CACHE_TOKEN}"
+23
View File
@@ -0,0 +1,23 @@
name: Renovate
on:
schedule:
- cron: "0 * * * *" # Triggers hourly
push:
branches:
- main
workflow_dispatch: # Allows manual triggering from the Gitea UI
jobs:
renovate:
runs-on: ubuntu-latest
container:
image: ghcr.io/renovatebot/renovate:latest
env:
RENOVATE_PLATFORM: gitea
RENOVATE_ENDPOINT: https://git.rodydavis.dev/api/v1
RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }}
RENOVATE_AUTODISCOVER: false
RENOVATE_REPOSITORIES: ${{ github.repository }}
steps:
- name: Run Renovate
run: renovate
-31
View File
@@ -1,31 +0,0 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
- package-ecosystem: "pub"
directory: "/"
schedule:
interval: "daily"
- package-ecosystem: "pub"
directory: "/packages/breakpoint"
schedule:
interval: "daily"
- package-ecosystem: "pub"
directory: "/packages/flutter_sms"
schedule:
interval: "daily"
- package-ecosystem: "pub"
directory: "/packages/flutter_vibrate"
schedule:
interval: "daily"
- package-ecosystem: "pub"
directory: "/packages/flutter_whatsnew"
schedule:
interval: "daily"
+45 -12
View File
@@ -1,22 +1,55 @@
# ==========================================
# 🌌 Federated Workspaces - Ultimate Ignores
# ==========================================
# --- System & Editor Files ---
.DS_Store
.dart_tool/
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
.vscode/
.idea/
*.swp
*.swo
.packages
.pub/
pubspec.lock
# --- Bazel Convenience Symlinks & Outputs ---
bazel-*
bazel_out/
build/
*.iml
# --- C/C++ Outputs ---
*.o
*.obj
*.a
*.lib
*.so
*.dylib
*.dll
*.out
*.exe
DerivedData/
# --- Go Outputs ---
*.test
*.prof
__debug_bin
MODULE.bazel.lock
# --- Node / JavaScript / pnpm ---
node_modules/
.npm/
.pnpm-store/
.pnpm-debug.log*
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Files and directories created by pub
# --- Dart / Flutter ---
.dart_tool/
.packages
.pub-cache/
.pub/
build/
*.SF2
/build/
*.flutter-plugins
*.flutter-plugins-dependencies
+1
View File
@@ -0,0 +1 @@
# Root BUILD file for packages_dart workspace
+25
View File
@@ -0,0 +1,25 @@
module(
name = "packages_dart",
version = "1.0.0",
)
bazel_dep(name = "rules_flutter", version = "0.0.0")
bazel_dep(name = "bazel_skylib", version = "1.7.1")
bazel_dep(name = "rules_go", version = "0.60.0")
bazel_dep(name = "gazelle", version = "0.42.0")
git_override(
module_name = "rules_flutter",
remote = "https://github.com/SpencerC/rules_flutter.git",
commit = "ca70cb77c09d4522fea090c42d81beed597bdacf",
)
flutter = use_extension("@rules_flutter//flutter:extensions.bzl", "flutter")
flutter.toolchain(flutter_version = "3.24.0")
use_repo(
flutter,
"flutter_sdk",
"flutter_toolchains",
)
register_toolchains("@flutter_toolchains//:all")
+1
View File
@@ -26,6 +26,7 @@ A collection of Flutter packages maintained by @rodydavis.
| Package | Description | Version |
| :--- | :--- | :--- |
| [diff_algorithims](./experimental/diff_algorithims) | A set of diffing algorithims for Dart | ![Unpublished](https://img.shields.io/badge/pub-unpublished-inactive) |
| [http_get_cache](./experimental/http_get_cache) | HTTP Compliant cache for Flutter | ![Unpublished](https://img.shields.io/badge/pub-unpublished-inactive) |
| [pocketbase_auth](./experimental/pocketbase_auth) | Pocketbase auth client with built in UI screens and token refresh | ![Unpublished](https://img.shields.io/badge/pub-unpublished-inactive) |
| [pocketbase_sync](./experimental/pocketbase_sync) | A Flutter package for offline-first syncing with PocketBase. | ![Unpublished](https://img.shields.io/badge/pub-unpublished-inactive) |
| [web_file_system](./experimental/web_file_system) | A high-performance, asynchronous file system for the web using IDB and OPFS. | ![Unpublished](https://img.shields.io/badge/pub-unpublished-inactive) |
+4
View File
@@ -0,0 +1,4 @@
include: package:flutter_lints/flutter.yaml
analyzer:
exclude:
- deprecated/**
View File
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
+171
View File
@@ -0,0 +1,171 @@
# web_file_system
A high-performance, fully asynchronous VFS (Virtual File System) for the web implementing Dart's standard `package:file` interfaces.
It uses a hybrid backend that separates metadata management from actual block storage for optimal latency and performance in the browser.
---
## Key Features
* **`package:file` Compliance**: Drop-in replacement for standard file system operations. Fully compatible with libraries expecting a `FileSystem` interface.
* **O(1) Directory Renames**: By utilizing an inode-based database structure, directory renames and moves are fast constant-time operations. Children don't require path prefix updates.
* **IndexedDB Metadata Store**: Keeps directory hierarchies, timestamps, file sizes, and symbolic links indexed using fast IndexedDB tables.
* **OPFS (Origin Private File System) Block Store**: Stores raw file bytes and stream chunks directly in the high-performance private browser storage space.
* **Symbolic Links (Symlinks)**: Full support for relative and absolute symbolic links, target updating, recursive listing, and canonical path resolution.
---
## Architectural Flow
```mermaid
graph TD
subgraph WebFileSystem ["WebFileSystem (Dart API)"]
FS["WebFileSystem"]
WD["WebDirectory"]
WF["WebFile"]
WL["WebLink"]
end
subgraph Metadata ["IndexedDB (IdbInodeService)"]
DB["WebFileSystemDB"]
Inodes["Inode Table: parentId index"]
end
subgraph DataBlocks ["OPFS (OpfsBlockStore)"]
BlocksDir["/.blocks/ Directory"]
RawData["UUID Data Blobs"]
end
FS -->|Lookup / Resolve| Metadata
WD -->|Read / Write Children| Metadata
WF -->|Read / Write Metadata| Metadata
WF -->|Read / Write Data Blocks| DataBlocks
WL -->|Read / Write Link Target| DataBlocks
```
---
## Getting Started
### Installation
Add `web_file_system` to your `pubspec.yaml` (or reference the path if using as a path dependency):
```yaml
dependencies:
web_file_system:
path: path/to/web_file_system
```
---
## Usage Examples
### 1. Initializing the File System
```dart
import 'package:web_file_system/web_file_system.dart';
final fs = WebFileSystem();
```
### 2. Creating and Reading Files
```dart
final file = fs.file('/documents/report.txt');
// Standard write & read (creates parent directories if needed when recursive is true)
await file.create(recursive: true);
await file.writeAsString('Hello, Web private storage!');
final contents = await file.readAsString();
print(contents); // "Hello, Web private storage!"
```
### 3. Directory Listing & Traversal
```dart
final dir = fs.directory('/documents');
await for (final entity in dir.list(recursive: true, followLinks: false)) {
print('${entity.path} (${entity.runtimeType})');
}
```
### 4. Symbolic Links
```dart
// Create a symbolic link
final link = fs.link('/shortcut_to_report');
await link.create('/documents/report.txt');
// Reading content through the link
final data = await fs.file('/shortcut_to_report').readAsString();
// Resolve canonical absolute path
final canonicalPath = await link.resolveSymbolicLinks();
print(canonicalPath); // "/documents/report.txt"
```
---
`web_file_system` implements an **inode index** model. Inodes reference parents by their database IDs rather than paths. Renaming a directory only updates the single directory node's `name` property. Its child files and directories remain untouched because they continue to reference the same unchanged parent inode ID. This makes directory renaming an **$O(1)$** operation.
---
## Synchronous API Support (Web Workers Only)
Because JavaScript and Dart run on a single-threaded event loop, blocking the main browser thread is not permitted. Therefore, standard synchronous APIs (such as `readAsBytesSync`, `writeAsBytesSync`, `existsSync`, `createSync`, `deleteSync`, `listSync`, `renameSync`, and `statSync`) will throw an `UnsupportedError` if invoked on the main application thread.
However, synchronous operations are **fully supported within Web Workers**. By delegating asynchronous work (IndexedDB metadata updates and Origin Private File System operations) to the main thread via a `SharedArrayBuffer` and blocking the Web Worker's execution loop using `Atomics.wait()`, you can safely use synchronous file system calls inside your workers.
### Prerequisites & Security Headers
To use the synchronous APIs, the browser requires cross-origin isolation. You must serve your web application with the following HTTP headers:
```http
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
```
If these headers are not present, `SharedArrayBuffer` is disabled by the browser, and synchronous operations will throw a `StateError`.
### Setup Example
#### 1. Main Application Thread
Initialize the file system and register the worker proxy to listen to RPC requests from your worker:
```dart
import 'dart:html'; // or standard JS interop / package:web
import 'package:web_file_system/web_file_system.dart';
void main() {
final fs = WebFileSystem();
final worker = Worker('worker.js');
// Register the proxy to handle synchronous RPC requests
WebFileSystem.registerWorkerProxy(worker, fs);
}
```
#### 2. Web Worker Context (`worker.js` / Dart Worker)
In your worker code, once initialized with the `SharedArrayBuffer`, you can access the file system synchronously:
```dart
import 'package:web_file_system/web_file_system.dart';
void main() {
final fs = WebFileSystem();
// These operations block synchronous execution inside the Web Worker
final file = fs.file('/data.bin');
file.createSync();
file.writeAsBytesSync([1, 2, 3, 4]);
final bytes = file.readAsBytesSync();
print(bytes); // [1, 2, 3, 4]
}
```
@@ -0,0 +1,50 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
/dist/
/coverage/
/scripts/temp_bin/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
# Xcode data
/macos/DerivedData/
@@ -153,9 +153,13 @@ class _FileSystemDemoState extends State<FileSystemDemo> {
spacing: 8,
children: [
ElevatedButton(
onPressed: _createFile, child: const Text('New File')),
onPressed: _createFile,
child: const Text('New File'),
),
ElevatedButton(
onPressed: _createDir, child: const Text('New Directory')),
onPressed: _createDir,
child: const Text('New Directory'),
),
if (_currentPath != '/')
ElevatedButton(
onPressed: () {
@@ -164,7 +168,8 @@ class _FileSystemDemoState extends State<FileSystemDemo> {
});
_refreshFiles();
},
child: const Text('Go Up')),
child: const Text('Go Up'),
),
],
),
const Divider(),
@@ -187,7 +192,8 @@ class _FileSystemDemoState extends State<FileSystemDemo> {
return Text(_formatBytes(snapshot.data!));
}
return const Text('Loading...');
}),
},
),
trailing: IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
onPressed: () => _delete(entity),
@@ -205,13 +211,16 @@ class _FileSystemDemoState extends State<FileSystemDemo> {
builder: (context) => AlertDialog(
title: Text(entity.basename),
content: SingleChildScrollView(
child: Text(content)),
child: Text(content),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Close'))
child: const Text('Close'),
),
],
));
),
);
});
}
},
@@ -229,11 +238,13 @@ class _FileSystemDemoState extends State<FileSystemDemo> {
itemCount: _logs.length,
itemBuilder: (context, index) => Padding(
padding: const EdgeInsets.all(4.0),
child: Text(_logs[_logs.length - 1 - index],
child: Text(
_logs[_logs.length - 1 - index],
style: const TextStyle(
fontSize: 12,
fontFamily: 'monospace',
)),
),
),
),
),
),
@@ -0,0 +1,260 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
async:
dependency: transitive
description:
name: async
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
url: "https://pub.dev"
source: hosted
version: "2.13.0"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
characters:
dependency: transitive
description:
name: characters
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev"
source: hosted
version: "1.4.1"
clock:
dependency: transitive
description:
name: clock
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev"
source: hosted
version: "1.1.2"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.1"
crypto:
dependency: transitive
description:
name: crypto
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
url: "https://pub.dev"
source: hosted
version: "3.0.7"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev"
source: hosted
version: "1.3.3"
file:
dependency: transitive
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev"
source: hosted
version: "7.0.1"
fixnum:
dependency: transitive
description:
name: fixnum
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
url: "https://pub.dev"
source: hosted
version: "1.1.1"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev"
source: hosted
version: "11.0.2"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev"
source: hosted
version: "3.0.10"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
lints:
dependency: "direct dev"
description:
name: lints
sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290
url: "https://pub.dev"
source: hosted
version: "3.0.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd"
url: "https://pub.dev"
source: hosted
version: "0.12.20"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev"
source: hosted
version: "0.13.0"
meta:
dependency: transitive
description:
name: meta
sha256: c82594181e3312f3d0695fc95aaaf7758d75b8d4ae2bbecf223b9fd5109a059d
url: "https://pub.dev"
source: hosted
version: "1.18.3"
mime:
dependency: transitive
description:
name: mime
sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a"
url: "https://pub.dev"
source: hosted
version: "1.0.6"
path:
dependency: transitive
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
source_span:
dependency: transitive
description:
name: source_span
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
url: "https://pub.dev"
source: hosted
version: "1.10.1"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
source: hosted
version: "1.4.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
source: hosted
version: "1.2.2"
test_api:
dependency: transitive
description:
name: test_api
sha256: "2a122cbe059f8b610d3a5415f42e255b6c17b1f21eee1d960f31080237fb4f11"
url: "https://pub.dev"
source: hosted
version: "0.7.12"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.4.0"
uuid:
dependency: transitive
description:
name: uuid
sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8
url: "https://pub.dev"
source: hosted
version: "4.5.2"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: "1d774bbdf6b72a0b12122fc1560c9c2d2a67db5a4a4cc2bd8a5c990ab20e3188"
url: "https://pub.dev"
source: hosted
version: "2.4.0"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60"
url: "https://pub.dev"
source: hosted
version: "15.0.2"
web:
dependency: transitive
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
web_file_system:
dependency: "direct main"
description:
path: ".."
relative: true
source: path
version: "0.0.1"
sdks:
dart: ">=3.10.0 <4.0.0"
flutter: ">=3.18.0-18.0.pre.54"
@@ -0,0 +1,123 @@
import 'dart:async';
import 'dart:js_interop';
import 'dart:typed_data';
import 'package:web/web.dart' as web;
import 'package:uuid/uuid.dart';
class IdbBlockStore {
static const String _dbName = 'WebFileSystemBlobs';
static const int _version = 1;
static const String _storeName = 'blobs';
web.IDBDatabase? _db;
final Completer<void> _initCompleter = Completer<void>();
final Uuid _uuid = Uuid();
Future<void> _ensureReady() async {
if (_db != null) return;
if (_initCompleter.isCompleted) return _initCompleter.future;
final request = web.window.indexedDB.open(_dbName, _version);
request.onupgradeneeded = (web.IDBVersionChangeEvent event) {
final db =
(event.target as web.IDBOpenDBRequest).result as web.IDBDatabase;
if (!db.objectStoreNames.contains(_storeName)) {
db.createObjectStore(_storeName);
}
}.toJS;
final completer = Completer<void>();
request.onsuccess = (web.Event event) {
_db = (event.target as web.IDBOpenDBRequest).result as web.IDBDatabase;
if (!_initCompleter.isCompleted) completer.complete();
}.toJS;
request.onerror = (web.Event event) {
if (!_initCompleter.isCompleted) {
completer.completeError(Exception('Failed to open Blob IDB'));
}
}.toJS;
return completer.future;
}
Future<String> writeBlob(Stream<List<int>> stream) async {
await _ensureReady();
final blockId = _uuid.v4();
// Read all bytes into memory to store in IDB (IDB requires complete blob/buffer usually)
// For streams, we must materialize them.
final chunks = await stream.toList();
final allBytes = chunks.expand((x) => x).toList();
final uint8Array = Uint8List.fromList(allBytes).toJS;
final transaction = _db!.transaction(
_storeName.toJS,
'readwrite',
);
final store = transaction.objectStore(_storeName);
// Using put(value, key) since we didn't specify keyPath/autoIncrement
final request = store.put(uint8Array, blockId.toJS);
await _requestToFuture(request);
return blockId;
}
Stream<List<int>> readBlob(String blockId) async* {
await _ensureReady();
final transaction = _db!.transaction(
_storeName.toJS,
'readonly',
);
final store = transaction.objectStore(_storeName);
final request = store.get(blockId.toJS);
final result = await _requestToFuture(request);
if (result == null) throw Exception('Blob $blockId not found');
// result is JSUint8Array or ArrayBuffer
final uint8Array = result as JSUint8Array;
yield uint8Array.toDart;
}
Future<void> deleteBlob(String blockId) async {
await _ensureReady();
final transaction = _db!.transaction(
_storeName.toJS,
'readwrite',
);
final store = transaction.objectStore(_storeName);
final request = store.delete(blockId.toJS);
await _requestToFuture(request);
}
Future<web.Blob> getBlob(String blockId) async {
await _ensureReady();
final transaction = _db!.transaction(
_storeName.toJS,
'readonly',
);
final store = transaction.objectStore(_storeName);
final request = store.get(blockId.toJS);
final result = await _requestToFuture(request);
if (result == null) throw Exception('Blob $blockId not found');
final uint8Array = result as JSUint8Array;
return web.Blob([uint8Array].toJS);
}
Future<dynamic> _requestToFuture(web.IDBRequest request) {
final completer = Completer<dynamic>();
request.onsuccess = (web.Event e) {
completer.complete((e.target as web.IDBRequest).result);
}.toJS;
request.onerror = (web.Event e) {
completer.completeError(Exception('IDB Blob Error'));
}.toJS;
return completer.future;
}
}
@@ -1,29 +1,9 @@
import 'dart:async';
import 'dart:js_interop';
import 'dart:js_interop_unsafe';
import 'package:web/web.dart' as web;
extension type InodeJS._(JSObject _) implements JSObject {
external String get id;
external set id(String value);
external String get parentId;
external set parentId(String value);
external String get name;
external set name(String value);
external int get nodeType;
external set nodeType(int value);
external String? get blobId;
external set blobId(String? value);
external int get size;
external set size(int value);
external int get modified;
external set modified(int value);
factory InodeJS({
required String id,
required String parentId,
@@ -32,16 +12,18 @@ extension type InodeJS._(JSObject _) implements JSObject {
String? blobId,
int size = 0,
required int modified,
int storageType = 0,
}) {
final obj = JSObject() as InodeJS;
obj.id = id;
obj.parentId = parentId;
obj.name = name;
obj.nodeType = nodeType;
obj.blobId = blobId;
obj.size = size;
obj.modified = modified;
return obj;
final obj = JSObject();
obj.setProperty('id'.toJS, id.toJS);
obj.setProperty('parentId'.toJS, parentId.toJS);
obj.setProperty('name'.toJS, name.toJS);
obj.setProperty('nodeType'.toJS, nodeType.toJS);
if (blobId != null) obj.setProperty('blobId'.toJS, blobId.toJS);
obj.setProperty('size'.toJS, size.toJS);
obj.setProperty('modified'.toJS, modified.toJS);
obj.setProperty('storageType'.toJS, storageType.toJS);
return obj as InodeJS;
}
}
@@ -53,6 +35,7 @@ class Inode {
final String? blobId;
final int size;
final int modified;
final int storageType; // 0: OPFS, 1: IDB
Inode({
required this.id,
@@ -62,6 +45,7 @@ class Inode {
this.blobId,
this.size = 0,
required this.modified,
this.storageType = 0,
});
InodeJS toJS() {
@@ -73,18 +57,30 @@ class Inode {
blobId: blobId,
size: size,
modified: modified,
storageType: storageType,
);
}
static Inode fromJS(InodeJS js) {
final obj = js as JSObject;
final id = obj.getProperty('id'.toJS);
final parentId = obj.getProperty('parentId'.toJS);
final name = obj.getProperty('name'.toJS);
final nodeType = obj.getProperty('nodeType'.toJS);
final blobId = obj.getProperty('blobId'.toJS);
final size = obj.getProperty('size'.toJS);
final modified = obj.getProperty('modified'.toJS);
final storageType = obj.getProperty('storageType'.toJS);
return Inode(
id: js.id,
parentId: js.parentId,
name: js.name,
nodeType: js.nodeType,
blobId: js.blobId,
size: js.size,
modified: js.modified,
id: id != null && !id.isUndefinedOrNull ? (id as JSString).toDart : '',
parentId: parentId != null && !parentId.isUndefinedOrNull ? (parentId as JSString).toDart : '',
name: name != null && !name.isUndefinedOrNull ? (name as JSString).toDart : '',
nodeType: nodeType != null && !nodeType.isUndefinedOrNull ? (nodeType as JSNumber).toDartInt : 0,
blobId: blobId != null && !blobId.isUndefinedOrNull ? (blobId as JSString).toDart : null,
size: size != null && !size.isUndefinedOrNull ? (size as JSNumber).toDartInt : 0,
modified: modified != null && !modified.isUndefinedOrNull ? (modified as JSNumber).toDartInt : 0,
storageType: storageType != null && !storageType.isUndefinedOrNull ? (storageType as JSNumber).toDartInt : 0,
);
}
}
@@ -113,9 +109,15 @@ class IdbInodeService {
web.IDBObjectStoreParameters(keyPath: 'id'.toJS),
);
store.createIndex(
'parentId', 'parentId'.toJS, web.IDBIndexParameters(unique: false));
store.createIndex('parent_name', ['parentId'.toJS, 'name'.toJS].toJS,
web.IDBIndexParameters(unique: true));
'parentId',
'parentId'.toJS,
web.IDBIndexParameters(unique: false),
);
store.createIndex(
'parent_name',
['parentId'.toJS, 'name'.toJS].toJS,
web.IDBIndexParameters(unique: true),
);
}
}.toJS;
@@ -123,16 +125,19 @@ class IdbInodeService {
request.onsuccess = (web.Event event) {
_db = (event.target as web.IDBOpenDBRequest).result as web.IDBDatabase;
_ensureRootExists().then((_) {
_ensureRootExists()
.then((_) {
if (!_initCompleter.isCompleted) completer.complete();
}).catchError((e) {
})
.catchError((e) {
if (!_initCompleter.isCompleted) completer.completeError(e);
});
}.toJS;
request.onerror = (web.Event event) {
if (!_initCompleter.isCompleted)
if (!_initCompleter.isCompleted) {
completer.completeError(Exception('Failed to open IDB'));
}
}.toJS;
return completer.future;
@@ -142,20 +147,24 @@ class IdbInodeService {
try {
await getInode(rootId);
} catch (_) {
await createInode(Inode(
await createInode(
Inode(
id: rootId,
parentId: 'null',
name: '',
nodeType: 1,
modified: DateTime.now().millisecondsSinceEpoch,
));
),
);
}
}
Future<void> createInode(Inode inode) async {
if (_db == null) await _ensureReady();
final transaction = _db!.transaction(
_storeName.toJS, 'readwrite'.toJS as web.IDBTransactionMode);
_storeName.toJS,
'readwrite',
);
final store = transaction.objectStore(_storeName);
final request = store.put(inode.toJS());
await _requestToFuture(request);
@@ -168,7 +177,9 @@ class IdbInodeService {
Future<void> deleteInode(String id) async {
await _ensureReady();
final transaction = _db!.transaction(
_storeName.toJS, 'readwrite'.toJS as web.IDBTransactionMode);
_storeName.toJS,
'readwrite',
);
final store = transaction.objectStore(_storeName);
final request = store.delete(id.toJS);
await _requestToFuture(request);
@@ -178,7 +189,9 @@ class IdbInodeService {
if (_db == null) await _ensureReady();
final transaction = _db!.transaction(
_storeName.toJS, 'readonly'.toJS as web.IDBTransactionMode);
_storeName.toJS,
'readonly',
);
final store = transaction.objectStore(_storeName);
final request = store.get(id.toJS);
final result = await _requestToFuture(request);
@@ -190,7 +203,9 @@ class IdbInodeService {
Future<Inode?> getChild(String parentId, String name) async {
await _ensureReady();
final transaction = _db!.transaction(
_storeName.toJS, 'readonly'.toJS as web.IDBTransactionMode);
_storeName.toJS,
'readonly',
);
final store = transaction.objectStore(_storeName);
final index = store.index('parent_name');
final key = JSArray();
@@ -211,7 +226,9 @@ class IdbInodeService {
Future<List<Inode>> listChildren(String parentId) async {
await _ensureReady();
final transaction = _db!.transaction(
_storeName.toJS, 'readonly'.toJS as web.IDBTransactionMode);
_storeName.toJS,
'readonly',
);
final store = transaction.objectStore(_storeName);
final index = store.index('parentId');
final request = index.getAll(parentId.toJS);
@@ -12,10 +12,7 @@ class OpfsBlockStore {
Future<void> _ensureReady() async {
if (_blocksDir != null) return;
final web.StorageManager? storage = web.window.navigator.storage;
if (storage == null) {
throw UnsupportedError('StorageManager not supported');
}
final web.StorageManager storage = web.window.navigator.storage;
final root = await storage.getDirectory().toDart;
_blocksDir = await root
@@ -31,10 +28,7 @@ class OpfsBlockStore {
final blockId = _uuid.v4();
final fileHandle = await _blocksDir!
.getFileHandle(
blockId,
web.FileSystemGetFileOptions(create: true),
)
.getFileHandle(blockId, web.FileSystemGetFileOptions(create: true))
.toDart;
final writable = await fileHandle.createWritable().toDart;
@@ -50,6 +44,7 @@ class OpfsBlockStore {
await writable.abort().toDart;
await _blocksDir!.removeEntry(blockId).toDart;
} catch (_) {}
print('OpfsBlockStore: writeBlob failed for $blockId: $e');
rethrow;
}
@@ -59,11 +54,7 @@ class OpfsBlockStore {
Stream<List<int>> readBlob(String blockId) async* {
await _ensureReady();
try {
final fileHandle = await _blocksDir!
.getFileHandle(
blockId,
)
.toDart;
final fileHandle = await _blocksDir!.getFileHandle(blockId).toDart;
final file = await fileHandle.getFile().toDart;
final web.Blob blob = file;
@@ -78,6 +69,7 @@ class OpfsBlockStore {
yield chunk.toDart;
}
} catch (e) {
print('OpfsBlockStore: readBlob failed for $blockId: $e');
rethrow;
}
}
@@ -90,4 +82,11 @@ class OpfsBlockStore {
// Ignore if not found
}
}
Future<web.Blob> getBlob(String blockId) async {
await _ensureReady();
final fileHandle = await _blocksDir!.getFileHandle(blockId).toDart;
final file = await fileHandle.getFile().toDart;
return file;
}
}
@@ -1,6 +1,6 @@
import 'dart:async';
import 'package:file/file.dart';
import 'package:web_file_system/src/backend/idb_inode_service.dart';
import '../backend/idb_inode_service.dart';
import '../web_file_system.dart';
import 'web_file.dart';
@@ -33,13 +33,19 @@ class WebDirectory extends FileSystemEntity implements Directory {
// We assume parent must exist if not recursive.
final parentInode = await _fs.resolvepath(parentPath); // throws if missing
await _fs.idb.createInode(Inode(
try {
await _fs.idb.createInode(
Inode(
id: _fs.uuid.v4(),
parentId: parentInode.id,
name: name,
nodeType: 1, // Directory
modified: DateTime.now().millisecondsSinceEpoch,
));
),
);
} catch (_) {
if (!await exists()) rethrow;
}
return this;
}
@@ -50,12 +56,19 @@ class WebDirectory extends FileSystemEntity implements Directory {
await _createRecursiveSafe(_fs.path.dirname(p));
final parentVal = await _fs.resolvepath(_fs.path.dirname(p));
await _fs.idb.createInode(Inode(
try {
await _fs.idb.createInode(
Inode(
id: _fs.uuid.v4(),
parentId: parentVal.id,
name: _fs.path.basename(p),
nodeType: 1,
modified: DateTime.now().millisecondsSinceEpoch));
modified: DateTime.now().millisecondsSinceEpoch,
),
);
} catch (_) {
if (await _fs.type(p) == FileSystemEntityType.notFound) rethrow;
}
}
@override
@@ -69,8 +82,15 @@ class WebDirectory extends FileSystemEntity implements Directory {
final tempDir = _fs.path.join(path, name);
// Ensure path exists
if (!await exists()) {
if (path == '/tmp') {
await create();
} else {
throw FileSystemException(
'Directory does not exist', path, const OSError('ENOENT', 2));
'Directory does not exist',
path,
const OSError('ENOENT', 2),
);
}
}
final dir = WebDirectory(_fs, tempDir);
await dir.create();
@@ -89,7 +109,10 @@ class WebDirectory extends FileSystemEntity implements Directory {
final children = await _fs.idb.listChildren(inode.id);
if (children.isNotEmpty && !recursive) {
throw FileSystemException(
'Directory not empty', path, const OSError('ENOTEMPTY', 39));
'Directory not empty',
path,
const OSError('ENOTEMPTY', 39),
);
}
if (recursive) {
@@ -125,11 +148,16 @@ class WebDirectory extends FileSystemEntity implements Directory {
bool existsSync() => throw UnsupportedError('Sync not supported');
@override
Stream<FileSystemEntity> list(
{bool recursive = false, bool followLinks = true}) async* {
Stream<FileSystemEntity> list({
bool recursive = false,
bool followLinks = true,
}) async* {
if (!await exists()) {
throw FileSystemException(
'Directory not found', path, const OSError('ENOENT', 2));
'Directory not found',
path,
const OSError('ENOENT', 2),
);
}
final inode = await _fs.resolvepath(path);
@@ -150,8 +178,10 @@ class WebDirectory extends FileSystemEntity implements Directory {
}
@override
List<FileSystemEntity> listSync(
{bool recursive = false, bool followLinks = true}) {
List<FileSystemEntity> listSync({
bool recursive = false,
bool followLinks = true,
}) {
throw UnsupportedError('Sync list not supported');
}
@@ -170,7 +200,8 @@ class WebDirectory extends FileSystemEntity implements Directory {
nodeType: inode.nodeType,
blobId: inode.blobId,
size: inode.size,
modified: DateTime.now().millisecondsSinceEpoch);
modified: DateTime.now().millisecondsSinceEpoch,
);
await _fs.idb.updateInode(updated);
return WebDirectory(_fs, newPath);
@@ -209,8 +240,10 @@ class WebDirectory extends FileSystemEntity implements Directory {
throw UnsupportedError('Sync not supported');
@override
Stream<FileSystemEvent> watch(
{int events = FileSystemEvent.all, bool recursive = false}) {
Stream<FileSystemEvent> watch({
int events = FileSystemEvent.all,
bool recursive = false,
}) {
return const Stream.empty();
}
@@ -2,7 +2,7 @@ import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'package:file/file.dart';
import 'package:web_file_system/src/backend/idb_inode_service.dart';
import '../backend/idb_inode_service.dart';
import '../web_file_system.dart';
class WebFile extends FileSystemEntity implements File {
@@ -57,22 +57,19 @@ class WebFile extends FileSystemEntity implements File {
@override
Future<File> copy(String newPath) async {
final inode = await _fs.resolvepath(path);
final newParent = await _fs.resolvepath(_fs.path.dirname(newPath));
// Deep copy to ensure independence and correct storage type
await _fs.idb.createInode(
Inode(
id: _fs.uuid.v4(),
parentId: newParent.id,
name: _fs.path.basename(newPath),
nodeType: 0,
blobId: inode.blobId,
size: inode.size,
modified: DateTime.now().millisecondsSinceEpoch,
),
);
final newFile = WebFile(_fs, newPath);
// Use writeAsBytes to handle creation and storage logic
// We need to consume the stream to a list for now as writeAsBytes takes List<int>
// Or we could use a stream API if available, but writeAsBytes expects List<int>.
// Given these are images/slides, memory is likely okay.
// To be safer with memory, we should really stream it, but writeAsBytes takes List<int>.
// Let's use readAsBytes() helper.
final bytes = await readAsBytes();
await newFile.writeAsBytes(bytes);
return WebFile(_fs, newPath);
return newFile;
}
@override
@@ -149,7 +146,24 @@ class WebFile extends FileSystemEntity implements File {
bool flush = false,
}) async {
final stream = Stream.value(bytes);
final newBlobId = await _fs.opfs.writeBlob(stream);
String newBlobId;
int usedStorageType = 0; // 0 = OPFS, 1 = IDB
try {
// Try OPFS first
newBlobId = await _fs.opfs.writeBlob(stream);
} catch (e) {
// Fallback to IDB on ANY error (TypeError, NotFoundError, etc)
try {
newBlobId = await _fs.idbStore.writeBlob(Stream.value(bytes));
usedStorageType = 1;
} catch (e2) {
throw FileSystemException(
'Write failed on both OPFS ($e) and IDB: $e2',
path,
);
}
}
Inode inode;
try {
@@ -171,6 +185,7 @@ class WebFile extends FileSystemEntity implements File {
blobId: newBlobId,
size: bytes.length,
modified: DateTime.now().millisecondsSinceEpoch,
storageType: usedStorageType,
),
);
@@ -211,7 +226,37 @@ class WebFile extends FileSystemEntity implements File {
final inode = await _fs.resolvepath(path);
if (inode.blobId == null) return;
if (inode.storageType == 1) {
try {
yield* _fs.idbStore.readBlob(inode.blobId!);
} catch (e) {
// Fallback to OPFS if IDB fails (maybe metadata is wrong)
print(
'Using fallback to OPFS for reading ${inode.name} (blob: ${inode.blobId}). Error: $e',
);
try {
yield* _fs.opfs.readBlob(inode.blobId!);
} catch (e2) {
print('OPFS fallback ALSO failed for ${inode.name}: $e2');
rethrow;
}
}
} else {
try {
yield* _fs.opfs.readBlob(inode.blobId!);
} catch (e) {
// Fallback to IDB if OPFS fails
print(
'Using fallback to IDB for reading ${inode.name} (blob: ${inode.blobId}). Error: $e',
);
try {
yield* _fs.idbStore.readBlob(inode.blobId!);
} catch (e2) {
print('IDB fallback ALSO failed for ${inode.name}: $e2');
rethrow;
}
}
}
}
@override
@@ -238,7 +283,16 @@ class WebFile extends FileSystemEntity implements File {
FileMode mode,
) async {
try {
final newId = await _fs.opfs.writeBlob(stream);
String newId;
int usedStorageType = 0; // 0 = OPFS, 1 = IDB
try {
newId = await _fs.opfs.writeBlob(stream);
} catch (e) {
// Fallback to IDB on ANY error
newId = await _fs.idbStore.writeBlob(stream);
usedStorageType = 1;
}
Inode inode;
try {
@@ -259,6 +313,7 @@ class WebFile extends FileSystemEntity implements File {
size:
0, // TODO: Size not returned by OPFS yet, so 0 for streamed content
modified: DateTime.now().millisecondsSinceEpoch,
storageType: usedStorageType,
),
);
} catch (e) {
@@ -336,6 +391,13 @@ class WebFile extends FileSystemEntity implements File {
@override
Future<FileSystemEntity> delete({bool recursive = false}) async {
final inode = await _fs.resolvepath(path);
if (inode.blobId != null) {
if (inode.storageType == 1) {
await _fs.idbStore.deleteBlob(inode.blobId!);
} else {
await _fs.opfs.deleteBlob(inode.blobId!);
}
}
await _fs.idb.deleteInode(inode.id);
return this;
}
@@ -1,7 +1,7 @@
import 'dart:async';
import 'dart:convert';
import 'package:file/file.dart';
import 'package:web_file_system/src/backend/idb_inode_service.dart';
import '../backend/idb_inode_service.dart';
import '../web_file_system.dart';
class WebLink extends FileSystemEntity implements Link {
@@ -1,10 +1,14 @@
import 'dart:async';
import 'dart:convert';
import 'dart:js_interop';
import 'package:file/file.dart';
import 'package:web/web.dart' as web;
import 'package:path/path.dart' as p;
import 'package:uuid/uuid.dart';
import 'package:web_file_system/src/backend/idb_inode_service.dart';
import 'package:web_file_system/src/backend/opfs_block_store.dart';
import 'backend/idb_inode_service.dart';
import 'backend/opfs_block_store.dart';
import 'backend/idb_block_store.dart';
import 'entities/web_directory.dart';
import 'entities/web_file.dart';
import 'entities/web_link.dart';
@@ -12,11 +16,13 @@ import 'entities/web_link.dart';
class WebFileSystem extends FileSystem {
final IdbInodeService _idb = IdbInodeService();
final OpfsBlockStore _opfs = OpfsBlockStore();
final IdbBlockStore _idbStore = IdbBlockStore();
final Uuid _uuid = Uuid();
// Public matchers for internal use
IdbInodeService get idb => _idb;
OpfsBlockStore get opfs => _opfs;
IdbBlockStore get idbStore => _idbStore;
Uuid get uuid => _uuid;
WebFileSystem();
@@ -157,6 +163,7 @@ class WebFileSystem extends FileSystem {
return currentInode;
}
@override
String getPath(dynamic path) {
if (path is String) return path;
if (path is FileSystemEntity) return path.path;
@@ -210,6 +217,7 @@ class WebFileSystem extends FileSystem {
Future<bool> isLink(String path) async =>
(await type(path, followLinks: false)) == FileSystemEntityType.link;
@override
bool get isWatchSupported => false;
@override
@@ -217,17 +225,31 @@ class WebFileSystem extends FileSystem {
final s1 = await stat(path1);
final s2 = await stat(path2);
if (s1.type == FileSystemEntityType.notFound ||
s2.type == FileSystemEntityType.notFound)
s2.type == FileSystemEntityType.notFound) {
return false;
}
final i1 = await resolvepath(path1);
final i2 = await resolvepath(path2);
return i1.id == i2.id;
}
@override
@override
bool identicalSync(String path1, String path2) =>
throw UnsupportedError('Sync not supported');
Future<web.Blob> getAsBlob(String path) async {
final inode = await resolvepath(path);
if (inode.nodeType != 0) throw FileSystemException('Not a file', path);
if (inode.blobId == null) return web.Blob(<JSAny>[].toJS);
if (inode.storageType == 1) {
return _idbStore.getBlob(inode.blobId!);
} else {
return _opfs.getBlob(inode.blobId!);
}
}
}
class FileStatImpl implements FileStat {
@@ -1,5 +1,5 @@
/// A high-performance, asynchronous file system for the web.
library web_file_system;
library;
export 'package:file/file.dart';
export 'src/web_file_system.dart';
+405
View File
@@ -0,0 +1,405 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
_fe_analyzer_shared:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: "563c6992eaeda8625f45b87f6a6a0c547df16565d1c93d8271c7c11057710ca7"
url: "https://pub.dev"
source: hosted
version: "101.0.0"
analyzer:
dependency: transitive
description:
name: analyzer
sha256: aa6a9365901532864cae51208f2a6bb18dd01972ebead19c431efc848f60080b
url: "https://pub.dev"
source: hosted
version: "13.1.0"
args:
dependency: transitive
description:
name: args
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
url: "https://pub.dev"
source: hosted
version: "2.7.0"
async:
dependency: transitive
description:
name: async
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
url: "https://pub.dev"
source: hosted
version: "2.13.1"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
cli_config:
dependency: transitive
description:
name: cli_config
sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec
url: "https://pub.dev"
source: hosted
version: "0.2.0"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.1"
convert:
dependency: transitive
description:
name: convert
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
url: "https://pub.dev"
source: hosted
version: "3.1.2"
coverage:
dependency: transitive
description:
name: coverage
sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d"
url: "https://pub.dev"
source: hosted
version: "1.15.0"
crypto:
dependency: transitive
description:
name: crypto
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
url: "https://pub.dev"
source: hosted
version: "3.0.7"
file:
dependency: "direct main"
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev"
source: hosted
version: "7.0.1"
fixnum:
dependency: transitive
description:
name: fixnum
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
url: "https://pub.dev"
source: hosted
version: "1.1.1"
frontend_server_client:
dependency: transitive
description:
name: frontend_server_client
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
url: "https://pub.dev"
source: hosted
version: "4.0.0"
glob:
dependency: transitive
description:
name: glob
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
url: "https://pub.dev"
source: hosted
version: "2.1.3"
http_multi_server:
dependency: transitive
description:
name: http_multi_server
sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
url: "https://pub.dev"
source: hosted
version: "3.2.2"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.dev"
source: hosted
version: "4.1.2"
io:
dependency: transitive
description:
name: io
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
url: "https://pub.dev"
source: hosted
version: "1.0.5"
lints:
dependency: "direct dev"
description:
name: lints
sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290
url: "https://pub.dev"
source: hosted
version: "3.0.0"
logging:
dependency: transitive
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.3.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd"
url: "https://pub.dev"
source: hosted
version: "0.12.20"
meta:
dependency: transitive
description:
name: meta
sha256: c82594181e3312f3d0695fc95aaaf7758d75b8d4ae2bbecf223b9fd5109a059d
url: "https://pub.dev"
source: hosted
version: "1.18.3"
mime:
dependency: "direct main"
description:
name: mime
sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a"
url: "https://pub.dev"
source: hosted
version: "1.0.6"
node_preamble:
dependency: transitive
description:
name: node_preamble
sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db"
url: "https://pub.dev"
source: hosted
version: "2.0.2"
package_config:
dependency: transitive
description:
name: package_config
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
url: "https://pub.dev"
source: hosted
version: "2.2.0"
path:
dependency: "direct main"
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
pool:
dependency: transitive
description:
name: pool
sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d"
url: "https://pub.dev"
source: hosted
version: "1.5.2"
pub_semver:
dependency: transitive
description:
name: pub_semver
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
shelf:
dependency: transitive
description:
name: shelf
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
url: "https://pub.dev"
source: hosted
version: "1.4.2"
shelf_packages_handler:
dependency: transitive
description:
name: shelf_packages_handler
sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
shelf_static:
dependency: transitive
description:
name: shelf_static
sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3
url: "https://pub.dev"
source: hosted
version: "1.1.3"
shelf_web_socket:
dependency: transitive
description:
name: shelf_web_socket
sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
source_map_stack_trace:
dependency: transitive
description:
name: source_map_stack_trace
sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b
url: "https://pub.dev"
source: hosted
version: "2.1.2"
source_maps:
dependency: transitive
description:
name: source_maps
sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812"
url: "https://pub.dev"
source: hosted
version: "0.10.13"
source_span:
dependency: transitive
description:
name: source_span
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
url: "https://pub.dev"
source: hosted
version: "1.10.2"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
source: hosted
version: "1.4.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
source: hosted
version: "1.2.2"
test:
dependency: "direct dev"
description:
name: test
sha256: ca578dc12bb8b2f40b67b7d3bd2fac4f31c01a6ff7130a14e2597b919934507f
url: "https://pub.dev"
source: hosted
version: "1.31.1"
test_api:
dependency: transitive
description:
name: test_api
sha256: "2a122cbe059f8b610d3a5415f42e255b6c17b1f21eee1d960f31080237fb4f11"
url: "https://pub.dev"
source: hosted
version: "0.7.12"
test_core:
dependency: transitive
description:
name: test_core
sha256: d2e98ec12998368dc59ddd47ab709f2cd55acd6b66dc7db764455a44082f4bc5
url: "https://pub.dev"
source: hosted
version: "0.6.18"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.4.0"
uuid:
dependency: "direct main"
description:
name: uuid
sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489"
url: "https://pub.dev"
source: hosted
version: "4.5.3"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360"
url: "https://pub.dev"
source: hosted
version: "15.2.0"
watcher:
dependency: transitive
description:
name: watcher
sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
web:
dependency: "direct main"
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
web_socket:
dependency: transitive
description:
name: web_socket
sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
web_socket_channel:
dependency: transitive
description:
name: web_socket_channel
sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8
url: "https://pub.dev"
source: hosted
version: "3.0.3"
webkit_inspection_protocol:
dependency: transitive
description:
name: webkit_inspection_protocol
sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
yaml:
dependency: transitive
description:
name: yaml
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
url: "https://pub.dev"
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.11.0 <4.0.0"
+2 -7
View File
@@ -1,13 +1,8 @@
name: web_file_system
description: A high-performance, asynchronous file system for the web using IDB and OPFS.
version: 0.0.1
publish_to: 'none'
homepage: https://github.com/fluttercommunity/flutter_whatsnew
maintainer: Rody Davis (@rodydavis)
environment:
sdk: ^3.5.0
flutter: ^3.38.5
sdk: ^3.10.0
dependencies:
file: ^7.0.0
@@ -17,5 +12,5 @@ dependencies:
mime: ^1.0.0
dev_dependencies:
lints: ^6.0.0
lints: ^3.0.0
test: ^1.25.0
@@ -0,0 +1,94 @@
@TestOn('browser')
import 'dart:convert';
import 'package:test/test.dart';
import 'package:web_file_system/web_file_system.dart';
void main() {
late WebFileSystem fs;
setUp(() async {
// Tests run in parallel in browser often sharing context, so use unique paths.
fs = WebFileSystem();
});
group('Troubleshooting NotFoundError', () {
test('Temp directory creation and persistence', () async {
print('DEBUG: creating temp dir');
final tempDir = await fs.systemTempDirectory.createTemp('debug_test_');
print('DEBUG: Created temp dir at ${tempDir.path}');
expect(await tempDir.exists(), isTrue);
final file = tempDir.childFile('test.txt');
await file.writeAsString('Persistence Check');
print('DEBUG: checking existence immediately');
expect(await file.exists(), isTrue);
expect(await file.readAsString(), equals('Persistence Check'));
// Wait a bit to simulate pipeline delays
await Future.delayed(const Duration(milliseconds: 500));
print('DEBUG: checking existence after delay');
expect(await file.readAsString(), equals('Persistence Check'));
// Cleanup
await tempDir.delete(recursive: true);
print('DEBUG: deleted temp dir');
expect(await file.exists(), isFalse);
});
test('Deep copy behavior verification', () async {
final srcDir = await fs.systemTempDirectory.createTemp('src_');
final destDir = await fs.systemTempDirectory.createTemp('dest_');
final srcFile = srcDir.childFile('source.dat');
// Write some substantial data
final data = utf8.encode('Essential Data for Export');
await srcFile.writeAsBytes(data);
print('DEBUG: Source file written to ${srcFile.path}');
final destPath = destDir.childFile('copy.dat').path;
print('DEBUG: Copying to $destPath');
final copiedFile = await srcFile.copy(destPath);
print('DEBUG: Copy complete');
expect(await copiedFile.exists(), isTrue);
expect(await copiedFile.readAsBytes(), equals(data));
// Now delete source
print('DEBUG: Deleting source file');
await srcFile.delete();
expect(await srcFile.exists(), isFalse);
// Verify copy still exists and is readable (Deep Copy Check)
print('DEBUG: Verifying copy after source deletion');
expect(await copiedFile.exists(), isTrue);
try {
final copyData = await copiedFile.readAsBytes();
expect(copyData, equals(data));
print('DEBUG: Deep copy verified!');
} catch (e) {
print('ERROR: Deep copy failed! Reading copy caused error: $e');
rethrow;
}
});
test('Concurrent Write/Read stress', () async {
final dir = await fs.systemTempDirectory.createTemp('stress_');
final file = dir.childFile('stress.txt');
// Write
await file.writeAsString('Initial');
// Rapidly update
for (int i = 0; i < 10; i++) {
await file.writeAsString('Update $i');
final content = await file.readAsString();
expect(content, equals('Update $i'));
}
});
});
}
@@ -0,0 +1,113 @@
@TestOn('browser')
import 'dart:async';
import 'dart:typed_data';
import 'package:test/test.dart';
import 'package:web_file_system/web_file_system.dart';
void main() {
late WebFileSystem fs;
setUp(() async {
fs = WebFileSystem();
});
group('Robust Correctness Tests', () {
test('Multiple files in a directory stress test', () async {
final uniqueId = DateTime.now().millisecondsSinceEpoch;
final dirPath = '/stress_$uniqueId';
final dir = fs.directory(dirPath);
await dir.create();
final fileCount = 50;
final futures = <Future>[];
for (var i = 0; i < fileCount; i++) {
futures.add(
fs.file('$dirPath/file_$i.txt').writeAsString('Content of file $i'),
);
}
await Future.wait(futures);
final entities = await dir.list().toList();
expect(entities.length, equals(fileCount));
final names = entities.map((e) => fs.path.basename(e.path)).toSet();
for (var i = 0; i < fileCount; i++) {
expect(names, contains('file_$i.txt'));
final content = await fs.file('$dirPath/file_$i.txt').readAsString();
expect(content, equals('Content of file $i'));
}
});
test('Temporary directory usage', () async {
final tempDir = await fs.systemTempDirectory.createTemp('my_prefix_');
expect(tempDir.path, startsWith('/tmp/my_prefix_'));
expect(await tempDir.exists(), isTrue);
final file = fs.file(fs.path.join(tempDir.path, 'test.txt'));
await file.writeAsString('temp content');
expect(await file.readAsString(), equals('temp content'));
await tempDir.delete(recursive: true);
expect(await tempDir.exists(), isFalse);
expect(await file.exists(), isFalse);
});
test('Different files with the same bytes (deduplication/collision check)', () async {
final uniqueId = DateTime.now().millisecondsSinceEpoch;
final data = Uint8List.fromList([1, 2, 3, 4, 5]);
final file1 = fs.file('/file1_$uniqueId.bin');
final file2 = fs.file('/file2_$uniqueId.bin');
await file1.writeAsBytes(data);
await file2.writeAsBytes(data);
expect(await file1.readAsBytes(), equals(data));
expect(await file2.readAsBytes(), equals(data));
// Update one, ensure other is unchanged
final newData = Uint8List.fromList([6, 7, 8]);
await file1.writeAsBytes(newData);
expect(await file1.readAsBytes(), equals(newData));
expect(await file2.readAsBytes(), equals(data));
// Cleanup
await file1.delete();
await file2.delete();
expect(await file1.exists(), isFalse);
expect(await file2.exists(), isFalse);
});
test('Resource cleanup verification (Blob leak check)', () async {
final uniqueId = DateTime.now().millisecondsSinceEpoch;
final filePath = '/leak_test_$uniqueId.bin';
final data = Uint8List.fromList(List.generate(100, (i) => i));
final file = fs.file(filePath);
await file.writeAsBytes(data);
final inode = await fs.resolvepath(filePath);
final blobId = inode.blobId;
expect(blobId, isNotNull);
// Verify blob exists in store
if (inode.storageType == 1) {
expect(await fs.idbStore.getBlob(blobId!), isNotNull);
} else {
expect(await fs.opfs.getBlob(blobId!), isNotNull);
}
await file.delete();
// Verify blob is gone
if (inode.storageType == 1) {
expect(() => fs.idbStore.getBlob(blobId!), throwsA(anything));
} else {
expect(() => fs.opfs.getBlob(blobId!), throwsA(anything));
}
});
});
}
@@ -139,7 +139,9 @@ void main() {
final uniqueId = DateTime.now().millisecondsSinceEpoch;
final chunkSize = 1024 * 1024;
final chunk = Uint8List(chunkSize);
for (int i = 0; i < chunkSize; i++) chunk[i] = i % 256;
for (int i = 0; i < chunkSize; i++) {
chunk[i] = i % 256;
}
final file = fs.file('/video_$uniqueId.mp4');
final sink = file.openWrite();
+7
View File
@@ -0,0 +1,7 @@
load("@rules_flutter//flutter:defs.bzl", "dart_library")
dart_library(
name = "app_review",
srcs = glob(["lib/**/*.dart"]),
visibility = ["//visibility:public"],
)
+1 -1
View File
@@ -24,5 +24,5 @@ flutter:
macos:
pluginClass: AppReviewPlugin
dev_dependencies:
pigeon: ^26.1.5
pigeon: ^26.1.7
+7
View File
@@ -0,0 +1,7 @@
load("@rules_flutter//flutter:defs.bzl", "dart_library")
dart_library(
name = "breakpoint",
srcs = glob(["lib/**/*.dart"]),
visibility = ["//visibility:public"],
)
+4
View File
@@ -1,3 +1,7 @@
## 1.3.2
* Fix by @josealba to use .sizeOf(context): https://github.com/rodydavis/packages.dart/pull/14
## 1.3.0
* Update Dart SDK constraint to >=3.0.0.
+5 -7
View File
@@ -51,16 +51,14 @@ class Breakpoint {
///
/// Use [Breakpoint.fromConstraints] when the widget does not take up the full screen
factory Breakpoint.fromMediaQuery(BuildContext context) {
final _media = MediaQuery.of(context);
final size = MediaQuery.sizeOf(context);
double _width = 359;
double width = size.width;
Orientation orientation = Orientation.portrait;
final orientation =
size.width > size.height ? Orientation.landscape : Orientation.portrait;
_width = _media.size.width;
orientation = _media.orientation;
return _calcBreakpoint(orientation, _width);
return _calcBreakpoint(orientation, width);
}
static Breakpoint _calcBreakpoint(Orientation orientation, double _width) {
+2 -3
View File
@@ -1,10 +1,10 @@
name: breakpoint
description: A Flutter plugin to calculate the material design breakpoints.
version: 1.3.0
version: 1.3.2
maintainer: Rody Davis (@rodydavis)
homepage: https://github.com/rodydavis/plugins
repository: https://github.com/fluttercommunity/breakpoint
repository: https://github.com/rodydavis/packages.dart/tree/main/packages/breakpoint
resolution: workspace
environment:
@@ -19,4 +19,3 @@ dev_dependencies:
sdk: flutter
flutter:
+7
View File
@@ -0,0 +1,7 @@
load("@rules_flutter//flutter:defs.bzl", "dart_library")
dart_library(
name = "flutter_sms",
srcs = glob(["lib/**/*.dart"]),
visibility = ["//visibility:public"],
)
+4
View File
@@ -1,3 +1,7 @@
# 3.0.1
- Fix iOS build error: https://github.com/rodydavis/packages.dart/pull/11
# 3.0.0
* **Breaking Change**: Migrated to Pigeon for type-safe platform channels.
@@ -13,7 +13,7 @@ public class SwiftFlutterSmsPlugin: NSObject, FlutterPlugin, SmsHostApi, UINavig
public func sendSms(message: String, recipients: [String], completion: @escaping (Result<String, Error>) -> Void) {
#if targetEnvironment(simulator)
completion(.failure(FlutterError(
completion(.failure(PigeonError(
code: "message_not_sent",
message: "Cannot send message on this device!",
details: "Cannot send SMS and MMS on a Simulator. Test on a real device."
@@ -27,7 +27,7 @@ public class SwiftFlutterSmsPlugin: NSObject, FlutterPlugin, SmsHostApi, UINavig
controller.messageComposeDelegate = self
UIApplication.shared.keyWindow?.rootViewController?.present(controller, animated: true, completion: nil)
} else {
completion(.failure(FlutterError(
completion(.failure(PigeonError(
code: "device_not_capable",
message: "The current device is not capable of sending text messages.",
details: "A device may be unable to send messages if it does not support messaging or if it is not currently configured to send messages. This only applies to the ability to send text messages via iMessage, SMS, and MMS."
@@ -36,14 +36,14 @@ public class SwiftFlutterSmsPlugin: NSObject, FlutterPlugin, SmsHostApi, UINavig
#endif
}
public func canSendSms() -> Bool {
public func canSendSms(completion: @escaping (Result<Bool, Error>) -> Void) {
#if targetEnvironment(simulator)
return false
completion(.success(false))
#else
if (MFMessageComposeViewController.canSendText()) {
return true
completion(.success(true))
} else {
return false
completion(.success(false))
}
#endif
}
+2 -2
View File
@@ -1,6 +1,6 @@
name: flutter_sms
description: A Flutter plugin to send SMS and MMS on iOS and Android.
version: 3.0.0
version: 3.0.1
homepage: https://github.com/rodydavis/packages.dart
repository: https://github.com/rodydavis/packages.dart
maintainer: Rody Davis (@rodydavis)
@@ -21,7 +21,7 @@ dependencies:
dev_dependencies:
flutter_test:
sdk: flutter
pigeon: ^26.1.5
pigeon: ^26.1.7
flutter:
plugin:
+7
View File
@@ -0,0 +1,7 @@
load("@rules_flutter//flutter:defs.bzl", "dart_library")
dart_library(
name = "flutter_vibrate",
srcs = glob(["lib/**/*.dart"]),
visibility = ["//visibility:public"],
)
@@ -3,7 +3,13 @@ import UIKit
import AudioToolbox
public class SwiftVibratePlugin: NSObject, FlutterPlugin, VibrateApi {
private let isDevice = TARGET_OS_SIMULATOR == 0
private var isDevice: Bool {
#if targetEnvironment(simulator)
return false
#else
return true
#endif
}
public static func register(with registrar: FlutterPluginRegistrar) {
let messenger : FlutterBinaryMessenger = registrar.messenger()
-2
View File
@@ -1,6 +1,4 @@
#import <Flutter/Flutter.h>
#import <TargetConditionals.h>
#import <TargetConditionals.h>
@interface VibratePlugin : NSObject<FlutterPlugin>
@end
+1 -1
View File
@@ -15,7 +15,7 @@ dependencies:
dev_dependencies:
flutter_lints: ^6.0.0
pigeon: ^26.1.5
pigeon: ^26.1.7
flutter:
plugin:
+7
View File
@@ -0,0 +1,7 @@
load("@rules_flutter//flutter:defs.bzl", "dart_library")
dart_library(
name = "flutter_whatsnew",
srcs = glob(["lib/**/*.dart"]),
visibility = ["//visibility:public"],
)
@@ -1 +1 @@
{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"shared_preferences_foundation","path":"/Users/rodydavis/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.6/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"android":[{"name":"shared_preferences_android","path":"/Users/rodydavis/.pub-cache/hosted/pub.dev/shared_preferences_android-2.4.18/","native_build":true,"dependencies":[],"dev_dependency":false}],"macos":[{"name":"shared_preferences_foundation","path":"/Users/rodydavis/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.6/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"linux":[{"name":"path_provider_linux","path":"/Users/rodydavis/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_linux","path":"/Users/rodydavis/.pub-cache/hosted/pub.dev/shared_preferences_linux-2.4.1/","native_build":false,"dependencies":["path_provider_linux"],"dev_dependency":false}],"windows":[{"name":"path_provider_windows","path":"/Users/rodydavis/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_windows","path":"/Users/rodydavis/.pub-cache/hosted/pub.dev/shared_preferences_windows-2.4.1/","native_build":false,"dependencies":["path_provider_windows"],"dev_dependency":false}],"web":[{"name":"shared_preferences_web","path":"/Users/rodydavis/.pub-cache/hosted/pub.dev/shared_preferences_web-2.4.3/","dependencies":[],"dev_dependency":false}]},"dependencyGraph":[{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"shared_preferences","dependencies":["shared_preferences_android","shared_preferences_foundation","shared_preferences_linux","shared_preferences_web","shared_preferences_windows"]},{"name":"shared_preferences_android","dependencies":[]},{"name":"shared_preferences_foundation","dependencies":[]},{"name":"shared_preferences_linux","dependencies":["path_provider_linux"]},{"name":"shared_preferences_web","dependencies":[]},{"name":"shared_preferences_windows","dependencies":["path_provider_windows"]}],"date_created":"2026-01-15 17:05:21.433136","version":"3.38.5","swift_package_manager_enabled":{"ios":false,"macos":false}}
{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"shared_preferences_foundation","path":"/Users/rodydavis/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.6/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"android":[{"name":"shared_preferences_android","path":"/Users/rodydavis/.pub-cache/hosted/pub.dev/shared_preferences_android-2.4.20/","native_build":true,"dependencies":[],"dev_dependency":false}],"macos":[{"name":"shared_preferences_foundation","path":"/Users/rodydavis/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.6/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"linux":[{"name":"path_provider_linux","path":"/Users/rodydavis/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_linux","path":"/Users/rodydavis/.pub-cache/hosted/pub.dev/shared_preferences_linux-2.4.1/","native_build":false,"dependencies":["path_provider_linux"],"dev_dependency":false}],"windows":[{"name":"path_provider_windows","path":"/Users/rodydavis/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_windows","path":"/Users/rodydavis/.pub-cache/hosted/pub.dev/shared_preferences_windows-2.4.1/","native_build":false,"dependencies":["path_provider_windows"],"dev_dependency":false}],"web":[{"name":"shared_preferences_web","path":"/Users/rodydavis/.pub-cache/hosted/pub.dev/shared_preferences_web-2.4.3/","dependencies":[],"dev_dependency":false}]},"dependencyGraph":[{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"shared_preferences","dependencies":["shared_preferences_android","shared_preferences_foundation","shared_preferences_linux","shared_preferences_web","shared_preferences_windows"]},{"name":"shared_preferences_android","dependencies":[]},{"name":"shared_preferences_foundation","dependencies":[]},{"name":"shared_preferences_linux","dependencies":["path_provider_linux"]},{"name":"shared_preferences_web","dependencies":[]},{"name":"shared_preferences_windows","dependencies":["path_provider_windows"]}],"date_created":"2026-02-16 10:38:50.657587","version":"3.38.5","swift_package_manager_enabled":{"ios":false,"macos":false}}
+7
View File
@@ -0,0 +1,7 @@
load("@rules_flutter//flutter:defs.bzl", "dart_library")
dart_library(
name = "undo",
srcs = glob(["lib/**/*.dart"]),
visibility = ["//visibility:public"],
)
+2 -1
View File
@@ -11,4 +11,5 @@ environment:
dev_dependencies:
lints: ^6.0.0
test: ^1.24.9
flutter_test:
sdk: flutter
+1 -3
View File
@@ -1,6 +1,4 @@
import 'dart:async';
import 'package:test/test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:undo/undo.dart';
void main() {
+3 -179
View File
@@ -65,14 +65,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.0"
cli_config:
dependency: transitive
description:
name: cli_config
sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec
url: "https://pub.dev"
source: hosted
version: "0.2.0"
clock:
dependency: transitive
description:
@@ -105,14 +97,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.1.2"
coverage:
dependency: transitive
description:
name: coverage
sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d"
url: "https://pub.dev"
source: hosted
version: "1.15.0"
crypto:
dependency: transitive
description:
@@ -167,7 +151,7 @@ packages:
source: sdk
version: "0.0.0"
flutter_lints:
dependency: transitive
dependency: "direct dev"
description:
name: flutter_lints
sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
@@ -192,14 +176,6 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
frontend_server_client:
dependency: transitive
description:
name: frontend_server_client
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
url: "https://pub.dev"
source: hosted
version: "4.0.0"
glob:
dependency: transitive
description:
@@ -216,38 +192,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.2"
http_multi_server:
dependency: transitive
description:
name: http_multi_server
sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
url: "https://pub.dev"
source: hosted
version: "3.2.2"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.dev"
source: hosted
version: "4.1.2"
io:
dependency: transitive
description:
name: io
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
url: "https://pub.dev"
source: hosted
version: "1.0.5"
js:
dependency: transitive
description:
name: js
sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc"
url: "https://pub.dev"
source: hosted
version: "0.7.2"
leak_tracker:
dependency: transitive
description:
@@ -280,14 +224,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.0.0"
logging:
dependency: transitive
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.3.0"
markdown:
dependency: transitive
description:
@@ -320,22 +256,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.17.0"
mime:
dependency: transitive
description:
name: mime
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
node_preamble:
dependency: transitive
description:
name: node_preamble
sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db"
url: "https://pub.dev"
source: hosted
version: "2.0.2"
package_config:
dependency: transitive
description:
@@ -380,10 +300,10 @@ packages:
dependency: transitive
description:
name: pigeon
sha256: fcfee18cf18dae434a149879e6876214c1b57c8d2d5d0cc55a5ba0c85278b886
sha256: b12f739047654cd65338dc7e9c6c03cbd4a1de179364a45c9e8bbeccf23729c6
url: "https://pub.dev"
source: hosted
version: "26.1.5"
version: "26.1.7"
platform:
dependency: transitive
description:
@@ -400,14 +320,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.8"
pool:
dependency: transitive
description:
name: pool
sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d"
url: "https://pub.dev"
source: hosted
version: "1.5.2"
pub_semver:
dependency: transitive
description:
@@ -472,59 +384,11 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shelf:
dependency: transitive
description:
name: shelf
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
url: "https://pub.dev"
source: hosted
version: "1.4.2"
shelf_packages_handler:
dependency: transitive
description:
name: shelf_packages_handler
sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
shelf_static:
dependency: transitive
description:
name: shelf_static
sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3
url: "https://pub.dev"
source: hosted
version: "1.1.3"
shelf_web_socket:
dependency: transitive
description:
name: shelf_web_socket
sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
source_map_stack_trace:
dependency: transitive
description:
name: source_map_stack_trace
sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b
url: "https://pub.dev"
source: hosted
version: "2.1.2"
source_maps:
dependency: transitive
description:
name: source_maps
sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812"
url: "https://pub.dev"
source: hosted
version: "0.10.13"
source_span:
dependency: transitive
description:
@@ -565,14 +429,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.2.2"
test:
dependency: transitive
description:
name: test
sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7"
url: "https://pub.dev"
source: hosted
version: "1.26.3"
test_api:
dependency: transitive
description:
@@ -581,14 +437,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.7.7"
test_core:
dependency: transitive
description:
name: test_core
sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0"
url: "https://pub.dev"
source: hosted
version: "0.6.12"
typed_data:
dependency: transitive
description:
@@ -693,30 +541,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.1"
web_socket:
dependency: transitive
description:
name: web_socket
sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
web_socket_channel:
dependency: transitive
description:
name: web_socket_channel
sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8
url: "https://pub.dev"
source: hosted
version: "3.0.3"
webkit_inspection_protocol:
dependency: transitive
description:
name: webkit_inspection_protocol
sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
xdg_directories:
dependency: transitive
description:
+3
View File
@@ -4,6 +4,9 @@ publish_to: none
environment:
sdk: ^3.5.0
dev_dependencies:
flutter_lints: ^6.0.0
workspace:
- packages/flutter_vibrate
- packages/flutter_sms
+6
View File
@@ -0,0 +1,6 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended"
]
}