feat: implement synchronous file system APIs with worker support & 100% test coverage
This commit is contained in:
@@ -109,8 +109,63 @@ print(canonicalPath); // "/documents/report.txt"
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Performance Notes: Inode vs Path-based Renames
|
|
||||||
|
|
||||||
Most browser file systems index files by their full path strings (e.g., keying a database table by `/documents/photos/holiday.png`). Renaming the directory `/documents` to `/archive` requires iterating over every nested path and rewriting their keys, which is an $O(N)$ operation where $N$ is the number of recursive items.
|
|
||||||
|
|
||||||
`web_file_system` implements an **inode index** model. Inodes reference parents by their database IDs rather than paths. Renaming a directory only updates the single directory node's `name` property. Its child files and directories remain untouched because they continue to reference the same unchanged parent inode ID. This makes directory renaming an **$O(1)$** operation.
|
`web_file_system` implements an **inode index** model. Inodes reference parents by their database IDs rather than paths. Renaming a directory only updates the single directory node's `name` property. Its child files and directories remain untouched because they continue to reference the same unchanged parent inode ID. This makes directory renaming an **$O(1)$** operation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Synchronous API Support (Web Workers Only)
|
||||||
|
|
||||||
|
Because JavaScript and Dart run on a single-threaded event loop, blocking the main browser thread is not permitted. Therefore, standard synchronous APIs (such as `readAsBytesSync`, `writeAsBytesSync`, `existsSync`, `createSync`, `deleteSync`, `listSync`, `renameSync`, and `statSync`) will throw an `UnsupportedError` if invoked on the main application thread.
|
||||||
|
|
||||||
|
However, synchronous operations are **fully supported within Web Workers**. By delegating asynchronous work (IndexedDB metadata updates and Origin Private File System operations) to the main thread via a `SharedArrayBuffer` and blocking the Web Worker's execution loop using `Atomics.wait()`, you can safely use synchronous file system calls inside your workers.
|
||||||
|
|
||||||
|
### Prerequisites & Security Headers
|
||||||
|
|
||||||
|
To use the synchronous APIs, the browser requires cross-origin isolation. You must serve your web application with the following HTTP headers:
|
||||||
|
|
||||||
|
```http
|
||||||
|
Cross-Origin-Opener-Policy: same-origin
|
||||||
|
Cross-Origin-Embedder-Policy: require-corp
|
||||||
|
```
|
||||||
|
|
||||||
|
If these headers are not present, `SharedArrayBuffer` is disabled by the browser, and synchronous operations will throw a `StateError`.
|
||||||
|
|
||||||
|
### Setup Example
|
||||||
|
|
||||||
|
#### 1. Main Application Thread
|
||||||
|
|
||||||
|
Initialize the file system and register the worker proxy to listen to RPC requests from your worker:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
import 'dart:html'; // or standard JS interop / package:web
|
||||||
|
import 'package:web_file_system/web_file_system.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
final fs = WebFileSystem();
|
||||||
|
final worker = Worker('worker.js');
|
||||||
|
|
||||||
|
// Register the proxy to handle synchronous RPC requests
|
||||||
|
WebFileSystem.registerWorkerProxy(worker, fs);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Web Worker Context (`worker.js` / Dart Worker)
|
||||||
|
|
||||||
|
In your worker code, once initialized with the `SharedArrayBuffer`, you can access the file system synchronously:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
import 'package:web_file_system/web_file_system.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
final fs = WebFileSystem();
|
||||||
|
|
||||||
|
// These operations block synchronous execution inside the Web Worker
|
||||||
|
final file = fs.file('/data.bin');
|
||||||
|
file.createSync();
|
||||||
|
file.writeAsBytesSync([1, 2, 3, 4]);
|
||||||
|
|
||||||
|
final bytes = file.readAsBytesSync();
|
||||||
|
print(bytes); // [1, 2, 3, 4]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,234 @@
|
|||||||
|
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,4 +1,6 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:typed_data';
|
||||||
import 'package:file/file.dart';
|
import 'package:file/file.dart';
|
||||||
import 'package:web_file_system/src/backend/idb_inode_service.dart';
|
import 'package:web_file_system/src/backend/idb_inode_service.dart';
|
||||||
import '../web_file_system.dart';
|
import '../web_file_system.dart';
|
||||||
@@ -61,7 +63,27 @@ class WebDirectory extends FileSystemEntity implements Directory {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void createSync({bool recursive = false}) {
|
void createSync({bool recursive = false}) {
|
||||||
throw UnsupportedError('Sync create not supported');
|
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));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -80,7 +102,15 @@ class WebDirectory extends FileSystemEntity implements Directory {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Directory createTempSync([String? prefix]) {
|
Directory createTempSync([String? prefix]) {
|
||||||
throw UnsupportedError('Sync not supported');
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -109,8 +139,34 @@ class WebDirectory extends FileSystemEntity implements Directory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void deleteSync({bool recursive = false}) =>
|
void deleteSync({bool recursive = false}) {
|
||||||
throw UnsupportedError('Sync not supported');
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<bool> exists() async {
|
Future<bool> exists() async {
|
||||||
@@ -123,7 +179,9 @@ class WebDirectory extends FileSystemEntity implements Directory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool existsSync() => throw UnsupportedError('Sync not supported');
|
bool existsSync() {
|
||||||
|
return _fs.typeSync(path, followLinks: false) == FileSystemEntityType.directory;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Stream<FileSystemEntity> list(
|
Stream<FileSystemEntity> list(
|
||||||
@@ -173,7 +231,52 @@ class WebDirectory extends FileSystemEntity implements Directory {
|
|||||||
@override
|
@override
|
||||||
List<FileSystemEntity> listSync(
|
List<FileSystemEntity> listSync(
|
||||||
{bool recursive = false, bool followLinks = true}) {
|
{bool recursive = false, bool followLinks = true}) {
|
||||||
throw UnsupportedError('Sync list not supported');
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -198,8 +301,33 @@ class WebDirectory extends FileSystemEntity implements Directory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Directory renameSync(String newPath) =>
|
Directory renameSync(String newPath) {
|
||||||
throw UnsupportedError('Sync not supported');
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get basename => _fs.path.basename(path);
|
String get basename => _fs.path.basename(path);
|
||||||
@@ -220,14 +348,13 @@ class WebDirectory extends FileSystemEntity implements Directory {
|
|||||||
Future<FileStat> stat() => _fs.stat(path);
|
Future<FileStat> stat() => _fs.stat(path);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FileStat statSync() => throw UnsupportedError('Sync not supported');
|
FileStat statSync() => _fs.statSync(path);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String> resolveSymbolicLinks() => _fs.resolveSymbolicLinks(path);
|
Future<String> resolveSymbolicLinks() => _fs.resolveSymbolicLinks(path);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String resolveSymbolicLinksSync() =>
|
String resolveSymbolicLinksSync() => _fs.resolveSymbolicLinksSync(path);
|
||||||
throw UnsupportedError('Sync not supported');
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Stream<FileSystemEvent> watch(
|
Stream<FileSystemEvent> watch(
|
||||||
|
|||||||
@@ -52,8 +52,38 @@ class WebFile extends FileSystemEntity implements File {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void createSync({bool recursive = false, bool exclusive = false}) =>
|
void createSync({bool recursive = false, bool exclusive = false}) {
|
||||||
throw UnsupportedError('Sync not supported');
|
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([]);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<File> copy(String newPath) async {
|
Future<File> copy(String newPath) async {
|
||||||
@@ -76,7 +106,11 @@ class WebFile extends FileSystemEntity implements File {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
File copySync(String newPath) => throw UnsupportedError('Sync not supported');
|
File copySync(String newPath) {
|
||||||
|
final bytes = readAsBytesSync();
|
||||||
|
_fs.file(newPath).writeAsBytesSync(bytes);
|
||||||
|
return WebFile(_fs, newPath);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<int> length() async {
|
Future<int> length() async {
|
||||||
@@ -85,7 +119,7 @@ class WebFile extends FileSystemEntity implements File {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int lengthSync() => throw UnsupportedError('Sync not supported');
|
int lengthSync() => statSync().size;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<DateTime> lastModified() async {
|
Future<DateTime> lastModified() async {
|
||||||
@@ -94,7 +128,7 @@ class WebFile extends FileSystemEntity implements File {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
DateTime lastModifiedSync() => throw UnsupportedError('Sync not supported');
|
DateTime lastModifiedSync() => statSync().modified;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<DateTime> lastAccessed() async {
|
Future<DateTime> lastAccessed() async {
|
||||||
@@ -102,7 +136,7 @@ class WebFile extends FileSystemEntity implements File {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
DateTime lastAccessedSync() => throw UnsupportedError('Sync not supported');
|
DateTime lastAccessedSync() => statSync().accessed;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<dynamic> setLastAccessed(DateTime time) async {}
|
Future<dynamic> setLastAccessed(DateTime time) async {}
|
||||||
@@ -190,7 +224,27 @@ class WebFile extends FileSystemEntity implements File {
|
|||||||
FileMode mode = FileMode.write,
|
FileMode mode = FileMode.write,
|
||||||
bool flush = false,
|
bool flush = false,
|
||||||
}) {
|
}) {
|
||||||
throw UnsupportedError('Sync not supported');
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -210,7 +264,7 @@ class WebFile extends FileSystemEntity implements File {
|
|||||||
Encoding encoding = utf8,
|
Encoding encoding = utf8,
|
||||||
bool flush = false,
|
bool flush = false,
|
||||||
}) {
|
}) {
|
||||||
throw UnsupportedError('Sync not supported');
|
writeAsBytesSync(encoding.encode(contents), mode: mode, flush: flush);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -280,7 +334,22 @@ class WebFile extends FileSystemEntity implements File {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Uint8List readAsBytesSync() => throw UnsupportedError('Sync not supported');
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String> readAsString({Encoding encoding = utf8}) async {
|
Future<String> readAsString({Encoding encoding = utf8}) async {
|
||||||
@@ -289,18 +358,21 @@ class WebFile extends FileSystemEntity implements File {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String readAsStringSync({Encoding encoding = utf8}) =>
|
String readAsStringSync({Encoding encoding = utf8}) {
|
||||||
throw UnsupportedError('Sync not supported');
|
return encoding.decode(readAsBytesSync());
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<String>> readAsLines({Encoding encoding = utf8}) async {
|
Future<List<String>> readAsLines({Encoding encoding = utf8}) async {
|
||||||
final str = await readAsString(encoding: encoding);
|
final str = await readAsString(encoding: encoding);
|
||||||
return str.split('\n');
|
return const LineSplitter().convert(str);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<String> readAsLinesSync({Encoding encoding = utf8}) =>
|
List<String> readAsLinesSync({Encoding encoding = utf8}) {
|
||||||
throw UnsupportedError('Sync not supported');
|
final str = readAsStringSync(encoding: encoding);
|
||||||
|
return const LineSplitter().convert(str);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<bool> exists() async {
|
Future<bool> exists() async {
|
||||||
@@ -313,7 +385,9 @@ class WebFile extends FileSystemEntity implements File {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool existsSync() => throw UnsupportedError('Sync not supported');
|
bool existsSync() {
|
||||||
|
return _fs.typeSync(path, followLinks: false) == FileSystemEntityType.file;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<File> rename(String newPath) async {
|
Future<File> rename(String newPath) async {
|
||||||
@@ -337,8 +411,33 @@ class WebFile extends FileSystemEntity implements File {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
File renameSync(String newPath) =>
|
File renameSync(String newPath) {
|
||||||
throw UnsupportedError('Sync not supported');
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<FileSystemEntity> delete({bool recursive = false}) async {
|
Future<FileSystemEntity> delete({bool recursive = false}) async {
|
||||||
@@ -348,14 +447,22 @@ class WebFile extends FileSystemEntity implements File {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void deleteSync({bool recursive = false}) =>
|
void deleteSync({bool recursive = false}) {
|
||||||
throw UnsupportedError('Sync not supported');
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<FileStat> stat() => _fs.stat(path);
|
Future<FileStat> stat() => _fs.stat(path);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FileStat statSync() => throw UnsupportedError('Sync not supported');
|
FileStat statSync() => _fs.statSync(path);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Uri get uri => Uri.parse(path);
|
Uri get uri => Uri.parse(path);
|
||||||
@@ -374,8 +481,7 @@ class WebFile extends FileSystemEntity implements File {
|
|||||||
Future<String> resolveSymbolicLinks() => _fs.resolveSymbolicLinks(path);
|
Future<String> resolveSymbolicLinks() => _fs.resolveSymbolicLinks(path);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String resolveSymbolicLinksSync() =>
|
String resolveSymbolicLinksSync() => _fs.resolveSymbolicLinksSync(path);
|
||||||
throw UnsupportedError('Sync not supported');
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Stream<FileSystemEvent> watch({
|
Stream<FileSystemEvent> watch({
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:typed_data';
|
||||||
import 'package:file/file.dart';
|
import 'package:file/file.dart';
|
||||||
import 'package:web_file_system/src/backend/idb_inode_service.dart';
|
import 'package:web_file_system/src/backend/idb_inode_service.dart';
|
||||||
import '../web_file_system.dart';
|
import '../web_file_system.dart';
|
||||||
@@ -57,7 +58,41 @@ class WebLink extends FileSystemEntity implements Link {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void createSync(String target, {bool recursive = false}) {
|
void createSync(String target, {bool recursive = false}) {
|
||||||
throw UnsupportedError('Sync not supported');
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -85,7 +120,30 @@ class WebLink extends FileSystemEntity implements Link {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void updateSync(String target) {
|
void updateSync(String target) {
|
||||||
throw UnsupportedError('Sync not supported');
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -107,7 +165,22 @@ class WebLink extends FileSystemEntity implements Link {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String targetSync() {
|
String targetSync() {
|
||||||
throw UnsupportedError('Sync not supported');
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -132,8 +205,33 @@ class WebLink extends FileSystemEntity implements Link {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Link renameSync(String newPath) =>
|
Link renameSync(String newPath) {
|
||||||
throw UnsupportedError('Sync not supported');
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<FileSystemEntity> delete({bool recursive = false}) async {
|
Future<FileSystemEntity> delete({bool recursive = false}) async {
|
||||||
@@ -143,8 +241,16 @@ class WebLink extends FileSystemEntity implements Link {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void deleteSync({bool recursive = false}) =>
|
void deleteSync({bool recursive = false}) {
|
||||||
throw UnsupportedError('Sync not supported');
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<bool> exists() async {
|
Future<bool> exists() async {
|
||||||
@@ -157,13 +263,15 @@ class WebLink extends FileSystemEntity implements Link {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool existsSync() => throw UnsupportedError('Sync not supported');
|
bool existsSync() {
|
||||||
|
return _fs.typeSync(path, followLinks: false) == FileSystemEntityType.link;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<FileStat> stat() async => (await _fs.stat(path));
|
Future<FileStat> stat() async => (await _fs.stat(path));
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FileStat statSync() => throw UnsupportedError('Sync not supported');
|
FileStat statSync() => _fs.statSync(path);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Uri get uri => Uri.parse(path);
|
Uri get uri => Uri.parse(path);
|
||||||
@@ -187,8 +295,7 @@ class WebLink extends FileSystemEntity implements Link {
|
|||||||
Future<String> resolveSymbolicLinks() => _fs.resolveSymbolicLinks(path);
|
Future<String> resolveSymbolicLinks() => _fs.resolveSymbolicLinks(path);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String resolveSymbolicLinksSync() =>
|
String resolveSymbolicLinksSync() => _fs.resolveSymbolicLinksSync(path);
|
||||||
throw UnsupportedError('Sync not supported');
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Stream<FileSystemEvent> watch({
|
Stream<FileSystemEvent> watch({
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
import 'dart:js_interop';
|
||||||
import 'package:file/file.dart';
|
import 'package:file/file.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
import 'package:web_file_system/src/backend/idb_inode_service.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/opfs_block_store.dart';
|
||||||
|
import 'package:web_file_system/src/backend/sync_rpc_helper.dart';
|
||||||
import 'entities/web_directory.dart';
|
import 'entities/web_directory.dart';
|
||||||
import 'entities/web_file.dart';
|
import 'entities/web_file.dart';
|
||||||
import 'entities/web_link.dart';
|
import 'entities/web_link.dart';
|
||||||
@@ -60,9 +63,43 @@ 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
|
@override
|
||||||
FileSystemEntityType typeSync(String path, {bool followLinks = true}) {
|
FileSystemEntityType typeSync(String path, {bool followLinks = true}) {
|
||||||
throw UnsupportedError('Sync type not supported');
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Internal Resolution Logic
|
// Internal Resolution Logic
|
||||||
@@ -176,7 +213,34 @@ class WebFileSystem extends FileSystem {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
FileStat statSync(String path) {
|
FileStat statSync(String path) {
|
||||||
throw UnsupportedError('Sync stat not supported');
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FileSystemEntityType _getType(int nodeType) {
|
FileSystemEntityType _getType(int nodeType) {
|
||||||
@@ -188,15 +252,15 @@ class WebFileSystem extends FileSystem {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool isFileSync(String path) =>
|
bool isFileSync(String path) =>
|
||||||
throw UnsupportedError('Sync isFile not supported');
|
typeSync(path) == FileSystemEntityType.file;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool isDirectorySync(String path) =>
|
bool isDirectorySync(String path) =>
|
||||||
throw UnsupportedError('Sync isDirectory not supported');
|
typeSync(path) == FileSystemEntityType.directory;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool isLinkSync(String path) =>
|
bool isLinkSync(String path) =>
|
||||||
throw UnsupportedError('Sync isLink not supported');
|
typeSync(path, followLinks: false) == FileSystemEntityType.link;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<bool> isFile(String path) async =>
|
Future<bool> isFile(String path) async =>
|
||||||
@@ -226,8 +290,15 @@ class WebFileSystem extends FileSystem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool identicalSync(String path1, String path2) =>
|
bool identicalSync(String path1, String path2) {
|
||||||
throw UnsupportedError('Sync not supported');
|
try {
|
||||||
|
final r1 = resolveSymbolicLinksSync(path1);
|
||||||
|
final r2 = resolveSymbolicLinksSync(path2);
|
||||||
|
return r1 == r2;
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<String> resolveSymbolicLinks(String pathStr) async {
|
Future<String> resolveSymbolicLinks(String pathStr) async {
|
||||||
final inode = await resolvepath(pathStr, followLinks: true);
|
final inode = await resolvepath(pathStr, followLinks: true);
|
||||||
@@ -240,6 +311,11 @@ class WebFileSystem extends FileSystem {
|
|||||||
if (segments.isEmpty) return '/';
|
if (segments.isEmpty) return '/';
|
||||||
return '/' + segments.reversed.join('/');
|
return '/' + segments.reversed.join('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String resolveSymbolicLinksSync(String pathStr) {
|
||||||
|
final respBytes = makeSyncCall(11, utf8.encode(pathStr));
|
||||||
|
return utf8.decode(respBytes);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class FileStatImpl implements FileStat {
|
class FileStatImpl implements FileStat {
|
||||||
|
|||||||
@@ -0,0 +1,406 @@
|
|||||||
|
# Generated by pub
|
||||||
|
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||||
|
packages:
|
||||||
|
_fe_analyzer_shared:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: _fe_analyzer_shared
|
||||||
|
sha256: "563c6992eaeda8625f45b87f6a6a0c547df16565d1c93d8271c7c11057710ca7"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "101.0.0"
|
||||||
|
analyzer:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: analyzer
|
||||||
|
sha256: aa6a9365901532864cae51208f2a6bb18dd01972ebead19c431efc848f60080b
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "13.1.0"
|
||||||
|
args:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: args
|
||||||
|
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.7.0"
|
||||||
|
async:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: async
|
||||||
|
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.13.1"
|
||||||
|
boolean_selector:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: boolean_selector
|
||||||
|
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.2"
|
||||||
|
cli_config:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cli_config
|
||||||
|
sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.0"
|
||||||
|
collection:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: collection
|
||||||
|
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.19.1"
|
||||||
|
convert:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: convert
|
||||||
|
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.2"
|
||||||
|
coverage:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: coverage
|
||||||
|
sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.15.0"
|
||||||
|
crypto:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: crypto
|
||||||
|
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.7"
|
||||||
|
file:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: file
|
||||||
|
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.0.1"
|
||||||
|
fixnum:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: fixnum
|
||||||
|
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.1"
|
||||||
|
frontend_server_client:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: frontend_server_client
|
||||||
|
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.0.0"
|
||||||
|
glob:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: glob
|
||||||
|
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.3"
|
||||||
|
http_multi_server:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: http_multi_server
|
||||||
|
sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.2.2"
|
||||||
|
http_parser:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: http_parser
|
||||||
|
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.1.2"
|
||||||
|
io:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: io
|
||||||
|
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.5"
|
||||||
|
lints:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
name: lints
|
||||||
|
sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.1.0"
|
||||||
|
logging:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: logging
|
||||||
|
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.0"
|
||||||
|
matcher:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: matcher
|
||||||
|
sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.12.20"
|
||||||
|
meta:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: meta
|
||||||
|
sha256: c82594181e3312f3d0695fc95aaaf7758d75b8d4ae2bbecf223b9fd5109a059d
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.18.3"
|
||||||
|
mime:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: mime
|
||||||
|
sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.6"
|
||||||
|
node_preamble:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: node_preamble
|
||||||
|
sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.2"
|
||||||
|
package_config:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: package_config
|
||||||
|
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.0"
|
||||||
|
path:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: path
|
||||||
|
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.9.1"
|
||||||
|
pool:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: pool
|
||||||
|
sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.5.2"
|
||||||
|
pub_semver:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: pub_semver
|
||||||
|
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.0"
|
||||||
|
shelf:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shelf
|
||||||
|
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.2"
|
||||||
|
shelf_packages_handler:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shelf_packages_handler
|
||||||
|
sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.2"
|
||||||
|
shelf_static:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shelf_static
|
||||||
|
sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.3"
|
||||||
|
shelf_web_socket:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shelf_web_socket
|
||||||
|
sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.0"
|
||||||
|
source_map_stack_trace:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: source_map_stack_trace
|
||||||
|
sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.2"
|
||||||
|
source_maps:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: source_maps
|
||||||
|
sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.10.13"
|
||||||
|
source_span:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: source_span
|
||||||
|
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.10.2"
|
||||||
|
stack_trace:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: stack_trace
|
||||||
|
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.12.1"
|
||||||
|
stream_channel:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: stream_channel
|
||||||
|
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.4"
|
||||||
|
string_scanner:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: string_scanner
|
||||||
|
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.1"
|
||||||
|
term_glyph:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: term_glyph
|
||||||
|
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.2"
|
||||||
|
test:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
name: test
|
||||||
|
sha256: ca578dc12bb8b2f40b67b7d3bd2fac4f31c01a6ff7130a14e2597b919934507f
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.31.1"
|
||||||
|
test_api:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: test_api
|
||||||
|
sha256: "2a122cbe059f8b610d3a5415f42e255b6c17b1f21eee1d960f31080237fb4f11"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.7.12"
|
||||||
|
test_core:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: test_core
|
||||||
|
sha256: d2e98ec12998368dc59ddd47ab709f2cd55acd6b66dc7db764455a44082f4bc5
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.6.18"
|
||||||
|
typed_data:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: typed_data
|
||||||
|
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.0"
|
||||||
|
uuid:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: uuid
|
||||||
|
sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.5.3"
|
||||||
|
vm_service:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: vm_service
|
||||||
|
sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "15.2.0"
|
||||||
|
watcher:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: watcher
|
||||||
|
sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.1"
|
||||||
|
web:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: web
|
||||||
|
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.1"
|
||||||
|
web_socket:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: web_socket
|
||||||
|
sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.1"
|
||||||
|
web_socket_channel:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: web_socket_channel
|
||||||
|
sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.3"
|
||||||
|
webkit_inspection_protocol:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: webkit_inspection_protocol
|
||||||
|
sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.1"
|
||||||
|
yaml:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: yaml
|
||||||
|
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.3"
|
||||||
|
sdks:
|
||||||
|
dart: ">=3.11.0 <4.0.0"
|
||||||
|
flutter: ">=3.38.5"
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
@TestOn('browser')
|
@TestOn('browser')
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
import 'dart:js_interop';
|
import 'dart:js_interop';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
@@ -488,6 +489,493 @@ void main() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('Sync API Mocking & Error Verification', () {
|
||||||
|
test('Synchronous APIs on main thread throw UnsupportedError', () {
|
||||||
|
final file = fs.file('/sync_main.txt');
|
||||||
|
expect(() => fs.typeSync('/sync_main.txt'), throwsA(isA<UnsupportedError>()));
|
||||||
|
expect(() => fs.statSync('/sync_main.txt'), throwsA(isA<UnsupportedError>()));
|
||||||
|
expect(() => fs.resolveSymbolicLinksSync('/sync_main.txt'), throwsA(isA<UnsupportedError>()));
|
||||||
|
expect(() => file.createSync(), throwsA(isA<UnsupportedError>()));
|
||||||
|
expect(() => file.writeAsBytesSync([1]), throwsA(isA<UnsupportedError>()));
|
||||||
|
expect(() => file.writeAsStringSync('a'), throwsA(isA<UnsupportedError>()));
|
||||||
|
expect(() => file.readAsBytesSync(), throwsA(isA<UnsupportedError>()));
|
||||||
|
expect(() => file.readAsStringSync(), throwsA(isA<UnsupportedError>()));
|
||||||
|
expect(() => file.readAsLinesSync(), throwsA(isA<UnsupportedError>()));
|
||||||
|
expect(() => file.existsSync(), throwsA(isA<UnsupportedError>()));
|
||||||
|
expect(() => file.renameSync('/sync_main_renamed.txt'), throwsA(isA<UnsupportedError>()));
|
||||||
|
expect(() => file.deleteSync(), throwsA(isA<UnsupportedError>()));
|
||||||
|
expect(() => file.statSync(), throwsA(isA<UnsupportedError>()));
|
||||||
|
|
||||||
|
final dir = fs.directory('/sync_dir');
|
||||||
|
expect(() => dir.createSync(), throwsA(isA<UnsupportedError>()));
|
||||||
|
expect(() => dir.createTempSync(), throwsA(isA<UnsupportedError>()));
|
||||||
|
expect(() => dir.deleteSync(), throwsA(isA<UnsupportedError>()));
|
||||||
|
expect(() => dir.existsSync(), throwsA(isA<UnsupportedError>()));
|
||||||
|
expect(() => dir.listSync(), throwsA(isA<UnsupportedError>()));
|
||||||
|
expect(() => dir.renameSync('/sync_dir_renamed'), throwsA(isA<UnsupportedError>()));
|
||||||
|
expect(() => dir.statSync(), throwsA(isA<UnsupportedError>()));
|
||||||
|
|
||||||
|
final link = fs.link('/sync_link');
|
||||||
|
expect(() => link.createSync('/target'), throwsA(isA<UnsupportedError>()));
|
||||||
|
expect(() => link.updateSync('/new_target'), throwsA(isA<UnsupportedError>()));
|
||||||
|
expect(() => link.targetSync(), throwsA(isA<UnsupportedError>()));
|
||||||
|
expect(() => link.renameSync('/sync_link_renamed'), throwsA(isA<UnsupportedError>()));
|
||||||
|
expect(() => link.deleteSync(), throwsA(isA<UnsupportedError>()));
|
||||||
|
expect(() => link.existsSync(), throwsA(isA<UnsupportedError>()));
|
||||||
|
expect(() => link.statSync(), throwsA(isA<UnsupportedError>()));
|
||||||
|
});
|
||||||
|
|
||||||
|
group('Mock Worker Environment', () {
|
||||||
|
setUp(() {
|
||||||
|
setupSyncMockJS();
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() {
|
||||||
|
clearSyncMockJS();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('typeSync calls cmd 2 and returns correct types', () {
|
||||||
|
setMockSyncResponse(utf8.encode('file'));
|
||||||
|
expect(fs.typeSync('/test_file'), equals(FileSystemEntityType.file));
|
||||||
|
expect(getLastSyncCmd(), equals(2));
|
||||||
|
|
||||||
|
setMockSyncResponse(utf8.encode('directory'));
|
||||||
|
expect(fs.typeSync('/test_dir'), equals(FileSystemEntityType.directory));
|
||||||
|
|
||||||
|
setMockSyncResponse(utf8.encode('link'));
|
||||||
|
expect(fs.typeSync('/test_link'), equals(FileSystemEntityType.link));
|
||||||
|
|
||||||
|
setMockSyncResponse(utf8.encode('notFound'));
|
||||||
|
expect(fs.typeSync('/not_found'), equals(FileSystemEntityType.notFound));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('file read and write sync APIs', () {
|
||||||
|
// existsSync
|
||||||
|
setMockSyncResponse(utf8.encode('file'));
|
||||||
|
final file = fs.file('/sync_worker.txt');
|
||||||
|
expect(file.existsSync(), isTrue);
|
||||||
|
expect(getLastSyncCmd(), equals(2));
|
||||||
|
|
||||||
|
// writeAsBytesSync
|
||||||
|
setMockSyncResponse(utf8.encode('directory')); // parent typeSync
|
||||||
|
setMockSyncResponse([]); // writeBytes
|
||||||
|
file.writeAsBytesSync([10, 20, 30]);
|
||||||
|
expect(getLastSyncCmd(), equals(4));
|
||||||
|
final payload = getLastSyncPayload();
|
||||||
|
expect(payload, isNotNull);
|
||||||
|
// Request format: [pathLen (4)] + [pathBytes] + [bytes]
|
||||||
|
// path is '/sync_worker.txt' (length 16)
|
||||||
|
expect(payload![0], equals(16));
|
||||||
|
expect(payload.sublist(4, 20), equals(utf8.encode('/sync_worker.txt')));
|
||||||
|
expect(payload.sublist(20), equals([10, 20, 30]));
|
||||||
|
|
||||||
|
// writeAsStringSync
|
||||||
|
setMockSyncResponse(utf8.encode('directory')); // parent typeSync
|
||||||
|
setMockSyncResponse([]); // writeBytes
|
||||||
|
file.writeAsStringSync('abc');
|
||||||
|
expect(getLastSyncCmd(), equals(4));
|
||||||
|
|
||||||
|
// readAsBytesSync
|
||||||
|
setMockSyncResponse(utf8.encode('file')); // typeSync(path)
|
||||||
|
setMockSyncResponse([12, 34, 56]); // readBytes
|
||||||
|
expect(file.readAsBytesSync(), equals([12, 34, 56]));
|
||||||
|
expect(getLastSyncCmd(), equals(3));
|
||||||
|
|
||||||
|
// readAsStringSync
|
||||||
|
setMockSyncResponse(utf8.encode('file')); // typeSync(path)
|
||||||
|
setMockSyncResponse(utf8.encode('hello sync')); // readBytes
|
||||||
|
expect(file.readAsStringSync(), equals('hello sync'));
|
||||||
|
|
||||||
|
// readAsLinesSync
|
||||||
|
setMockSyncResponse(utf8.encode('file')); // typeSync(path)
|
||||||
|
setMockSyncResponse(utf8.encode('line1\nline2\r\nline3')); // readBytes
|
||||||
|
expect(file.readAsLinesSync(), equals(['line1', 'line2', 'line3']));
|
||||||
|
|
||||||
|
// deleteSync
|
||||||
|
setMockSyncResponse(utf8.encode('file')); // typeSync(path)
|
||||||
|
setMockSyncResponse([]); // delete
|
||||||
|
file.deleteSync();
|
||||||
|
expect(getLastSyncCmd(), equals(6));
|
||||||
|
|
||||||
|
// statSync
|
||||||
|
final statJson = json.encode({
|
||||||
|
'type': 'file',
|
||||||
|
'size': 1500,
|
||||||
|
'modified': 1718100000000,
|
||||||
|
});
|
||||||
|
setMockSyncResponse(utf8.encode(statJson));
|
||||||
|
final stat = file.statSync();
|
||||||
|
expect(getLastSyncCmd(), equals(9));
|
||||||
|
expect(stat.type, equals(FileSystemEntityType.file));
|
||||||
|
expect(stat.size, equals(1500));
|
||||||
|
expect(stat.modified.millisecondsSinceEpoch, equals(1718100000000));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('directory sync APIs', () {
|
||||||
|
final dir = fs.directory('/sync_dir');
|
||||||
|
|
||||||
|
// createSync
|
||||||
|
setMockSyncResponse(utf8.encode('notFound')); // existsSync
|
||||||
|
setMockSyncResponse(utf8.encode('directory')); // parent typeSync
|
||||||
|
setMockSyncResponse([]); // createDir
|
||||||
|
dir.createSync();
|
||||||
|
expect(getLastSyncCmd(), equals(5));
|
||||||
|
|
||||||
|
// deleteSync
|
||||||
|
setMockSyncResponse(utf8.encode('directory')); // typeSync(path) inside deleteSync
|
||||||
|
setMockSyncResponse(utf8.encode('directory')); // typeSync(path) inside listSync -> existsSync
|
||||||
|
setMockSyncResponse(utf8.encode('[]')); // listSync
|
||||||
|
setMockSyncResponse([]); // delete
|
||||||
|
dir.deleteSync();
|
||||||
|
expect(getLastSyncCmd(), equals(6));
|
||||||
|
|
||||||
|
// listSync
|
||||||
|
final listData = json.encode([
|
||||||
|
{'path': '/sync_dir/file.txt', 'type': 'file'},
|
||||||
|
{'path': '/sync_dir/sub', 'type': 'directory'},
|
||||||
|
{'path': '/sync_dir/link', 'type': 'link'},
|
||||||
|
]);
|
||||||
|
setMockSyncResponse(utf8.encode('directory')); // listSync -> existsSync
|
||||||
|
setMockSyncResponse(utf8.encode(listData)); // listSync
|
||||||
|
final list = dir.listSync(followLinks: false);
|
||||||
|
expect(getLastSyncCmd(), equals(10));
|
||||||
|
expect(list.length, equals(3));
|
||||||
|
expect(list[0], isA<File>());
|
||||||
|
expect(list[0].path, equals('/sync_dir/file.txt'));
|
||||||
|
expect(list[1], isA<Directory>());
|
||||||
|
expect(list[2], isA<Link>());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('link sync APIs', () {
|
||||||
|
final link = fs.link('/sync_link');
|
||||||
|
|
||||||
|
// createSync
|
||||||
|
setMockSyncResponse(utf8.encode('notFound')); // existsSync
|
||||||
|
setMockSyncResponse(utf8.encode('directory')); // parent typeSync
|
||||||
|
setMockSyncResponse([]); // createLink
|
||||||
|
link.createSync('/target_path');
|
||||||
|
expect(getLastSyncCmd(), equals(7));
|
||||||
|
|
||||||
|
// updateSync
|
||||||
|
setMockSyncResponse(utf8.encode('link')); // typeSync check
|
||||||
|
setMockSyncResponse([]); // updateLink
|
||||||
|
link.updateSync('/new_target');
|
||||||
|
expect(getLastSyncCmd(), equals(13));
|
||||||
|
|
||||||
|
// targetSync
|
||||||
|
setMockSyncResponse(utf8.encode('link')); // typeSync check
|
||||||
|
setMockSyncResponse(utf8.encode('/resolved_target')); // readLink
|
||||||
|
expect(link.targetSync(), equals('/resolved_target'));
|
||||||
|
expect(getLastSyncCmd(), equals(8));
|
||||||
|
|
||||||
|
// deleteSync
|
||||||
|
setMockSyncResponse(utf8.encode('link')); // typeSync check
|
||||||
|
setMockSyncResponse([]); // delete
|
||||||
|
link.deleteSync();
|
||||||
|
expect(getLastSyncCmd(), equals(6));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sync error paths and branch coverage', () {
|
||||||
|
// 1. SharedArrayBuffer unsupported
|
||||||
|
jsEval('window.SharedArrayBuffer = undefined;');
|
||||||
|
expect(() => fs.typeSync('/'), throwsStateError);
|
||||||
|
expect(() => fs.statSync('/'), throwsStateError);
|
||||||
|
jsEval('window.SharedArrayBuffer = function() {};'); // restore
|
||||||
|
|
||||||
|
// 2. typeSync catch block
|
||||||
|
jsEval('''
|
||||||
|
window.originalSendVFSSyncRequest = window.sendVFSSyncRequest;
|
||||||
|
window.sendVFSSyncRequest = function() { throw new Error("mock error"); };
|
||||||
|
''');
|
||||||
|
expect(fs.typeSync('/'), equals(FileSystemEntityType.notFound));
|
||||||
|
expect(fs.statSync('/').type, equals(FileSystemEntityType.notFound));
|
||||||
|
jsEval('window.sendVFSSyncRequest = window.originalSendVFSSyncRequest;'); // restore
|
||||||
|
|
||||||
|
// 3. statSync type branches (directory, link, other/notFound)
|
||||||
|
// directory
|
||||||
|
setMockSyncResponse(utf8.encode(json.encode({'type': 'directory', 'size': 0, 'modified': 123})));
|
||||||
|
expect(fs.statSync('/dir').type, equals(FileSystemEntityType.directory));
|
||||||
|
// link
|
||||||
|
setMockSyncResponse(utf8.encode(json.encode({'type': 'link', 'size': 0, 'modified': 123})));
|
||||||
|
expect(fs.statSync('/link').type, equals(FileSystemEntityType.link));
|
||||||
|
// other
|
||||||
|
setMockSyncResponse(utf8.encode(json.encode({'type': 'unknown', 'size': 0, 'modified': 123})));
|
||||||
|
expect(fs.statSync('/unknown').type, equals(FileSystemEntityType.notFound));
|
||||||
|
|
||||||
|
// 4. WebDirectory createSync branches
|
||||||
|
final dir = fs.directory('/sync_dir_cov');
|
||||||
|
// Already exists
|
||||||
|
setMockSyncResponse(utf8.encode('directory')); // existsSync
|
||||||
|
dir.createSync(); // Should return early without exception
|
||||||
|
|
||||||
|
// Parent notFound, recursive: true
|
||||||
|
setMockSyncResponse(utf8.encode('notFound')); // child existsSync
|
||||||
|
setMockSyncResponse(utf8.encode('notFound')); // parent typeSync
|
||||||
|
setMockSyncResponse(utf8.encode('notFound')); // parent existsSync
|
||||||
|
setMockSyncResponse(utf8.encode('directory')); // parent-parent typeSync
|
||||||
|
setMockSyncResponse([]); // parent createDir
|
||||||
|
setMockSyncResponse([]); // child createDir
|
||||||
|
dir.createSync(recursive: true);
|
||||||
|
|
||||||
|
// Parent notFound, recursive: false throws
|
||||||
|
setMockSyncResponse(utf8.encode('notFound')); // existsSync
|
||||||
|
setMockSyncResponse(utf8.encode('notFound')); // parent typeSync
|
||||||
|
expect(() => dir.createSync(recursive: false), throwsA(isA<FileSystemException>()));
|
||||||
|
|
||||||
|
// Parent is file throws
|
||||||
|
setMockSyncResponse(utf8.encode('notFound')); // existsSync
|
||||||
|
setMockSyncResponse(utf8.encode('file')); // parent typeSync
|
||||||
|
expect(() => dir.createSync(), throwsA(isA<FileSystemException>()));
|
||||||
|
|
||||||
|
// createTempSync when directory notFound throws
|
||||||
|
setMockSyncResponse(utf8.encode('notFound')); // existsSync
|
||||||
|
expect(() => dir.createTempSync(), throwsA(isA<FileSystemException>()));
|
||||||
|
|
||||||
|
// createTempSync successful path
|
||||||
|
setMockSyncResponse(utf8.encode('directory')); // existsSync
|
||||||
|
setMockSyncResponse(utf8.encode('notFound')); // temp child existsSync
|
||||||
|
setMockSyncResponse(utf8.encode('directory')); // temp parent typeSync
|
||||||
|
setMockSyncResponse([]); // temp createDir
|
||||||
|
final tempDir = dir.createTempSync('foo');
|
||||||
|
expect(tempDir.path, contains('foo'));
|
||||||
|
|
||||||
|
// deleteSync path notFound throws
|
||||||
|
setMockSyncResponse(utf8.encode('notFound')); // typeSync
|
||||||
|
expect(() => dir.deleteSync(), throwsA(isA<FileSystemException>()));
|
||||||
|
|
||||||
|
// deleteSync path is file throws
|
||||||
|
setMockSyncResponse(utf8.encode('file')); // typeSync
|
||||||
|
expect(() => dir.deleteSync(), throwsA(isA<FileSystemException>()));
|
||||||
|
|
||||||
|
// deleteSync not empty throws
|
||||||
|
setMockSyncResponse(utf8.encode('directory')); // typeSync
|
||||||
|
setMockSyncResponse(utf8.encode('directory')); // existsSync inside listSync
|
||||||
|
setMockSyncResponse(utf8.encode(json.encode([{'path': '/sub/a', 'type': 'file'}]))); // listSync
|
||||||
|
expect(() => dir.deleteSync(), throwsA(isA<FileSystemException>()));
|
||||||
|
|
||||||
|
// listSync path notFound throws
|
||||||
|
setMockSyncResponse(utf8.encode('notFound')); // existsSync
|
||||||
|
expect(() => dir.listSync(), throwsA(isA<FileSystemException>()));
|
||||||
|
|
||||||
|
// listSync recursive containing a directory
|
||||||
|
setMockSyncResponse(utf8.encode('directory')); // existsSync
|
||||||
|
setMockSyncResponse(utf8.encode(json.encode([{'path': '/sync_dir_cov/sub', 'type': 'directory'}]))); // listSync level 1
|
||||||
|
setMockSyncResponse(utf8.encode('directory')); // existsSync recursive
|
||||||
|
setMockSyncResponse(utf8.encode('[]')); // listSync level 2
|
||||||
|
final listRec = dir.listSync(recursive: true);
|
||||||
|
expect(listRec.length, equals(1));
|
||||||
|
|
||||||
|
// listSync following link target to directory
|
||||||
|
setMockSyncResponse(utf8.encode('directory')); // existsSync
|
||||||
|
setMockSyncResponse(utf8.encode(json.encode([{'path': '/sync_dir_cov/lnk', 'type': 'link'}]))); // listSync
|
||||||
|
setMockSyncResponse(utf8.encode('directory')); // typeSync for link target
|
||||||
|
final listLnkDir = dir.listSync(followLinks: true);
|
||||||
|
expect(listLnkDir.length, equals(1));
|
||||||
|
expect(listLnkDir[0], isA<Directory>());
|
||||||
|
|
||||||
|
// listSync following link target to file (non-directory)
|
||||||
|
setMockSyncResponse(utf8.encode('directory')); // existsSync
|
||||||
|
setMockSyncResponse(utf8.encode(json.encode([{'path': '/sync_dir_cov/lnk', 'type': 'link'}]))); // listSync
|
||||||
|
setMockSyncResponse(utf8.encode('file')); // typeSync for link target
|
||||||
|
final listLnkFile = dir.listSync(followLinks: true);
|
||||||
|
expect(listLnkFile.length, equals(1));
|
||||||
|
expect(listLnkFile[0], isA<File>());
|
||||||
|
|
||||||
|
// listSync following link target throws/notFound
|
||||||
|
setMockSyncResponse(utf8.encode('directory')); // existsSync
|
||||||
|
setMockSyncResponse(utf8.encode(json.encode([{'path': '/sync_dir_cov/lnk', 'type': 'link'}]))); // listSync
|
||||||
|
setMockSyncResponse(utf8.encode('notFound')); // typeSync for link target
|
||||||
|
final listLnkNotFound = dir.listSync(followLinks: true);
|
||||||
|
expect(listLnkNotFound.length, equals(1));
|
||||||
|
expect(listLnkNotFound[0], isA<Link>());
|
||||||
|
|
||||||
|
// listSync recursive following link target to directory
|
||||||
|
setMockSyncResponse(utf8.encode('directory')); // existsSync
|
||||||
|
setMockSyncResponse(utf8.encode(json.encode([{'path': '/sync_dir_cov/lnk', 'type': 'link'}]))); // listSync level 1
|
||||||
|
setMockSyncResponse(utf8.encode('directory')); // typeSync for link target
|
||||||
|
setMockSyncResponse(utf8.encode('directory')); // existsSync recursive
|
||||||
|
setMockSyncResponse(utf8.encode('[]')); // listSync level 2
|
||||||
|
final listRecLnkDir = dir.listSync(recursive: true, followLinks: true);
|
||||||
|
expect(listRecLnkDir.length, equals(1));
|
||||||
|
expect(listRecLnkDir[0], isA<Directory>());
|
||||||
|
|
||||||
|
// listSync following link target where typeSync throws (covered via SAB disabled mid-run)
|
||||||
|
jsEval('''
|
||||||
|
window.originalSendVFSSyncRequest = window.sendVFSSyncRequest;
|
||||||
|
window.sendVFSSyncRequest = function(cmd, req) {
|
||||||
|
if (cmd === 10) {
|
||||||
|
window.SharedArrayBuffer = undefined; // disable SAB
|
||||||
|
const listData = JSON.stringify([{'path': '/sync_dir_cov/lnk', 'type': 'link'}]);
|
||||||
|
const bytes = new TextEncoder().encode(listData);
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
return window.originalSendVFSSyncRequest(cmd, req);
|
||||||
|
};
|
||||||
|
''');
|
||||||
|
setMockSyncResponse(utf8.encode('directory')); // existsSync
|
||||||
|
// listSync response is provided by custom sendVFSSyncRequest function
|
||||||
|
final listLnkThrow = dir.listSync(followLinks: true);
|
||||||
|
expect(listLnkThrow.length, equals(1));
|
||||||
|
expect(listLnkThrow[0], isA<Link>());
|
||||||
|
jsEval('''
|
||||||
|
window.sendVFSSyncRequest = window.originalSendVFSSyncRequest;
|
||||||
|
window.SharedArrayBuffer = function() {}; // restore SAB
|
||||||
|
''');
|
||||||
|
|
||||||
|
// makeSyncCall SharedArrayBuffer unsupported
|
||||||
|
jsEval('window.SharedArrayBuffer = undefined;');
|
||||||
|
expect(() => dir.resolveSymbolicLinksSync(), throwsStateError);
|
||||||
|
jsEval('window.SharedArrayBuffer = function() {};'); // restore
|
||||||
|
|
||||||
|
// renameSync original path not found throws
|
||||||
|
setMockSyncResponse(utf8.encode('notFound')); // typeSync
|
||||||
|
expect(() => dir.renameSync('/new'), throwsA(isA<FileSystemException>()));
|
||||||
|
|
||||||
|
// renameSync new parent not directory throws
|
||||||
|
setMockSyncResponse(utf8.encode('directory')); // typeSync self
|
||||||
|
setMockSyncResponse(utf8.encode('file')); // typeSync parent
|
||||||
|
expect(() => dir.renameSync('/parent_file/new'), throwsA(isA<FileSystemException>()));
|
||||||
|
|
||||||
|
// renameSync successful
|
||||||
|
setMockSyncResponse(utf8.encode('directory')); // typeSync self
|
||||||
|
setMockSyncResponse(utf8.encode('directory')); // typeSync parent
|
||||||
|
setMockSyncResponse([]); // rename
|
||||||
|
final renamedDir = dir.renameSync('/new_parent/new_dir');
|
||||||
|
expect(renamedDir.path, equals('/new_parent/new_dir'));
|
||||||
|
|
||||||
|
// resolveSymbolicLinksSync
|
||||||
|
setMockSyncResponse(utf8.encode('/resolved/path'));
|
||||||
|
expect(dir.resolveSymbolicLinksSync(), equals('/resolved/path'));
|
||||||
|
|
||||||
|
// 5. WebFile sync branches
|
||||||
|
final file = fs.file('/sync_file_cov');
|
||||||
|
// createSync already exists, exclusive: true throws
|
||||||
|
setMockSyncResponse(utf8.encode('file')); // existsSync
|
||||||
|
expect(() => file.createSync(exclusive: true), throwsA(isA<FileSystemException>()));
|
||||||
|
|
||||||
|
// createSync already exists, exclusive: false returns early
|
||||||
|
setMockSyncResponse(utf8.encode('file')); // existsSync
|
||||||
|
file.createSync(exclusive: false); // returns early
|
||||||
|
|
||||||
|
// createSync parent notFound, recursive: false throws
|
||||||
|
setMockSyncResponse(utf8.encode('notFound')); // existsSync
|
||||||
|
setMockSyncResponse(utf8.encode('notFound')); // parent typeSync
|
||||||
|
expect(() => file.createSync(), throwsA(isA<FileSystemException>()));
|
||||||
|
|
||||||
|
// createSync parent notFound, recursive: true
|
||||||
|
setMockSyncResponse(utf8.encode('notFound')); // existsSync
|
||||||
|
setMockSyncResponse(utf8.encode('notFound')); // parent typeSync
|
||||||
|
setMockSyncResponse(utf8.encode('notFound')); // parent existsSync
|
||||||
|
setMockSyncResponse(utf8.encode('directory')); // parent-parent typeSync
|
||||||
|
setMockSyncResponse([]); // parent createDir
|
||||||
|
setMockSyncResponse(utf8.encode('directory')); // parent check for writeBytes
|
||||||
|
setMockSyncResponse([]); // writeBytes (create file)
|
||||||
|
file.createSync(recursive: true);
|
||||||
|
|
||||||
|
// createSync parent is file throws
|
||||||
|
setMockSyncResponse(utf8.encode('notFound')); // existsSync
|
||||||
|
setMockSyncResponse(utf8.encode('file')); // parent typeSync
|
||||||
|
expect(() => file.createSync(), throwsA(isA<FileSystemException>()));
|
||||||
|
|
||||||
|
// writeAsBytesSync FileMode.append throws UnsupportedError
|
||||||
|
expect(() => file.writeAsBytesSync([], mode: FileMode.append), throwsUnsupportedError);
|
||||||
|
|
||||||
|
// writeAsBytesSync parent not directory throws
|
||||||
|
setMockSyncResponse(utf8.encode('file')); // parent typeSync
|
||||||
|
expect(() => file.writeAsBytesSync([]), throwsA(isA<FileSystemException>()));
|
||||||
|
|
||||||
|
// readAsBytesSync path notFound throws
|
||||||
|
setMockSyncResponse(utf8.encode('notFound')); // typeSync
|
||||||
|
expect(() => file.readAsBytesSync(), throwsA(isA<FileSystemException>()));
|
||||||
|
|
||||||
|
// readAsBytesSync path is directory throws
|
||||||
|
setMockSyncResponse(utf8.encode('directory')); // typeSync
|
||||||
|
expect(() => file.readAsBytesSync(), throwsA(isA<FileSystemException>()));
|
||||||
|
|
||||||
|
// renameSync path notFound throws
|
||||||
|
setMockSyncResponse(utf8.encode('notFound')); // typeSync self
|
||||||
|
expect(() => file.renameSync('/new'), throwsA(isA<FileSystemException>()));
|
||||||
|
|
||||||
|
// renameSync parent not directory throws
|
||||||
|
setMockSyncResponse(utf8.encode('file')); // typeSync self
|
||||||
|
setMockSyncResponse(utf8.encode('file')); // typeSync parent
|
||||||
|
expect(() => file.renameSync('/parent_file/new'), throwsA(isA<FileSystemException>()));
|
||||||
|
|
||||||
|
// renameSync successful
|
||||||
|
setMockSyncResponse(utf8.encode('file')); // typeSync self
|
||||||
|
setMockSyncResponse(utf8.encode('directory')); // typeSync parent
|
||||||
|
setMockSyncResponse([]); // rename
|
||||||
|
final renamedFile = file.renameSync('/new_parent/new_file');
|
||||||
|
expect(renamedFile.path, equals('/new_parent/new_file'));
|
||||||
|
|
||||||
|
// deleteSync path notFound throws
|
||||||
|
setMockSyncResponse(utf8.encode('notFound')); // typeSync self
|
||||||
|
expect(() => file.deleteSync(), throwsA(isA<FileSystemException>()));
|
||||||
|
|
||||||
|
// 6. WebLink sync branches
|
||||||
|
final link = fs.link('/sync_link_cov');
|
||||||
|
// createSync already exists throws
|
||||||
|
setMockSyncResponse(utf8.encode('link')); // existsSync
|
||||||
|
expect(() => link.createSync('/target'), throwsA(isA<FileSystemException>()));
|
||||||
|
|
||||||
|
// createSync parent notFound, recursive: false throws
|
||||||
|
setMockSyncResponse(utf8.encode('notFound')); // existsSync
|
||||||
|
setMockSyncResponse(utf8.encode('notFound')); // parent typeSync
|
||||||
|
expect(() => link.createSync('/target'), throwsA(isA<FileSystemException>()));
|
||||||
|
|
||||||
|
// createSync parent notFound, recursive: true
|
||||||
|
setMockSyncResponse(utf8.encode('notFound')); // existsSync
|
||||||
|
setMockSyncResponse(utf8.encode('notFound')); // parent typeSync
|
||||||
|
setMockSyncResponse(utf8.encode('notFound')); // parent existsSync
|
||||||
|
setMockSyncResponse(utf8.encode('directory')); // parent-parent typeSync
|
||||||
|
setMockSyncResponse([]); // parent createDir
|
||||||
|
setMockSyncResponse([]); // createLink
|
||||||
|
link.createSync('/target', recursive: true);
|
||||||
|
|
||||||
|
// createSync parent is file throws
|
||||||
|
setMockSyncResponse(utf8.encode('notFound')); // existsSync
|
||||||
|
setMockSyncResponse(utf8.encode('file')); // parent typeSync
|
||||||
|
expect(() => link.createSync('/target'), throwsA(isA<FileSystemException>()));
|
||||||
|
|
||||||
|
// updateSync path notFound throws
|
||||||
|
setMockSyncResponse(utf8.encode('notFound')); // typeSync self
|
||||||
|
expect(() => link.updateSync('/new'), throwsA(isA<FileSystemException>()));
|
||||||
|
|
||||||
|
// updateSync path is not link throws
|
||||||
|
setMockSyncResponse(utf8.encode('file')); // typeSync self
|
||||||
|
expect(() => link.updateSync('/new'), throwsA(isA<FileSystemException>()));
|
||||||
|
|
||||||
|
// targetSync path notFound throws
|
||||||
|
setMockSyncResponse(utf8.encode('notFound')); // typeSync self
|
||||||
|
expect(() => link.targetSync(), throwsA(isA<FileSystemException>()));
|
||||||
|
|
||||||
|
// targetSync path is not link throws
|
||||||
|
setMockSyncResponse(utf8.encode('file')); // typeSync self
|
||||||
|
expect(() => link.targetSync(), throwsA(isA<FileSystemException>()));
|
||||||
|
|
||||||
|
// renameSync path notFound throws
|
||||||
|
setMockSyncResponse(utf8.encode('notFound')); // typeSync self
|
||||||
|
expect(() => link.renameSync('/new'), throwsA(isA<FileSystemException>()));
|
||||||
|
|
||||||
|
// renameSync parent not directory throws
|
||||||
|
setMockSyncResponse(utf8.encode('link')); // typeSync self
|
||||||
|
setMockSyncResponse(utf8.encode('file')); // typeSync parent
|
||||||
|
expect(() => link.renameSync('/parent_file/new'), throwsA(isA<FileSystemException>()));
|
||||||
|
|
||||||
|
// renameSync successful
|
||||||
|
setMockSyncResponse(utf8.encode('link')); // typeSync self
|
||||||
|
setMockSyncResponse(utf8.encode('directory')); // typeSync parent
|
||||||
|
setMockSyncResponse([]); // rename
|
||||||
|
final renamedLink = link.renameSync('/new_parent/new_link');
|
||||||
|
expect(renamedLink.path, equals('/new_parent/new_link'));
|
||||||
|
|
||||||
|
// deleteSync path notFound throws
|
||||||
|
setMockSyncResponse(utf8.encode('notFound')); // typeSync self
|
||||||
|
expect(() => link.deleteSync(), throwsA(isA<FileSystemException>()));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@JS('eval')
|
@JS('eval')
|
||||||
@@ -591,3 +1079,64 @@ void setMockIDBPutShouldFail(bool value) {
|
|||||||
void setMockIDBIndexGetShouldFail(bool value) {
|
void setMockIDBIndexGetShouldFail(bool value) {
|
||||||
jsEval('window.mockIDBIndexGetShouldFail = $value;');
|
jsEval('window.mockIDBIndexGetShouldFail = $value;');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setupSyncMockJS() {
|
||||||
|
jsEval('''
|
||||||
|
window.originalImportScripts = window.importScripts;
|
||||||
|
window.importScripts = function() {};
|
||||||
|
window.originalSharedArrayBuffer = window.SharedArrayBuffer;
|
||||||
|
window.SharedArrayBuffer = function() {};
|
||||||
|
window.isVFSSyncWorkerInitialized = true;
|
||||||
|
window.initVFSSyncWorker = function() {};
|
||||||
|
|
||||||
|
window.lastSyncCmd = null;
|
||||||
|
window.lastSyncPayload = null;
|
||||||
|
window.mockSyncResponseArray = [];
|
||||||
|
window.syncCmdsHistory = [];
|
||||||
|
|
||||||
|
window.sendVFSSyncRequest = function(cmd, requestBytes) {
|
||||||
|
window.lastSyncCmd = cmd;
|
||||||
|
window.syncCmdsHistory.push(cmd);
|
||||||
|
window.lastSyncPayload = Array.from(requestBytes);
|
||||||
|
const resp = window.mockSyncResponseArray.shift() || new Uint8Array(0);
|
||||||
|
return resp;
|
||||||
|
};
|
||||||
|
''');
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearSyncMockJS() {
|
||||||
|
jsEval('''
|
||||||
|
window.importScripts = window.originalImportScripts;
|
||||||
|
window.SharedArrayBuffer = window.originalSharedArrayBuffer;
|
||||||
|
delete window.sendVFSSyncRequest;
|
||||||
|
delete window.lastSyncCmd;
|
||||||
|
delete window.lastSyncPayload;
|
||||||
|
delete window.mockSyncResponseArray;
|
||||||
|
delete window.syncCmdsHistory;
|
||||||
|
delete window.isVFSSyncWorkerInitialized;
|
||||||
|
delete window.initVFSSyncWorker;
|
||||||
|
''');
|
||||||
|
}
|
||||||
|
|
||||||
|
List<int> getSyncCmdsHistory() {
|
||||||
|
final jsVal = jsEval('JSON.stringify(window.syncCmdsHistory || [])');
|
||||||
|
if (jsVal == null) return [];
|
||||||
|
return List<int>.from(json.decode((jsVal as JSString).toDart));
|
||||||
|
}
|
||||||
|
|
||||||
|
int? getLastSyncCmd() {
|
||||||
|
final jsVal = jsEval('window.lastSyncCmd');
|
||||||
|
if (jsVal == null) return null;
|
||||||
|
return (jsVal as JSNumber).toDartInt;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<int>? getLastSyncPayload() {
|
||||||
|
final jsVal = jsEval('window.lastSyncPayload ? JSON.stringify(window.lastSyncPayload) : null');
|
||||||
|
if (jsVal == null) return null;
|
||||||
|
return List<int>.from(json.decode((jsVal as JSString).toDart));
|
||||||
|
}
|
||||||
|
|
||||||
|
void setMockSyncResponse(List<int> bytes) {
|
||||||
|
final jsonBytes = json.encode(bytes);
|
||||||
|
jsEval('window.mockSyncResponseArray.push(new Uint8Array($jsonBytes));');
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user