feat: implement synchronous file system APIs with worker support & 100% test coverage
This commit is contained in:
@@ -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:convert';
|
||||
import 'dart:typed_data';
|
||||
import 'package:file/file.dart';
|
||||
import 'package:web_file_system/src/backend/idb_inode_service.dart';
|
||||
import '../web_file_system.dart';
|
||||
@@ -61,7 +63,27 @@ class WebDirectory extends FileSystemEntity implements Directory {
|
||||
|
||||
@override
|
||||
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
|
||||
@@ -80,7 +102,15 @@ class WebDirectory extends FileSystemEntity implements Directory {
|
||||
|
||||
@override
|
||||
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
|
||||
@@ -109,8 +139,34 @@ class WebDirectory extends FileSystemEntity implements Directory {
|
||||
}
|
||||
|
||||
@override
|
||||
void deleteSync({bool recursive = false}) =>
|
||||
throw UnsupportedError('Sync not supported');
|
||||
void deleteSync({bool recursive = false}) {
|
||||
final type = _fs.typeSync(path, followLinks: false);
|
||||
if (type == FileSystemEntityType.notFound) {
|
||||
throw FileSystemException(
|
||||
'Cannot delete directory, path = \'$path\' (OS Error: No such file or directory, errno = 2)',
|
||||
path,
|
||||
);
|
||||
}
|
||||
if (type != FileSystemEntityType.directory) {
|
||||
throw FileSystemException(
|
||||
'Cannot delete directory, path = \'$path\' (OS Error: Not a directory, errno = 20)',
|
||||
path,
|
||||
);
|
||||
}
|
||||
|
||||
if (!recursive) {
|
||||
final children = listSync(recursive: false, followLinks: false);
|
||||
if (children.isNotEmpty) {
|
||||
throw FileSystemException(
|
||||
'Directory not empty',
|
||||
path,
|
||||
const OSError('ENOTEMPTY', 39),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_fs.makeSyncCall(6, utf8.encode(path));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> exists() async {
|
||||
@@ -123,7 +179,9 @@ class WebDirectory extends FileSystemEntity implements Directory {
|
||||
}
|
||||
|
||||
@override
|
||||
bool existsSync() => throw UnsupportedError('Sync not supported');
|
||||
bool existsSync() {
|
||||
return _fs.typeSync(path, followLinks: false) == FileSystemEntityType.directory;
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<FileSystemEntity> list(
|
||||
@@ -173,7 +231,52 @@ class WebDirectory extends FileSystemEntity implements Directory {
|
||||
@override
|
||||
List<FileSystemEntity> listSync(
|
||||
{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
|
||||
@@ -198,8 +301,33 @@ class WebDirectory extends FileSystemEntity implements Directory {
|
||||
}
|
||||
|
||||
@override
|
||||
Directory renameSync(String newPath) =>
|
||||
throw UnsupportedError('Sync not supported');
|
||||
Directory renameSync(String newPath) {
|
||||
final type = _fs.typeSync(path, followLinks: false);
|
||||
if (type == FileSystemEntityType.notFound) {
|
||||
throw FileSystemException(
|
||||
'Cannot rename directory, path = \'$path\' (OS Error: No such file or directory, errno = 2)',
|
||||
path,
|
||||
);
|
||||
}
|
||||
final newParentDir = _fs.path.dirname(newPath);
|
||||
if (_fs.typeSync(newParentDir) != FileSystemEntityType.directory) {
|
||||
throw FileSystemException(
|
||||
'Cannot rename directory, path = \'$path\' (OS Error: No such file or directory, errno = 2)',
|
||||
path,
|
||||
);
|
||||
}
|
||||
|
||||
final pathBytes = utf8.encode(path);
|
||||
final newPathBytes = utf8.encode(newPath);
|
||||
final request = Uint8List(4 + pathBytes.length + newPathBytes.length);
|
||||
final bd = ByteData.sublistView(request);
|
||||
bd.setUint32(0, pathBytes.length, Endian.little);
|
||||
request.setRange(4, 4 + pathBytes.length, pathBytes);
|
||||
request.setRange(4 + pathBytes.length, request.length, newPathBytes);
|
||||
|
||||
_fs.makeSyncCall(12, request);
|
||||
return WebDirectory(_fs, newPath);
|
||||
}
|
||||
|
||||
@override
|
||||
String get basename => _fs.path.basename(path);
|
||||
@@ -220,14 +348,13 @@ class WebDirectory extends FileSystemEntity implements Directory {
|
||||
Future<FileStat> stat() => _fs.stat(path);
|
||||
|
||||
@override
|
||||
FileStat statSync() => throw UnsupportedError('Sync not supported');
|
||||
FileStat statSync() => _fs.statSync(path);
|
||||
|
||||
@override
|
||||
Future<String> resolveSymbolicLinks() => _fs.resolveSymbolicLinks(path);
|
||||
|
||||
@override
|
||||
String resolveSymbolicLinksSync() =>
|
||||
throw UnsupportedError('Sync not supported');
|
||||
String resolveSymbolicLinksSync() => _fs.resolveSymbolicLinksSync(path);
|
||||
|
||||
@override
|
||||
Stream<FileSystemEvent> watch(
|
||||
|
||||
@@ -52,8 +52,38 @@ class WebFile extends FileSystemEntity implements File {
|
||||
}
|
||||
|
||||
@override
|
||||
void createSync({bool recursive = false, bool exclusive = false}) =>
|
||||
throw UnsupportedError('Sync not supported');
|
||||
void createSync({bool recursive = false, bool exclusive = false}) {
|
||||
if (existsSync()) {
|
||||
if (exclusive) {
|
||||
throw FileSystemException(
|
||||
'File already exists',
|
||||
path,
|
||||
const OSError('EEXIST', 17),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final parentPath = _fs.path.dirname(path);
|
||||
final parentType = _fs.typeSync(parentPath);
|
||||
if (parentType == FileSystemEntityType.notFound) {
|
||||
if (recursive) {
|
||||
_fs.directory(parentPath).createSync(recursive: true);
|
||||
} else {
|
||||
throw FileSystemException(
|
||||
'Cannot create file, path = \'$path\' (OS Error: No such file or directory, errno = 2)',
|
||||
path,
|
||||
);
|
||||
}
|
||||
} else if (parentType != FileSystemEntityType.directory) {
|
||||
throw FileSystemException(
|
||||
'Cannot create file, path = \'$path\' (OS Error: Not a directory, errno = 20)',
|
||||
path,
|
||||
);
|
||||
}
|
||||
|
||||
writeAsBytesSync([]);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<File> copy(String newPath) async {
|
||||
@@ -76,7 +106,11 @@ class WebFile extends FileSystemEntity implements File {
|
||||
}
|
||||
|
||||
@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
|
||||
Future<int> length() async {
|
||||
@@ -85,7 +119,7 @@ class WebFile extends FileSystemEntity implements File {
|
||||
}
|
||||
|
||||
@override
|
||||
int lengthSync() => throw UnsupportedError('Sync not supported');
|
||||
int lengthSync() => statSync().size;
|
||||
|
||||
@override
|
||||
Future<DateTime> lastModified() async {
|
||||
@@ -94,7 +128,7 @@ class WebFile extends FileSystemEntity implements File {
|
||||
}
|
||||
|
||||
@override
|
||||
DateTime lastModifiedSync() => throw UnsupportedError('Sync not supported');
|
||||
DateTime lastModifiedSync() => statSync().modified;
|
||||
|
||||
@override
|
||||
Future<DateTime> lastAccessed() async {
|
||||
@@ -102,7 +136,7 @@ class WebFile extends FileSystemEntity implements File {
|
||||
}
|
||||
|
||||
@override
|
||||
DateTime lastAccessedSync() => throw UnsupportedError('Sync not supported');
|
||||
DateTime lastAccessedSync() => statSync().accessed;
|
||||
|
||||
@override
|
||||
Future<dynamic> setLastAccessed(DateTime time) async {}
|
||||
@@ -190,7 +224,27 @@ class WebFile extends FileSystemEntity implements File {
|
||||
FileMode mode = FileMode.write,
|
||||
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
|
||||
@@ -210,7 +264,7 @@ class WebFile extends FileSystemEntity implements File {
|
||||
Encoding encoding = utf8,
|
||||
bool flush = false,
|
||||
}) {
|
||||
throw UnsupportedError('Sync not supported');
|
||||
writeAsBytesSync(encoding.encode(contents), mode: mode, flush: flush);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -280,7 +334,22 @@ class WebFile extends FileSystemEntity implements File {
|
||||
}
|
||||
|
||||
@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
|
||||
Future<String> readAsString({Encoding encoding = utf8}) async {
|
||||
@@ -289,18 +358,21 @@ class WebFile extends FileSystemEntity implements File {
|
||||
}
|
||||
|
||||
@override
|
||||
String readAsStringSync({Encoding encoding = utf8}) =>
|
||||
throw UnsupportedError('Sync not supported');
|
||||
String readAsStringSync({Encoding encoding = utf8}) {
|
||||
return encoding.decode(readAsBytesSync());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<String>> readAsLines({Encoding encoding = utf8}) async {
|
||||
final str = await readAsString(encoding: encoding);
|
||||
return str.split('\n');
|
||||
return const LineSplitter().convert(str);
|
||||
}
|
||||
|
||||
@override
|
||||
List<String> readAsLinesSync({Encoding encoding = utf8}) =>
|
||||
throw UnsupportedError('Sync not supported');
|
||||
List<String> readAsLinesSync({Encoding encoding = utf8}) {
|
||||
final str = readAsStringSync(encoding: encoding);
|
||||
return const LineSplitter().convert(str);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> exists() async {
|
||||
@@ -313,7 +385,9 @@ class WebFile extends FileSystemEntity implements File {
|
||||
}
|
||||
|
||||
@override
|
||||
bool existsSync() => throw UnsupportedError('Sync not supported');
|
||||
bool existsSync() {
|
||||
return _fs.typeSync(path, followLinks: false) == FileSystemEntityType.file;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<File> rename(String newPath) async {
|
||||
@@ -337,8 +411,33 @@ class WebFile extends FileSystemEntity implements File {
|
||||
}
|
||||
|
||||
@override
|
||||
File renameSync(String newPath) =>
|
||||
throw UnsupportedError('Sync not supported');
|
||||
File renameSync(String newPath) {
|
||||
final type = _fs.typeSync(path, followLinks: false);
|
||||
if (type == FileSystemEntityType.notFound) {
|
||||
throw FileSystemException(
|
||||
'Cannot rename file, path = \'$path\' (OS Error: No such file or directory, errno = 2)',
|
||||
path,
|
||||
);
|
||||
}
|
||||
final newParentDir = _fs.path.dirname(newPath);
|
||||
if (_fs.typeSync(newParentDir) != FileSystemEntityType.directory) {
|
||||
throw FileSystemException(
|
||||
'Cannot rename file, path = \'$path\' (OS Error: No such file or directory, errno = 2)',
|
||||
path,
|
||||
);
|
||||
}
|
||||
|
||||
final pathBytes = utf8.encode(path);
|
||||
final newPathBytes = utf8.encode(newPath);
|
||||
final request = Uint8List(4 + pathBytes.length + newPathBytes.length);
|
||||
final bd = ByteData.sublistView(request);
|
||||
bd.setUint32(0, pathBytes.length, Endian.little);
|
||||
request.setRange(4, 4 + pathBytes.length, pathBytes);
|
||||
request.setRange(4 + pathBytes.length, request.length, newPathBytes);
|
||||
|
||||
_fs.makeSyncCall(12, request);
|
||||
return WebFile(_fs, newPath);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<FileSystemEntity> delete({bool recursive = false}) async {
|
||||
@@ -348,14 +447,22 @@ class WebFile extends FileSystemEntity implements File {
|
||||
}
|
||||
|
||||
@override
|
||||
void deleteSync({bool recursive = false}) =>
|
||||
throw UnsupportedError('Sync not supported');
|
||||
void deleteSync({bool recursive = false}) {
|
||||
final type = _fs.typeSync(path, followLinks: false);
|
||||
if (type == FileSystemEntityType.notFound) {
|
||||
throw FileSystemException(
|
||||
'Cannot delete file, path = \'$path\' (OS Error: No such file or directory, errno = 2)',
|
||||
path,
|
||||
);
|
||||
}
|
||||
_fs.makeSyncCall(6, utf8.encode(path));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<FileStat> stat() => _fs.stat(path);
|
||||
|
||||
@override
|
||||
FileStat statSync() => throw UnsupportedError('Sync not supported');
|
||||
FileStat statSync() => _fs.statSync(path);
|
||||
|
||||
@override
|
||||
Uri get uri => Uri.parse(path);
|
||||
@@ -374,8 +481,7 @@ class WebFile extends FileSystemEntity implements File {
|
||||
Future<String> resolveSymbolicLinks() => _fs.resolveSymbolicLinks(path);
|
||||
|
||||
@override
|
||||
String resolveSymbolicLinksSync() =>
|
||||
throw UnsupportedError('Sync not supported');
|
||||
String resolveSymbolicLinksSync() => _fs.resolveSymbolicLinksSync(path);
|
||||
|
||||
@override
|
||||
Stream<FileSystemEvent> watch({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
import 'package:file/file.dart';
|
||||
import 'package:web_file_system/src/backend/idb_inode_service.dart';
|
||||
import '../web_file_system.dart';
|
||||
@@ -57,7 +58,41 @@ class WebLink extends FileSystemEntity implements Link {
|
||||
|
||||
@override
|
||||
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
|
||||
@@ -85,7 +120,30 @@ class WebLink extends FileSystemEntity implements Link {
|
||||
|
||||
@override
|
||||
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
|
||||
@@ -107,7 +165,22 @@ class WebLink extends FileSystemEntity implements Link {
|
||||
|
||||
@override
|
||||
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
|
||||
@@ -132,8 +205,33 @@ class WebLink extends FileSystemEntity implements Link {
|
||||
}
|
||||
|
||||
@override
|
||||
Link renameSync(String newPath) =>
|
||||
throw UnsupportedError('Sync not supported');
|
||||
Link renameSync(String newPath) {
|
||||
final type = _fs.typeSync(path, followLinks: false);
|
||||
if (type == FileSystemEntityType.notFound) {
|
||||
throw FileSystemException(
|
||||
'Cannot rename link, path = \'$path\' (OS Error: No such file or directory, errno = 2)',
|
||||
path,
|
||||
);
|
||||
}
|
||||
final newParentDir = _fs.path.dirname(newPath);
|
||||
if (_fs.typeSync(newParentDir) != FileSystemEntityType.directory) {
|
||||
throw FileSystemException(
|
||||
'Cannot rename link, path = \'$path\' (OS Error: No such file or directory, errno = 2)',
|
||||
path,
|
||||
);
|
||||
}
|
||||
|
||||
final pathBytes = utf8.encode(path);
|
||||
final newPathBytes = utf8.encode(newPath);
|
||||
final request = Uint8List(4 + pathBytes.length + newPathBytes.length);
|
||||
final bd = ByteData.sublistView(request);
|
||||
bd.setUint32(0, pathBytes.length, Endian.little);
|
||||
request.setRange(4, 4 + pathBytes.length, pathBytes);
|
||||
request.setRange(4 + pathBytes.length, request.length, newPathBytes);
|
||||
|
||||
_fs.makeSyncCall(12, request);
|
||||
return WebLink(_fs, newPath);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<FileSystemEntity> delete({bool recursive = false}) async {
|
||||
@@ -143,8 +241,16 @@ class WebLink extends FileSystemEntity implements Link {
|
||||
}
|
||||
|
||||
@override
|
||||
void deleteSync({bool recursive = false}) =>
|
||||
throw UnsupportedError('Sync not supported');
|
||||
void deleteSync({bool recursive = false}) {
|
||||
final type = _fs.typeSync(path, followLinks: false);
|
||||
if (type == FileSystemEntityType.notFound) {
|
||||
throw FileSystemException(
|
||||
'Cannot delete link, path = \'$path\' (OS Error: No such file or directory, errno = 2)',
|
||||
path,
|
||||
);
|
||||
}
|
||||
_fs.makeSyncCall(6, utf8.encode(path));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> exists() async {
|
||||
@@ -157,13 +263,15 @@ class WebLink extends FileSystemEntity implements Link {
|
||||
}
|
||||
|
||||
@override
|
||||
bool existsSync() => throw UnsupportedError('Sync not supported');
|
||||
bool existsSync() {
|
||||
return _fs.typeSync(path, followLinks: false) == FileSystemEntityType.link;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<FileStat> stat() async => (await _fs.stat(path));
|
||||
|
||||
@override
|
||||
FileStat statSync() => throw UnsupportedError('Sync not supported');
|
||||
FileStat statSync() => _fs.statSync(path);
|
||||
|
||||
@override
|
||||
Uri get uri => Uri.parse(path);
|
||||
@@ -187,8 +295,7 @@ class WebLink extends FileSystemEntity implements Link {
|
||||
Future<String> resolveSymbolicLinks() => _fs.resolveSymbolicLinks(path);
|
||||
|
||||
@override
|
||||
String resolveSymbolicLinksSync() =>
|
||||
throw UnsupportedError('Sync not supported');
|
||||
String resolveSymbolicLinksSync() => _fs.resolveSymbolicLinksSync(path);
|
||||
|
||||
@override
|
||||
Stream<FileSystemEvent> watch({
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:js_interop';
|
||||
import 'package:file/file.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:uuid/uuid.dart';
|
||||
import 'package:web_file_system/src/backend/idb_inode_service.dart';
|
||||
import 'package:web_file_system/src/backend/opfs_block_store.dart';
|
||||
import 'package:web_file_system/src/backend/sync_rpc_helper.dart';
|
||||
import 'entities/web_directory.dart';
|
||||
import 'entities/web_file.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
|
||||
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
|
||||
@@ -176,7 +213,34 @@ class WebFileSystem extends FileSystem {
|
||||
|
||||
@override
|
||||
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) {
|
||||
@@ -188,15 +252,15 @@ class WebFileSystem extends FileSystem {
|
||||
|
||||
@override
|
||||
bool isFileSync(String path) =>
|
||||
throw UnsupportedError('Sync isFile not supported');
|
||||
typeSync(path) == FileSystemEntityType.file;
|
||||
|
||||
@override
|
||||
bool isDirectorySync(String path) =>
|
||||
throw UnsupportedError('Sync isDirectory not supported');
|
||||
typeSync(path) == FileSystemEntityType.directory;
|
||||
|
||||
@override
|
||||
bool isLinkSync(String path) =>
|
||||
throw UnsupportedError('Sync isLink not supported');
|
||||
typeSync(path, followLinks: false) == FileSystemEntityType.link;
|
||||
|
||||
@override
|
||||
Future<bool> isFile(String path) async =>
|
||||
@@ -226,8 +290,15 @@ class WebFileSystem extends FileSystem {
|
||||
}
|
||||
|
||||
@override
|
||||
bool identicalSync(String path1, String path2) =>
|
||||
throw UnsupportedError('Sync not supported');
|
||||
bool identicalSync(String path1, String path2) {
|
||||
try {
|
||||
final r1 = resolveSymbolicLinksSync(path1);
|
||||
final r2 = resolveSymbolicLinksSync(path2);
|
||||
return r1 == r2;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> resolveSymbolicLinks(String pathStr) async {
|
||||
final inode = await resolvepath(pathStr, followLinks: true);
|
||||
@@ -240,6 +311,11 @@ class WebFileSystem extends FileSystem {
|
||||
if (segments.isEmpty) return '/';
|
||||
return '/' + segments.reversed.join('/');
|
||||
}
|
||||
|
||||
String resolveSymbolicLinksSync(String pathStr) {
|
||||
final respBytes = makeSyncCall(11, utf8.encode(pathStr));
|
||||
return utf8.decode(respBytes);
|
||||
}
|
||||
}
|
||||
|
||||
class FileStatImpl implements FileStat {
|
||||
|
||||
Reference in New Issue
Block a user