chore(web_file_system): sync changes from open_xml including idb_block_store, getAsBlob, and tests
This commit is contained in:
@@ -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,18 +153,23 @@ 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: () {
|
||||
setState(() {
|
||||
_currentPath = _fs.path.dirname(_currentPath);
|
||||
});
|
||||
_refreshFiles();
|
||||
},
|
||||
child: const Text('Go Up')),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_currentPath = _fs.path.dirname(_currentPath);
|
||||
});
|
||||
_refreshFiles();
|
||||
},
|
||||
child: const Text('Go Up'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(),
|
||||
@@ -181,13 +186,14 @@ class _FileSystemDemoState extends State<FileSystemDemo> {
|
||||
),
|
||||
title: Text(entity.basename),
|
||||
subtitle: FutureBuilder<int>(
|
||||
future: _getSize(entity),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return Text(_formatBytes(snapshot.data!));
|
||||
}
|
||||
return const Text('Loading...');
|
||||
}),
|
||||
future: _getSize(entity),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return Text(_formatBytes(snapshot.data!));
|
||||
}
|
||||
return const Text('Loading...');
|
||||
},
|
||||
),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.delete, color: Colors.red),
|
||||
onPressed: () => _delete(entity),
|
||||
@@ -201,17 +207,20 @@ class _FileSystemDemoState extends State<FileSystemDemo> {
|
||||
} else if (entity is File) {
|
||||
entity.readAsString().then((content) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(entity.basename),
|
||||
content: SingleChildScrollView(
|
||||
child: Text(content)),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Close'))
|
||||
],
|
||||
));
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(entity.basename),
|
||||
content: SingleChildScrollView(
|
||||
child: Text(content),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
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],
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontFamily: 'monospace',
|
||||
)),
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -95,16 +91,13 @@ class IdbInodeService {
|
||||
static const String _storeName = 'inodes';
|
||||
|
||||
web.IDBDatabase? _db;
|
||||
Future<void>? _initFuture;
|
||||
final Completer<void> _initCompleter = Completer<void>();
|
||||
static const String rootId = '00000000-0000-0000-0000-000000000000';
|
||||
|
||||
Future<void> _ensureReady() {
|
||||
if (_db != null) return Future.value();
|
||||
return _initFuture ??= _init();
|
||||
}
|
||||
Future<void> _ensureReady() async {
|
||||
if (_db != null) return;
|
||||
if (_initCompleter.isCompleted) return _initCompleter.future;
|
||||
|
||||
Future<void> _init() async {
|
||||
final completer = Completer<void>();
|
||||
final request = web.window.indexedDB.open(_dbName, _version);
|
||||
|
||||
request.onupgradeneeded = (web.IDBVersionChangeEvent event) {
|
||||
@@ -116,23 +109,35 @@ 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;
|
||||
|
||||
final completer = Completer<void>();
|
||||
|
||||
request.onsuccess = (web.Event event) {
|
||||
_db = (event.target as web.IDBOpenDBRequest).result as web.IDBDatabase;
|
||||
_ensureRootExists().then((_) {
|
||||
completer.complete();
|
||||
}).catchError((e) {
|
||||
completer.completeError(e);
|
||||
});
|
||||
_ensureRootExists()
|
||||
.then((_) {
|
||||
if (!_initCompleter.isCompleted) completer.complete();
|
||||
})
|
||||
.catchError((e) {
|
||||
if (!_initCompleter.isCompleted) completer.completeError(e);
|
||||
});
|
||||
}.toJS;
|
||||
|
||||
request.onerror = (web.Event event) {
|
||||
completer.completeError(Exception('Failed to open IDB'));
|
||||
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(
|
||||
id: rootId,
|
||||
parentId: 'null',
|
||||
name: '',
|
||||
nodeType: 1,
|
||||
modified: DateTime.now().millisecondsSinceEpoch,
|
||||
));
|
||||
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
|
||||
@@ -26,24 +23,19 @@ class OpfsBlockStore {
|
||||
.toDart;
|
||||
}
|
||||
|
||||
Future<(String, int)> writeBlob(Stream<List<int>> stream) async {
|
||||
Future<String> writeBlob(Stream<List<int>> stream) async {
|
||||
await _ensureReady();
|
||||
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;
|
||||
int totalBytes = 0;
|
||||
|
||||
try {
|
||||
await for (final chunk in stream) {
|
||||
final uint8 = Uint8List.fromList(chunk);
|
||||
totalBytes += uint8.length;
|
||||
await writable.write(uint8.toJS).toDart;
|
||||
}
|
||||
await writable.close().toDart;
|
||||
@@ -52,20 +44,17 @@ class OpfsBlockStore {
|
||||
await writable.abort().toDart;
|
||||
await _blocksDir!.removeEntry(blockId).toDart;
|
||||
} catch (_) {}
|
||||
print('OpfsBlockStore: writeBlob failed for $blockId: $e');
|
||||
rethrow;
|
||||
}
|
||||
|
||||
return (blockId, totalBytes);
|
||||
return blockId;
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -80,6 +69,7 @@ class OpfsBlockStore {
|
||||
yield chunk.toDart;
|
||||
}
|
||||
} catch (e) {
|
||||
print('OpfsBlockStore: readBlob failed for $blockId: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
@@ -92,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,234 +0,0 @@
|
||||
import 'dart:js_interop';
|
||||
import 'dart:typed_data';
|
||||
|
||||
@JS('eval')
|
||||
external JSAny? _jsEval(String code);
|
||||
|
||||
@JS('globalThis.registerVFSWorkerProxy')
|
||||
external void _registerVFSWorkerProxy(JSAny worker, JSAny asyncFS);
|
||||
|
||||
@JS('globalThis.initVFSSyncWorker')
|
||||
external void _initVFSSyncWorker();
|
||||
|
||||
@JS('globalThis.sendVFSSyncRequest')
|
||||
external JSUint8Array _sendVFSSyncRequest(int cmd, JSUint8Array requestBytes);
|
||||
|
||||
@JS('globalThis.isVFSSyncWorkerInitialized')
|
||||
external bool? get _isVFSSyncWorkerInitialized;
|
||||
|
||||
@JS('globalThis.SharedArrayBuffer')
|
||||
external JSAny? get _sharedArrayBufferClass;
|
||||
|
||||
class SyncRpcHelper {
|
||||
static bool get isSharedArrayBufferSupported {
|
||||
try {
|
||||
return _sharedArrayBufferClass != null;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static bool get isWorker {
|
||||
// In a worker, globalThis.document is undefined and globalThis.importScripts is defined
|
||||
try {
|
||||
final isWorkerResult = _jsEval("typeof importScripts !== 'undefined'");
|
||||
return (isWorkerResult as JSBoolean).toDart;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static void injectHelperScripts() {
|
||||
_jsEval('''
|
||||
if (typeof globalThis.registerVFSWorkerProxy === 'undefined') {
|
||||
globalThis.registerVFSWorkerProxy = function(worker, asyncFS) {
|
||||
let sab;
|
||||
let statusArray;
|
||||
let payloadArray;
|
||||
|
||||
worker.addEventListener('message', async function(e) {
|
||||
if (!e.data) return;
|
||||
if (e.data.type === 'INIT_SYNC_VFS') {
|
||||
sab = e.data.buffer;
|
||||
statusArray = new Int32Array(sab);
|
||||
payloadArray = new Uint8Array(sab);
|
||||
return;
|
||||
}
|
||||
if (e.data.type === 'SYNC_REQ') {
|
||||
const cmd = statusArray[1];
|
||||
const reqLen = statusArray[2];
|
||||
const decoder = new TextDecoder();
|
||||
const reqBytes = payloadArray.subarray(64, 64 + reqLen);
|
||||
|
||||
try {
|
||||
let result;
|
||||
if (cmd === 1) { // exists
|
||||
const path = decoder.decode(reqBytes);
|
||||
const type = await asyncFS.type(path);
|
||||
result = type.toString() !== 'FileSystemEntityType.notFound';
|
||||
payloadArray[64] = result ? 1 : 0;
|
||||
statusArray[3] = 1;
|
||||
} else if (cmd === 2) { // type
|
||||
const followLinks = reqBytes[0] === 1;
|
||||
const path = decoder.decode(reqBytes.subarray(1));
|
||||
const type = await asyncFS.type(path, { followLinks });
|
||||
const typeStr = type.toString().split('.').pop();
|
||||
const typeBytes = new TextEncoder().encode(typeStr);
|
||||
payloadArray.set(typeBytes, 64);
|
||||
statusArray[3] = typeBytes.length;
|
||||
} else if (cmd === 3) { // readBytes
|
||||
const path = decoder.decode(reqBytes);
|
||||
const bytes = await asyncFS.file(path).readAsBytes();
|
||||
payloadArray.set(bytes, 64);
|
||||
statusArray[3] = bytes.length;
|
||||
} else if (cmd === 4) { // writeBytes
|
||||
const view = new DataView(reqBytes.buffer, reqBytes.byteOffset, reqBytes.byteLength);
|
||||
const pathLen = view.getUint32(0, true);
|
||||
const path = decoder.decode(reqBytes.subarray(4, 4 + pathLen));
|
||||
const content = reqBytes.subarray(4 + pathLen);
|
||||
await asyncFS.file(path).writeAsBytes(content);
|
||||
statusArray[3] = 0;
|
||||
} else if (cmd === 5) { // createDir
|
||||
const path = decoder.decode(reqBytes);
|
||||
await asyncFS.directory(path).create(recursive: true);
|
||||
statusArray[3] = 0;
|
||||
} else if (cmd === 6) { // delete
|
||||
const path = decoder.decode(reqBytes);
|
||||
await asyncFS.file(path).delete(recursive: true);
|
||||
statusArray[3] = 0;
|
||||
} else if (cmd === 7) { // createLink
|
||||
const view = new DataView(reqBytes.buffer, reqBytes.byteOffset, reqBytes.byteLength);
|
||||
const pathLen = view.getUint32(0, true);
|
||||
const path = decoder.decode(reqBytes.subarray(4, 4 + pathLen));
|
||||
const target = decoder.decode(reqBytes.subarray(4 + pathLen));
|
||||
await asyncFS.link(path).create(target);
|
||||
statusArray[3] = 0;
|
||||
} else if (cmd === 8) { // readLink
|
||||
const path = decoder.decode(reqBytes);
|
||||
const target = await asyncFS.link(path).target();
|
||||
const targetBytes = new TextEncoder().encode(target);
|
||||
payloadArray.set(targetBytes, 64);
|
||||
statusArray[3] = targetBytes.length;
|
||||
} else if (cmd === 9) { // stat
|
||||
const path = decoder.decode(reqBytes);
|
||||
const stat = await asyncFS.stat(path);
|
||||
const statData = {
|
||||
type: stat.type.toString().split('.').pop(),
|
||||
size: stat.size,
|
||||
modified: stat.modified.millisecondsSinceEpoch
|
||||
};
|
||||
const statBytes = new TextEncoder().encode(JSON.stringify(statData));
|
||||
payloadArray.set(statBytes, 64);
|
||||
statusArray[3] = statBytes.length;
|
||||
} else if (cmd === 10) { // list
|
||||
const path = decoder.decode(reqBytes);
|
||||
const list = await asyncFS.directory(path).list(recursive: false, followLinks: false).toList();
|
||||
const entities = list.map(e => ({
|
||||
path: e.path,
|
||||
type: e.runtimeType.toString().toLowerCase().replace('impl', '').replace('web', '')
|
||||
}));
|
||||
const listBytes = new TextEncoder().encode(JSON.stringify(entities));
|
||||
payloadArray.set(listBytes, 64);
|
||||
statusArray[3] = listBytes.length;
|
||||
} else if (cmd === 11) { // resolveSymbolicLinks
|
||||
const path = decoder.decode(reqBytes);
|
||||
const resolved = await asyncFS.resolveSymbolicLinks(path);
|
||||
const resolvedBytes = new TextEncoder().encode(resolved);
|
||||
payloadArray.set(resolvedBytes, 64);
|
||||
statusArray[3] = resolvedBytes.length;
|
||||
} else if (cmd === 12) { // rename
|
||||
const view = new DataView(reqBytes.buffer, reqBytes.byteOffset, reqBytes.byteLength);
|
||||
const pathLen = view.getUint32(0, true);
|
||||
const path = decoder.decode(reqBytes.subarray(4, 4 + pathLen));
|
||||
const newPath = decoder.decode(reqBytes.subarray(4 + pathLen));
|
||||
|
||||
const inode = await asyncFS.resolvepath(path);
|
||||
const newParentDir = asyncFS.path.dirname(newPath);
|
||||
const newName = asyncFS.path.basename(newPath);
|
||||
const newParentInode = await asyncFS.resolvepath(newParentDir);
|
||||
|
||||
inode.parentId = newParentInode.id;
|
||||
inode.name = newName;
|
||||
inode.modified = Date.now();
|
||||
await asyncFS.idb.updateInode(inode);
|
||||
statusArray[3] = 0;
|
||||
} else if (cmd === 13) { // updateLink
|
||||
const view = new DataView(reqBytes.buffer, reqBytes.byteOffset, reqBytes.byteLength);
|
||||
const pathLen = view.getUint32(0, true);
|
||||
const path = decoder.decode(reqBytes.subarray(4, 4 + pathLen));
|
||||
const target = decoder.decode(reqBytes.subarray(4 + pathLen));
|
||||
await asyncFS.link(path).update(target);
|
||||
statusArray[3] = 0;
|
||||
}
|
||||
|
||||
Atomics.store(statusArray, 0, 2); // completed
|
||||
} catch (err) {
|
||||
console.error("VFS Proxy Error:", err);
|
||||
const errBytes = new TextEncoder().encode(err.toString());
|
||||
payloadArray.set(errBytes, 64);
|
||||
statusArray[3] = errBytes.length;
|
||||
Atomics.store(statusArray, 0, 3); // error
|
||||
}
|
||||
Atomics.notify(statusArray, 0);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof globalThis.initVFSSyncWorker === 'undefined') {
|
||||
globalThis.isVFSSyncWorkerInitialized = false;
|
||||
globalThis.initVFSSyncWorker = function() {
|
||||
if (globalThis.isVFSSyncWorkerInitialized) return;
|
||||
const sab = new SharedArrayBuffer(1024 * 1024 * 10); // 10MB
|
||||
const statusArray = new Int32Array(sab);
|
||||
const payloadArray = new Uint8Array(sab);
|
||||
|
||||
globalThis.postMessage({ type: 'INIT_SYNC_VFS', buffer: sab });
|
||||
|
||||
globalThis.sendVFSSyncRequest = function(cmd, requestBytes) {
|
||||
while (Atomics.load(statusArray, 0) !== 0) {
|
||||
// Idle wait
|
||||
}
|
||||
statusArray[1] = cmd;
|
||||
statusArray[2] = requestBytes.length;
|
||||
payloadArray.set(requestBytes, 64);
|
||||
|
||||
Atomics.store(statusArray, 0, 1);
|
||||
globalThis.postMessage({ type: 'SYNC_REQ' });
|
||||
|
||||
Atomics.wait(statusArray, 0, 1);
|
||||
|
||||
const status = Atomics.load(statusArray, 0);
|
||||
const respLen = statusArray[3];
|
||||
const respBytes = payloadArray.slice(64, 64 + respLen);
|
||||
|
||||
Atomics.store(statusArray, 0, 0);
|
||||
|
||||
if (status === 3) {
|
||||
throw new Error(new TextDecoder().decode(respBytes));
|
||||
}
|
||||
return respBytes;
|
||||
};
|
||||
globalThis.isVFSSyncWorkerInitialized = true;
|
||||
};
|
||||
}
|
||||
''');
|
||||
}
|
||||
|
||||
static void registerWorkerProxy(JSAny worker, JSAny asyncFS) {
|
||||
injectHelperScripts();
|
||||
_registerVFSWorkerProxy(worker, asyncFS);
|
||||
}
|
||||
|
||||
static void initSyncWorker() {
|
||||
injectHelperScripts();
|
||||
_initVFSSyncWorker();
|
||||
}
|
||||
|
||||
static Uint8List sendSyncRequest(int cmd, Uint8List requestBytes) {
|
||||
if (_isVFSSyncWorkerInitialized != true) {
|
||||
initSyncWorker();
|
||||
}
|
||||
return _sendVFSSyncRequest(cmd, requestBytes.toJS).toDart;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,8 @@
|
||||
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';
|
||||
import 'web_file.dart';
|
||||
import 'web_link.dart';
|
||||
|
||||
class WebDirectory extends FileSystemEntity implements Directory {
|
||||
final WebFileSystem _fs;
|
||||
@@ -36,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(
|
||||
id: _fs.uuid.v4(),
|
||||
parentId: parentInode.id,
|
||||
name: name,
|
||||
nodeType: 1, // Directory
|
||||
modified: DateTime.now().millisecondsSinceEpoch,
|
||||
));
|
||||
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;
|
||||
}
|
||||
@@ -53,37 +56,24 @@ 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(
|
||||
id: _fs.uuid.v4(),
|
||||
parentId: parentVal.id,
|
||||
name: _fs.path.basename(p),
|
||||
nodeType: 1,
|
||||
modified: DateTime.now().millisecondsSinceEpoch));
|
||||
try {
|
||||
await _fs.idb.createInode(
|
||||
Inode(
|
||||
id: _fs.uuid.v4(),
|
||||
parentId: parentVal.id,
|
||||
name: _fs.path.basename(p),
|
||||
nodeType: 1,
|
||||
modified: DateTime.now().millisecondsSinceEpoch,
|
||||
),
|
||||
);
|
||||
} catch (_) {
|
||||
if (await _fs.type(p) == FileSystemEntityType.notFound) rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void createSync({bool recursive = false}) {
|
||||
if (existsSync()) return;
|
||||
|
||||
final parentPath = _fs.path.dirname(path);
|
||||
final parentType = _fs.typeSync(parentPath);
|
||||
if (parentType == FileSystemEntityType.notFound) {
|
||||
if (recursive) {
|
||||
_fs.directory(parentPath).createSync(recursive: true);
|
||||
} else {
|
||||
throw FileSystemException(
|
||||
'Cannot create directory, path = \'$path\' (OS Error: No such file or directory, errno = 2)',
|
||||
path,
|
||||
);
|
||||
}
|
||||
} else if (parentType != FileSystemEntityType.directory) {
|
||||
throw FileSystemException(
|
||||
'Cannot create directory, path = \'$path\' (OS Error: Not a directory, errno = 20)',
|
||||
path,
|
||||
);
|
||||
}
|
||||
|
||||
_fs.makeSyncCall(5, utf8.encode(path));
|
||||
throw UnsupportedError('Sync create not supported');
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -92,8 +82,15 @@ class WebDirectory extends FileSystemEntity implements Directory {
|
||||
final tempDir = _fs.path.join(path, name);
|
||||
// Ensure path exists
|
||||
if (!await exists()) {
|
||||
throw FileSystemException(
|
||||
'Directory does not exist', path, const OSError('ENOENT', 2));
|
||||
if (path == '/tmp') {
|
||||
await create();
|
||||
} else {
|
||||
throw FileSystemException(
|
||||
'Directory does not exist',
|
||||
path,
|
||||
const OSError('ENOENT', 2),
|
||||
);
|
||||
}
|
||||
}
|
||||
final dir = WebDirectory(_fs, tempDir);
|
||||
await dir.create();
|
||||
@@ -102,15 +99,7 @@ class WebDirectory extends FileSystemEntity implements Directory {
|
||||
|
||||
@override
|
||||
Directory createTempSync([String? prefix]) {
|
||||
if (!existsSync()) {
|
||||
throw FileSystemException(
|
||||
'Directory does not exist', path, const OSError('ENOENT', 2));
|
||||
}
|
||||
final name = (prefix ?? 'temp') + _fs.uuid.v4();
|
||||
final tempDir = _fs.path.join(path, name);
|
||||
final dir = WebDirectory(_fs, tempDir);
|
||||
dir.createSync();
|
||||
return dir;
|
||||
throw UnsupportedError('Sync not supported');
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -120,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) {
|
||||
@@ -139,34 +131,8 @@ class WebDirectory extends FileSystemEntity implements Directory {
|
||||
}
|
||||
|
||||
@override
|
||||
void deleteSync({bool recursive = false}) {
|
||||
final type = _fs.typeSync(path, followLinks: false);
|
||||
if (type == FileSystemEntityType.notFound) {
|
||||
throw FileSystemException(
|
||||
'Cannot delete directory, path = \'$path\' (OS Error: No such file or directory, errno = 2)',
|
||||
path,
|
||||
);
|
||||
}
|
||||
if (type != FileSystemEntityType.directory) {
|
||||
throw FileSystemException(
|
||||
'Cannot delete directory, path = \'$path\' (OS Error: Not a directory, errno = 20)',
|
||||
path,
|
||||
);
|
||||
}
|
||||
|
||||
if (!recursive) {
|
||||
final children = listSync(recursive: false, followLinks: false);
|
||||
if (children.isNotEmpty) {
|
||||
throw FileSystemException(
|
||||
'Directory not empty',
|
||||
path,
|
||||
const OSError('ENOTEMPTY', 39),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_fs.makeSyncCall(6, utf8.encode(path));
|
||||
}
|
||||
void deleteSync({bool recursive = false}) =>
|
||||
throw UnsupportedError('Sync not supported');
|
||||
|
||||
@override
|
||||
Future<bool> exists() async {
|
||||
@@ -179,16 +145,19 @@ class WebDirectory extends FileSystemEntity implements Directory {
|
||||
}
|
||||
|
||||
@override
|
||||
bool existsSync() {
|
||||
return _fs.typeSync(path, followLinks: false) == FileSystemEntityType.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);
|
||||
@@ -202,26 +171,6 @@ class WebDirectory extends FileSystemEntity implements Directory {
|
||||
if (recursive) {
|
||||
yield* dir.list(recursive: true, followLinks: followLinks);
|
||||
}
|
||||
} else if (child.nodeType == 2) {
|
||||
if (!followLinks) {
|
||||
yield WebLink(_fs, childPath);
|
||||
} else {
|
||||
try {
|
||||
final resolved =
|
||||
await _fs.resolvepath(childPath, followLinks: true);
|
||||
if (resolved.nodeType == 1) {
|
||||
final dir = WebDirectory(_fs, childPath);
|
||||
yield dir;
|
||||
if (recursive) {
|
||||
yield* dir.list(recursive: true, followLinks: true);
|
||||
}
|
||||
} else {
|
||||
yield WebFile(_fs, childPath);
|
||||
}
|
||||
} catch (_) {
|
||||
yield WebLink(_fs, childPath);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
yield WebFile(_fs, childPath);
|
||||
}
|
||||
@@ -229,54 +178,11 @@ class WebDirectory extends FileSystemEntity implements Directory {
|
||||
}
|
||||
|
||||
@override
|
||||
List<FileSystemEntity> listSync(
|
||||
{bool recursive = false, bool followLinks = true}) {
|
||||
if (!existsSync()) {
|
||||
throw FileSystemException(
|
||||
'Directory not found', path, const OSError('ENOENT', 2));
|
||||
}
|
||||
|
||||
final respBytes = _fs.makeSyncCall(10, utf8.encode(path));
|
||||
final listJson = json.decode(utf8.decode(respBytes)) as List<dynamic>;
|
||||
|
||||
final List<FileSystemEntity> results = [];
|
||||
for (final item in listJson) {
|
||||
final itemMap = item as Map<String, dynamic>;
|
||||
final childPath = itemMap['path'] as String;
|
||||
final typeStr = itemMap['type'] as String;
|
||||
|
||||
if (typeStr == 'directory') {
|
||||
final dir = WebDirectory(_fs, childPath);
|
||||
results.add(dir);
|
||||
if (recursive) {
|
||||
results.addAll(dir.listSync(recursive: true, followLinks: followLinks));
|
||||
}
|
||||
} else if (typeStr == 'link') {
|
||||
if (!followLinks) {
|
||||
results.add(WebLink(_fs, childPath));
|
||||
} else {
|
||||
try {
|
||||
final resolvedType = _fs.typeSync(childPath, followLinks: true);
|
||||
if (resolvedType == FileSystemEntityType.directory) {
|
||||
final dir = WebDirectory(_fs, childPath);
|
||||
results.add(dir);
|
||||
if (recursive) {
|
||||
results.addAll(dir.listSync(recursive: true, followLinks: true));
|
||||
}
|
||||
} else if (resolvedType == FileSystemEntityType.notFound) {
|
||||
results.add(WebLink(_fs, childPath));
|
||||
} else {
|
||||
results.add(WebFile(_fs, childPath));
|
||||
}
|
||||
} catch (_) {
|
||||
results.add(WebLink(_fs, childPath));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
results.add(WebFile(_fs, childPath));
|
||||
}
|
||||
}
|
||||
return results;
|
||||
List<FileSystemEntity> listSync({
|
||||
bool recursive = false,
|
||||
bool followLinks = true,
|
||||
}) {
|
||||
throw UnsupportedError('Sync list not supported');
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -288,46 +194,22 @@ class WebDirectory extends FileSystemEntity implements Directory {
|
||||
final newParentInode = await _fs.resolvepath(newParentDir);
|
||||
|
||||
final updated = Inode(
|
||||
id: inode.id,
|
||||
parentId: newParentInode.id,
|
||||
name: newName,
|
||||
nodeType: inode.nodeType,
|
||||
blobId: inode.blobId,
|
||||
size: inode.size,
|
||||
modified: DateTime.now().millisecondsSinceEpoch);
|
||||
id: inode.id,
|
||||
parentId: newParentInode.id,
|
||||
name: newName,
|
||||
nodeType: inode.nodeType,
|
||||
blobId: inode.blobId,
|
||||
size: inode.size,
|
||||
modified: DateTime.now().millisecondsSinceEpoch,
|
||||
);
|
||||
|
||||
await _fs.idb.updateInode(updated);
|
||||
return WebDirectory(_fs, newPath);
|
||||
}
|
||||
|
||||
@override
|
||||
Directory renameSync(String newPath) {
|
||||
final type = _fs.typeSync(path, followLinks: false);
|
||||
if (type == FileSystemEntityType.notFound) {
|
||||
throw FileSystemException(
|
||||
'Cannot rename directory, path = \'$path\' (OS Error: No such file or directory, errno = 2)',
|
||||
path,
|
||||
);
|
||||
}
|
||||
final newParentDir = _fs.path.dirname(newPath);
|
||||
if (_fs.typeSync(newParentDir) != FileSystemEntityType.directory) {
|
||||
throw FileSystemException(
|
||||
'Cannot rename directory, path = \'$path\' (OS Error: No such file or directory, errno = 2)',
|
||||
path,
|
||||
);
|
||||
}
|
||||
|
||||
final pathBytes = utf8.encode(path);
|
||||
final newPathBytes = utf8.encode(newPath);
|
||||
final request = Uint8List(4 + pathBytes.length + newPathBytes.length);
|
||||
final bd = ByteData.sublistView(request);
|
||||
bd.setUint32(0, pathBytes.length, Endian.little);
|
||||
request.setRange(4, 4 + pathBytes.length, pathBytes);
|
||||
request.setRange(4 + pathBytes.length, request.length, newPathBytes);
|
||||
|
||||
_fs.makeSyncCall(12, request);
|
||||
return WebDirectory(_fs, newPath);
|
||||
}
|
||||
Directory renameSync(String newPath) =>
|
||||
throw UnsupportedError('Sync not supported');
|
||||
|
||||
@override
|
||||
String get basename => _fs.path.basename(path);
|
||||
@@ -348,17 +230,20 @@ class WebDirectory extends FileSystemEntity implements Directory {
|
||||
Future<FileStat> stat() => _fs.stat(path);
|
||||
|
||||
@override
|
||||
FileStat statSync() => _fs.statSync(path);
|
||||
FileStat statSync() => throw UnsupportedError('Sync not supported');
|
||||
|
||||
@override
|
||||
Future<String> resolveSymbolicLinks() => _fs.resolveSymbolicLinks(path);
|
||||
Future<String> resolveSymbolicLinks() async => path;
|
||||
|
||||
@override
|
||||
String resolveSymbolicLinksSync() => _fs.resolveSymbolicLinksSync(path);
|
||||
String resolveSymbolicLinksSync() =>
|
||||
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 {
|
||||
@@ -52,65 +52,28 @@ class WebFile extends FileSystemEntity implements File {
|
||||
}
|
||||
|
||||
@override
|
||||
void createSync({bool recursive = false, bool exclusive = false}) {
|
||||
if (existsSync()) {
|
||||
if (exclusive) {
|
||||
throw FileSystemException(
|
||||
'File already exists',
|
||||
path,
|
||||
const OSError('EEXIST', 17),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final parentPath = _fs.path.dirname(path);
|
||||
final parentType = _fs.typeSync(parentPath);
|
||||
if (parentType == FileSystemEntityType.notFound) {
|
||||
if (recursive) {
|
||||
_fs.directory(parentPath).createSync(recursive: true);
|
||||
} else {
|
||||
throw FileSystemException(
|
||||
'Cannot create file, path = \'$path\' (OS Error: No such file or directory, errno = 2)',
|
||||
path,
|
||||
);
|
||||
}
|
||||
} else if (parentType != FileSystemEntityType.directory) {
|
||||
throw FileSystemException(
|
||||
'Cannot create file, path = \'$path\' (OS Error: Not a directory, errno = 20)',
|
||||
path,
|
||||
);
|
||||
}
|
||||
|
||||
writeAsBytesSync([]);
|
||||
}
|
||||
void createSync({bool recursive = false, bool exclusive = false}) =>
|
||||
throw UnsupportedError('Sync not supported');
|
||||
|
||||
@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
|
||||
File copySync(String newPath) {
|
||||
final bytes = readAsBytesSync();
|
||||
_fs.file(newPath).writeAsBytesSync(bytes);
|
||||
return WebFile(_fs, newPath);
|
||||
}
|
||||
File copySync(String newPath) => throw UnsupportedError('Sync not supported');
|
||||
|
||||
@override
|
||||
Future<int> length() async {
|
||||
@@ -119,7 +82,7 @@ class WebFile extends FileSystemEntity implements File {
|
||||
}
|
||||
|
||||
@override
|
||||
int lengthSync() => statSync().size;
|
||||
int lengthSync() => throw UnsupportedError('Sync not supported');
|
||||
|
||||
@override
|
||||
Future<DateTime> lastModified() async {
|
||||
@@ -128,7 +91,7 @@ class WebFile extends FileSystemEntity implements File {
|
||||
}
|
||||
|
||||
@override
|
||||
DateTime lastModifiedSync() => statSync().modified;
|
||||
DateTime lastModifiedSync() => throw UnsupportedError('Sync not supported');
|
||||
|
||||
@override
|
||||
Future<DateTime> lastAccessed() async {
|
||||
@@ -136,7 +99,7 @@ class WebFile extends FileSystemEntity implements File {
|
||||
}
|
||||
|
||||
@override
|
||||
DateTime lastAccessedSync() => statSync().accessed;
|
||||
DateTime lastAccessedSync() => throw UnsupportedError('Sync not supported');
|
||||
|
||||
@override
|
||||
Future<dynamic> setLastAccessed(DateTime time) async {}
|
||||
@@ -183,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 {
|
||||
@@ -191,15 +171,8 @@ class WebFile extends FileSystemEntity implements File {
|
||||
if (mode == FileMode.append) {
|
||||
throw UnsupportedError('Append not yet optimized');
|
||||
}
|
||||
} on FileSystemException catch (_) {
|
||||
final parentPath = _fs.path.dirname(path);
|
||||
if (await _fs.type(parentPath) != FileSystemEntityType.directory) {
|
||||
throw FileSystemException(
|
||||
'Cannot open file, path = \'$path\' (OS Error: No such file or directory, errno = 2)',
|
||||
path,
|
||||
);
|
||||
}
|
||||
await create(recursive: false);
|
||||
} catch (_) {
|
||||
await create(recursive: true);
|
||||
inode = await _fs.resolvepath(path);
|
||||
}
|
||||
|
||||
@@ -212,6 +185,7 @@ class WebFile extends FileSystemEntity implements File {
|
||||
blobId: newBlobId,
|
||||
size: bytes.length,
|
||||
modified: DateTime.now().millisecondsSinceEpoch,
|
||||
storageType: usedStorageType,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -224,27 +198,7 @@ class WebFile extends FileSystemEntity implements File {
|
||||
FileMode mode = FileMode.write,
|
||||
bool flush = false,
|
||||
}) {
|
||||
if (mode == FileMode.append) {
|
||||
throw UnsupportedError('Append not yet optimized');
|
||||
}
|
||||
|
||||
final parentPath = _fs.path.dirname(path);
|
||||
if (_fs.typeSync(parentPath) != FileSystemEntityType.directory) {
|
||||
throw FileSystemException(
|
||||
'Cannot open file, path = \'$path\' (OS Error: No such file or directory, errno = 2)',
|
||||
path,
|
||||
);
|
||||
}
|
||||
|
||||
final pathBytes = utf8.encode(path);
|
||||
final pathLen = pathBytes.length;
|
||||
final request = Uint8List(4 + pathLen + bytes.length);
|
||||
final bd = ByteData.sublistView(request);
|
||||
bd.setUint32(0, pathLen, Endian.little);
|
||||
request.setRange(4, 4 + pathLen, pathBytes);
|
||||
request.setRange(4 + pathLen, request.length, bytes);
|
||||
|
||||
_fs.makeSyncCall(4, request);
|
||||
throw UnsupportedError('Sync not supported');
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -264,7 +218,7 @@ class WebFile extends FileSystemEntity implements File {
|
||||
Encoding encoding = utf8,
|
||||
bool flush = false,
|
||||
}) {
|
||||
writeAsBytesSync(encoding.encode(contents), mode: mode, flush: flush);
|
||||
throw UnsupportedError('Sync not supported');
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -272,7 +226,37 @@ class WebFile extends FileSystemEntity implements File {
|
||||
final inode = await _fs.resolvepath(path);
|
||||
if (inode.blobId == null) return;
|
||||
|
||||
yield* _fs.opfs.readBlob(inode.blobId!);
|
||||
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
|
||||
@@ -281,9 +265,16 @@ class WebFile extends FileSystemEntity implements File {
|
||||
|
||||
// Start background write but keep future to await in close()
|
||||
final writeFuture = _handleWrite(controller.stream, encoding, mode);
|
||||
writeFuture.catchError((Object _) {});
|
||||
|
||||
return _WebIOSink(controller, writeFuture, encoding);
|
||||
final sink = _WebIOSink(
|
||||
controller,
|
||||
encoding,
|
||||
onDone: () async {
|
||||
await writeFuture;
|
||||
},
|
||||
);
|
||||
|
||||
return sink;
|
||||
}
|
||||
|
||||
Future<void> _handleWrite(
|
||||
@@ -292,22 +283,23 @@ class WebFile extends FileSystemEntity implements File {
|
||||
FileMode mode,
|
||||
) async {
|
||||
try {
|
||||
final parentPath = _fs.path.dirname(path);
|
||||
if (await _fs.type(parentPath) != FileSystemEntityType.directory) {
|
||||
throw FileSystemException(
|
||||
'Cannot open file, path = \'$path\' (OS Error: No such file or directory, errno = 2)',
|
||||
path,
|
||||
);
|
||||
}
|
||||
String newId;
|
||||
int usedStorageType = 0; // 0 = OPFS, 1 = IDB
|
||||
|
||||
final (newId, size) = await _fs.opfs.writeBlob(stream);
|
||||
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 {
|
||||
inode = await _fs.resolvepath(path);
|
||||
} catch (_) {
|
||||
// Create if missing
|
||||
await create(recursive: false);
|
||||
await create(recursive: true);
|
||||
inode = await _fs.resolvepath(path);
|
||||
}
|
||||
|
||||
@@ -318,8 +310,10 @@ class WebFile extends FileSystemEntity implements File {
|
||||
name: inode.name,
|
||||
nodeType: 0,
|
||||
blobId: newId,
|
||||
size: size,
|
||||
size:
|
||||
0, // TODO: Size not returned by OPFS yet, so 0 for streamed content
|
||||
modified: DateTime.now().millisecondsSinceEpoch,
|
||||
storageType: usedStorageType,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
@@ -334,22 +328,7 @@ class WebFile extends FileSystemEntity implements File {
|
||||
}
|
||||
|
||||
@override
|
||||
Uint8List readAsBytesSync() {
|
||||
final type = _fs.typeSync(path);
|
||||
if (type == FileSystemEntityType.notFound) {
|
||||
throw FileSystemException(
|
||||
'Cannot open file, path = \'$path\' (OS Error: No such file or directory, errno = 2)',
|
||||
path,
|
||||
);
|
||||
}
|
||||
if (type == FileSystemEntityType.directory) {
|
||||
throw FileSystemException(
|
||||
'Cannot open file, path = \'$path\' (OS Error: Is a directory, errno = 21)',
|
||||
path,
|
||||
);
|
||||
}
|
||||
return _fs.makeSyncCall(3, utf8.encode(path));
|
||||
}
|
||||
Uint8List readAsBytesSync() => throw UnsupportedError('Sync not supported');
|
||||
|
||||
@override
|
||||
Future<String> readAsString({Encoding encoding = utf8}) async {
|
||||
@@ -358,21 +337,18 @@ class WebFile extends FileSystemEntity implements File {
|
||||
}
|
||||
|
||||
@override
|
||||
String readAsStringSync({Encoding encoding = utf8}) {
|
||||
return encoding.decode(readAsBytesSync());
|
||||
}
|
||||
String readAsStringSync({Encoding encoding = utf8}) =>
|
||||
throw UnsupportedError('Sync not supported');
|
||||
|
||||
@override
|
||||
Future<List<String>> readAsLines({Encoding encoding = utf8}) async {
|
||||
final str = await readAsString(encoding: encoding);
|
||||
return const LineSplitter().convert(str);
|
||||
return str.split('\n');
|
||||
}
|
||||
|
||||
@override
|
||||
List<String> readAsLinesSync({Encoding encoding = utf8}) {
|
||||
final str = readAsStringSync(encoding: encoding);
|
||||
return const LineSplitter().convert(str);
|
||||
}
|
||||
List<String> readAsLinesSync({Encoding encoding = utf8}) =>
|
||||
throw UnsupportedError('Sync not supported');
|
||||
|
||||
@override
|
||||
Future<bool> exists() async {
|
||||
@@ -385,9 +361,7 @@ class WebFile extends FileSystemEntity implements File {
|
||||
}
|
||||
|
||||
@override
|
||||
bool existsSync() {
|
||||
return _fs.typeSync(path, followLinks: false) == FileSystemEntityType.file;
|
||||
}
|
||||
bool existsSync() => throw UnsupportedError('Sync not supported');
|
||||
|
||||
@override
|
||||
Future<File> rename(String newPath) async {
|
||||
@@ -411,58 +385,32 @@ class WebFile extends FileSystemEntity implements File {
|
||||
}
|
||||
|
||||
@override
|
||||
File renameSync(String newPath) {
|
||||
final type = _fs.typeSync(path, followLinks: false);
|
||||
if (type == FileSystemEntityType.notFound) {
|
||||
throw FileSystemException(
|
||||
'Cannot rename file, path = \'$path\' (OS Error: No such file or directory, errno = 2)',
|
||||
path,
|
||||
);
|
||||
}
|
||||
final newParentDir = _fs.path.dirname(newPath);
|
||||
if (_fs.typeSync(newParentDir) != FileSystemEntityType.directory) {
|
||||
throw FileSystemException(
|
||||
'Cannot rename file, path = \'$path\' (OS Error: No such file or directory, errno = 2)',
|
||||
path,
|
||||
);
|
||||
}
|
||||
|
||||
final pathBytes = utf8.encode(path);
|
||||
final newPathBytes = utf8.encode(newPath);
|
||||
final request = Uint8List(4 + pathBytes.length + newPathBytes.length);
|
||||
final bd = ByteData.sublistView(request);
|
||||
bd.setUint32(0, pathBytes.length, Endian.little);
|
||||
request.setRange(4, 4 + pathBytes.length, pathBytes);
|
||||
request.setRange(4 + pathBytes.length, request.length, newPathBytes);
|
||||
|
||||
_fs.makeSyncCall(12, request);
|
||||
return WebFile(_fs, newPath);
|
||||
}
|
||||
File renameSync(String newPath) =>
|
||||
throw UnsupportedError('Sync not supported');
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
@override
|
||||
void deleteSync({bool recursive = false}) {
|
||||
final type = _fs.typeSync(path, followLinks: false);
|
||||
if (type == FileSystemEntityType.notFound) {
|
||||
throw FileSystemException(
|
||||
'Cannot delete file, path = \'$path\' (OS Error: No such file or directory, errno = 2)',
|
||||
path,
|
||||
);
|
||||
}
|
||||
_fs.makeSyncCall(6, utf8.encode(path));
|
||||
}
|
||||
void deleteSync({bool recursive = false}) =>
|
||||
throw UnsupportedError('Sync not supported');
|
||||
|
||||
@override
|
||||
Future<FileStat> stat() => _fs.stat(path);
|
||||
|
||||
@override
|
||||
FileStat statSync() => _fs.statSync(path);
|
||||
FileStat statSync() => throw UnsupportedError('Sync not supported');
|
||||
|
||||
@override
|
||||
Uri get uri => Uri.parse(path);
|
||||
@@ -478,10 +426,11 @@ class WebFile extends FileSystemEntity implements File {
|
||||
File get absolute => WebFile(_fs, _fs.path.absolute(path));
|
||||
|
||||
@override
|
||||
Future<String> resolveSymbolicLinks() => _fs.resolveSymbolicLinks(path);
|
||||
Future<String> resolveSymbolicLinks() async => path;
|
||||
|
||||
@override
|
||||
String resolveSymbolicLinksSync() => _fs.resolveSymbolicLinksSync(path);
|
||||
String resolveSymbolicLinksSync() =>
|
||||
throw UnsupportedError('Sync not supported');
|
||||
|
||||
@override
|
||||
Stream<FileSystemEvent> watch({
|
||||
@@ -494,10 +443,10 @@ class WebFile extends FileSystemEntity implements File {
|
||||
|
||||
class _WebIOSink implements IOSink {
|
||||
final StreamController<List<int>> _controller;
|
||||
final Future<void> _writeFuture;
|
||||
final Future<void> Function()? onDone;
|
||||
Encoding _encoding;
|
||||
|
||||
_WebIOSink(this._controller, this._writeFuture, this._encoding);
|
||||
_WebIOSink(this._controller, this._encoding, {this.onDone});
|
||||
|
||||
@override
|
||||
Encoding get encoding => _encoding;
|
||||
@@ -507,35 +456,27 @@ class _WebIOSink implements IOSink {
|
||||
|
||||
@override
|
||||
void add(List<int> data) {
|
||||
if (_controller.isClosed) return;
|
||||
_controller.add(data);
|
||||
}
|
||||
|
||||
@override
|
||||
void addError(Object error, [StackTrace? stackTrace]) {
|
||||
if (_controller.isClosed) return;
|
||||
_controller.addError(error, stackTrace);
|
||||
}
|
||||
|
||||
@override
|
||||
Future addStream(Stream<List<int>> stream) {
|
||||
return Future.any([
|
||||
_controller.addStream(stream),
|
||||
_writeFuture,
|
||||
]);
|
||||
return _controller.addStream(stream);
|
||||
}
|
||||
|
||||
@override
|
||||
Future close() async {
|
||||
await _controller.close();
|
||||
await _writeFuture;
|
||||
if (onDone != null) await onDone!();
|
||||
}
|
||||
|
||||
@override
|
||||
Future get done => Future.any([
|
||||
_controller.done,
|
||||
_writeFuture,
|
||||
]);
|
||||
Future get done => _controller.done;
|
||||
|
||||
@override
|
||||
Future flush() async {}
|
||||
|
||||
@@ -1,8 +1,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 WebLink extends FileSystemEntity implements Link {
|
||||
@@ -36,7 +35,7 @@ class WebLink extends FileSystemEntity implements Link {
|
||||
|
||||
// Write target path string to OPFS blob
|
||||
final stream = Stream.value(utf8.encode(target));
|
||||
final (blobId, _) = await _fs.opfs.writeBlob(stream);
|
||||
final blobId = await _fs.opfs.writeBlob(stream);
|
||||
|
||||
final parentPath = _fs.path.dirname(path);
|
||||
final parentInode = await _fs.resolvepath(parentPath);
|
||||
@@ -58,41 +57,7 @@ class WebLink extends FileSystemEntity implements Link {
|
||||
|
||||
@override
|
||||
void createSync(String target, {bool recursive = false}) {
|
||||
if (existsSync()) {
|
||||
throw FileSystemException(
|
||||
'Link already exists',
|
||||
path,
|
||||
const OSError('EEXIST', 17),
|
||||
);
|
||||
}
|
||||
|
||||
final parentPath = _fs.path.dirname(path);
|
||||
final parentType = _fs.typeSync(parentPath);
|
||||
if (parentType == FileSystemEntityType.notFound) {
|
||||
if (recursive) {
|
||||
_fs.directory(parentPath).createSync(recursive: true);
|
||||
} else {
|
||||
throw FileSystemException(
|
||||
'Cannot create link, path = \'$path\' (OS Error: No such file or directory, errno = 2)',
|
||||
path,
|
||||
);
|
||||
}
|
||||
} else if (parentType != FileSystemEntityType.directory) {
|
||||
throw FileSystemException(
|
||||
'Cannot create link, path = \'$path\' (OS Error: Not a directory, errno = 20)',
|
||||
path,
|
||||
);
|
||||
}
|
||||
|
||||
final pathBytes = utf8.encode(path);
|
||||
final targetBytes = utf8.encode(target);
|
||||
final request = Uint8List(4 + pathBytes.length + targetBytes.length);
|
||||
final bd = ByteData.sublistView(request);
|
||||
bd.setUint32(0, pathBytes.length, Endian.little);
|
||||
request.setRange(4, 4 + pathBytes.length, pathBytes);
|
||||
request.setRange(4 + pathBytes.length, request.length, targetBytes);
|
||||
|
||||
_fs.makeSyncCall(7, request);
|
||||
throw UnsupportedError('Sync not supported');
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -101,7 +66,7 @@ class WebLink extends FileSystemEntity implements Link {
|
||||
|
||||
// Write new blob
|
||||
final stream = Stream.value(utf8.encode(target));
|
||||
final (blobId, _) = await _fs.opfs.writeBlob(stream);
|
||||
final blobId = await _fs.opfs.writeBlob(stream);
|
||||
|
||||
await _fs.idb.updateInode(
|
||||
Inode(
|
||||
@@ -120,30 +85,7 @@ class WebLink extends FileSystemEntity implements Link {
|
||||
|
||||
@override
|
||||
void updateSync(String target) {
|
||||
final type = _fs.typeSync(path, followLinks: false);
|
||||
if (type == FileSystemEntityType.notFound) {
|
||||
throw FileSystemException(
|
||||
'Cannot update link, path = \'$path\' (OS Error: No such file or directory, errno = 2)',
|
||||
path,
|
||||
);
|
||||
}
|
||||
if (type != FileSystemEntityType.link) {
|
||||
throw FileSystemException(
|
||||
'Not a link',
|
||||
path,
|
||||
const OSError('EINVAL', 22),
|
||||
);
|
||||
}
|
||||
|
||||
final pathBytes = utf8.encode(path);
|
||||
final targetBytes = utf8.encode(target);
|
||||
final request = Uint8List(4 + pathBytes.length + targetBytes.length);
|
||||
final bd = ByteData.sublistView(request);
|
||||
bd.setUint32(0, pathBytes.length, Endian.little);
|
||||
request.setRange(4, 4 + pathBytes.length, pathBytes);
|
||||
request.setRange(4 + pathBytes.length, request.length, targetBytes);
|
||||
|
||||
_fs.makeSyncCall(13, request);
|
||||
throw UnsupportedError('Sync not supported');
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -165,22 +107,7 @@ class WebLink extends FileSystemEntity implements Link {
|
||||
|
||||
@override
|
||||
String targetSync() {
|
||||
final type = _fs.typeSync(path, followLinks: false);
|
||||
if (type == FileSystemEntityType.notFound) {
|
||||
throw FileSystemException(
|
||||
'Cannot read link, path = \'$path\' (OS Error: No such file or directory, errno = 2)',
|
||||
path,
|
||||
);
|
||||
}
|
||||
if (type != FileSystemEntityType.link) {
|
||||
throw FileSystemException(
|
||||
'Not a link',
|
||||
path,
|
||||
const OSError('EINVAL', 22),
|
||||
);
|
||||
}
|
||||
final respBytes = _fs.makeSyncCall(8, utf8.encode(path));
|
||||
return utf8.decode(respBytes);
|
||||
throw UnsupportedError('Sync not supported');
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -205,33 +132,8 @@ class WebLink extends FileSystemEntity implements Link {
|
||||
}
|
||||
|
||||
@override
|
||||
Link renameSync(String newPath) {
|
||||
final type = _fs.typeSync(path, followLinks: false);
|
||||
if (type == FileSystemEntityType.notFound) {
|
||||
throw FileSystemException(
|
||||
'Cannot rename link, path = \'$path\' (OS Error: No such file or directory, errno = 2)',
|
||||
path,
|
||||
);
|
||||
}
|
||||
final newParentDir = _fs.path.dirname(newPath);
|
||||
if (_fs.typeSync(newParentDir) != FileSystemEntityType.directory) {
|
||||
throw FileSystemException(
|
||||
'Cannot rename link, path = \'$path\' (OS Error: No such file or directory, errno = 2)',
|
||||
path,
|
||||
);
|
||||
}
|
||||
|
||||
final pathBytes = utf8.encode(path);
|
||||
final newPathBytes = utf8.encode(newPath);
|
||||
final request = Uint8List(4 + pathBytes.length + newPathBytes.length);
|
||||
final bd = ByteData.sublistView(request);
|
||||
bd.setUint32(0, pathBytes.length, Endian.little);
|
||||
request.setRange(4, 4 + pathBytes.length, pathBytes);
|
||||
request.setRange(4 + pathBytes.length, request.length, newPathBytes);
|
||||
|
||||
_fs.makeSyncCall(12, request);
|
||||
return WebLink(_fs, newPath);
|
||||
}
|
||||
Link renameSync(String newPath) =>
|
||||
throw UnsupportedError('Sync not supported');
|
||||
|
||||
@override
|
||||
Future<FileSystemEntity> delete({bool recursive = false}) async {
|
||||
@@ -241,16 +143,8 @@ class WebLink extends FileSystemEntity implements Link {
|
||||
}
|
||||
|
||||
@override
|
||||
void deleteSync({bool recursive = false}) {
|
||||
final type = _fs.typeSync(path, followLinks: false);
|
||||
if (type == FileSystemEntityType.notFound) {
|
||||
throw FileSystemException(
|
||||
'Cannot delete link, path = \'$path\' (OS Error: No such file or directory, errno = 2)',
|
||||
path,
|
||||
);
|
||||
}
|
||||
_fs.makeSyncCall(6, utf8.encode(path));
|
||||
}
|
||||
void deleteSync({bool recursive = false}) =>
|
||||
throw UnsupportedError('Sync not supported');
|
||||
|
||||
@override
|
||||
Future<bool> exists() async {
|
||||
@@ -263,15 +157,13 @@ class WebLink extends FileSystemEntity implements Link {
|
||||
}
|
||||
|
||||
@override
|
||||
bool existsSync() {
|
||||
return _fs.typeSync(path, followLinks: false) == FileSystemEntityType.link;
|
||||
}
|
||||
bool existsSync() => throw UnsupportedError('Sync not supported');
|
||||
|
||||
@override
|
||||
Future<FileStat> stat() async => (await _fs.stat(path));
|
||||
|
||||
@override
|
||||
FileStat statSync() => _fs.statSync(path);
|
||||
FileStat statSync() => throw UnsupportedError('Sync not supported');
|
||||
|
||||
@override
|
||||
Uri get uri => Uri.parse(path);
|
||||
@@ -292,10 +184,21 @@ class WebLink extends FileSystemEntity implements Link {
|
||||
Link get absolute => WebLink(_fs, _fs.path.absolute(path));
|
||||
|
||||
@override
|
||||
Future<String> resolveSymbolicLinks() => _fs.resolveSymbolicLinks(path);
|
||||
Future<String> resolveSymbolicLinks() async {
|
||||
// If we are a link, return target? No, resolveSymbolicLinks follows all the way to canonical path.
|
||||
// For now, simpler: resolve path logic.
|
||||
final targetPath = await target();
|
||||
// If target is relative, resolve against directory. This gets complex.
|
||||
// MVP: Return target path raw? No, contract says "path with all symbolic links resolved".
|
||||
// This requires full traversal logic.
|
||||
// For MVP just return the path as we stored it if it's absolute, or join if relative.
|
||||
if (_fs.path.isAbsolute(targetPath)) return targetPath;
|
||||
return _fs.path.normalize(_fs.path.join(dirname, targetPath));
|
||||
}
|
||||
|
||||
@override
|
||||
String resolveSymbolicLinksSync() => _fs.resolveSymbolicLinksSync(path);
|
||||
String resolveSymbolicLinksSync() =>
|
||||
throw UnsupportedError('Sync not supported');
|
||||
|
||||
@override
|
||||
Stream<FileSystemEvent> watch({
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
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 'package:web_file_system/src/backend/sync_rpc_helper.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';
|
||||
@@ -15,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();
|
||||
@@ -63,43 +66,9 @@ class WebFileSystem extends FileSystem {
|
||||
}
|
||||
}
|
||||
|
||||
static void registerWorkerProxy(dynamic worker, WebFileSystem fs) {
|
||||
SyncRpcHelper.registerWorkerProxy(worker as JSAny, fs as JSAny);
|
||||
}
|
||||
|
||||
Uint8List makeSyncCall(int cmd, Uint8List request) {
|
||||
if (!SyncRpcHelper.isWorker) {
|
||||
throw UnsupportedError('Synchronous operations are only supported inside Web Workers.');
|
||||
}
|
||||
if (!SyncRpcHelper.isSharedArrayBufferSupported) {
|
||||
throw StateError('SharedArrayBuffer is not supported. Ensure your site is cross-origin isolated with COOP/COEP headers.');
|
||||
}
|
||||
return SyncRpcHelper.sendSyncRequest(cmd, request);
|
||||
}
|
||||
|
||||
@override
|
||||
FileSystemEntityType typeSync(String path, {bool followLinks = true}) {
|
||||
if (!SyncRpcHelper.isWorker) {
|
||||
throw UnsupportedError('Synchronous operations are only supported inside Web Workers.');
|
||||
}
|
||||
if (!SyncRpcHelper.isSharedArrayBufferSupported) {
|
||||
throw StateError('SharedArrayBuffer is not supported. Ensure your site is cross-origin isolated with COOP/COEP headers.');
|
||||
}
|
||||
try {
|
||||
final pathBytes = utf8.encode(path);
|
||||
final payload = Uint8List(pathBytes.length + 1);
|
||||
payload[0] = followLinks ? 1 : 0;
|
||||
payload.setRange(1, payload.length, pathBytes);
|
||||
|
||||
final respBytes = makeSyncCall(2, payload);
|
||||
final typeStr = utf8.decode(respBytes);
|
||||
if (typeStr == 'file') return FileSystemEntityType.file;
|
||||
if (typeStr == 'directory') return FileSystemEntityType.directory;
|
||||
if (typeStr == 'link') return FileSystemEntityType.link;
|
||||
return FileSystemEntityType.notFound;
|
||||
} catch (_) {
|
||||
return FileSystemEntityType.notFound;
|
||||
}
|
||||
throw UnsupportedError('Sync type not supported');
|
||||
}
|
||||
|
||||
// Internal Resolution Logic
|
||||
@@ -194,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;
|
||||
@@ -213,34 +183,7 @@ class WebFileSystem extends FileSystem {
|
||||
|
||||
@override
|
||||
FileStat statSync(String path) {
|
||||
if (!SyncRpcHelper.isWorker) {
|
||||
throw UnsupportedError('Synchronous operations are only supported inside Web Workers.');
|
||||
}
|
||||
if (!SyncRpcHelper.isSharedArrayBufferSupported) {
|
||||
throw StateError('SharedArrayBuffer is not supported. Ensure your site is cross-origin isolated with COOP/COEP headers.');
|
||||
}
|
||||
try {
|
||||
final respBytes = makeSyncCall(9, utf8.encode(path));
|
||||
final statMap = json.decode(utf8.decode(respBytes)) as Map<String, dynamic>;
|
||||
final typeStr = statMap['type'] as String;
|
||||
final size = statMap['size'] as int;
|
||||
final modified = statMap['modified'] as int;
|
||||
|
||||
FileSystemEntityType type;
|
||||
if (typeStr == 'file') {
|
||||
type = FileSystemEntityType.file;
|
||||
} else if (typeStr == 'directory') {
|
||||
type = FileSystemEntityType.directory;
|
||||
} else if (typeStr == 'link') {
|
||||
type = FileSystemEntityType.link;
|
||||
} else {
|
||||
type = FileSystemEntityType.notFound;
|
||||
}
|
||||
|
||||
return FileStatImpl(modified, size, type);
|
||||
} catch (_) {
|
||||
return FileStatImpl(0, 0, FileSystemEntityType.notFound);
|
||||
}
|
||||
throw UnsupportedError('Sync stat not supported');
|
||||
}
|
||||
|
||||
FileSystemEntityType _getType(int nodeType) {
|
||||
@@ -252,15 +195,15 @@ class WebFileSystem extends FileSystem {
|
||||
|
||||
@override
|
||||
bool isFileSync(String path) =>
|
||||
typeSync(path) == FileSystemEntityType.file;
|
||||
throw UnsupportedError('Sync isFile not supported');
|
||||
|
||||
@override
|
||||
bool isDirectorySync(String path) =>
|
||||
typeSync(path) == FileSystemEntityType.directory;
|
||||
throw UnsupportedError('Sync isDirectory not supported');
|
||||
|
||||
@override
|
||||
bool isLinkSync(String path) =>
|
||||
typeSync(path, followLinks: false) == FileSystemEntityType.link;
|
||||
throw UnsupportedError('Sync isLink not supported');
|
||||
|
||||
@override
|
||||
Future<bool> isFile(String path) async =>
|
||||
@@ -274,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
|
||||
@@ -281,8 +225,9 @@ 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);
|
||||
@@ -290,31 +235,20 @@ class WebFileSystem extends FileSystem {
|
||||
}
|
||||
|
||||
@override
|
||||
bool identicalSync(String path1, String path2) {
|
||||
try {
|
||||
final r1 = resolveSymbolicLinksSync(path1);
|
||||
final r2 = resolveSymbolicLinksSync(path2);
|
||||
return r1 == r2;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@override
|
||||
bool identicalSync(String path1, String path2) =>
|
||||
throw UnsupportedError('Sync not supported');
|
||||
|
||||
Future<String> resolveSymbolicLinks(String pathStr) async {
|
||||
final inode = await resolvepath(pathStr, followLinks: true);
|
||||
final List<String> segments = [];
|
||||
Inode current = inode;
|
||||
while (current.id != IdbInodeService.rootId) {
|
||||
segments.add(current.name);
|
||||
current = await _idb.getInode(current.parentId);
|
||||
}
|
||||
if (segments.isEmpty) return '/';
|
||||
return '/' + segments.reversed.join('/');
|
||||
}
|
||||
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);
|
||||
|
||||
String resolveSymbolicLinksSync(String pathStr) {
|
||||
final respBytes = makeSyncCall(11, utf8.encode(pathStr));
|
||||
return utf8.decode(respBytes);
|
||||
if (inode.storageType == 1) {
|
||||
return _idbStore.getBlob(inode.blobId!);
|
||||
} else {
|
||||
return _opfs.getBlob(inode.blobId!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -141,10 +141,10 @@ packages:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: lints
|
||||
sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df"
|
||||
sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.0"
|
||||
version: "3.0.0"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -403,4 +403,3 @@ packages:
|
||||
version: "3.1.3"
|
||||
sdks:
|
||||
dart: ">=3.11.0 <4.0.0"
|
||||
flutter: ">=3.38.5"
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
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.10.0
|
||||
flutter: ^3.38.5
|
||||
|
||||
dependencies:
|
||||
file: ^7.0.0
|
||||
path: ^1.9.0
|
||||
web: ^1.1.1
|
||||
uuid: ^4.0.0
|
||||
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));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user