diff --git a/example/pubspec.lock b/example/pubspec.lock index 2b2a88c..560161f 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -144,18 +144,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" url: "https://pub.dev" source: hosted - version: "10.0.4" + version: "10.0.5" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.5" leak_tracker_testing: dependency: transitive description: @@ -184,18 +184,18 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.15.0" mime: dependency: transitive description: @@ -349,10 +349,10 @@ packages: dependency: transitive description: name: test_api - sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.7.2" typed_data: dependency: transitive description: @@ -413,10 +413,18 @@ packages: dependency: transitive description: name: vm_service - sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + url: "https://pub.dev" + source: hosted + version: "14.2.5" + web: + dependency: transitive + description: + name: web + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb url: "https://pub.dev" source: hosted - version: "14.2.1" + version: "1.1.0" win32: dependency: transitive description: @@ -434,5 +442,5 @@ packages: source: hosted version: "1.0.3" sdks: - dart: ">=3.3.0 <4.0.0" + dart: ">=3.4.0 <4.0.0" flutter: ">=3.18.0-18.0.pre.54" diff --git a/lib/src/storage/web_log_storage.dart b/lib/src/storage/web_log_storage.dart index 897edc1..4b08336 100644 --- a/lib/src/storage/web_log_storage.dart +++ b/lib/src/storage/web_log_storage.dart @@ -1,19 +1,14 @@ import 'dart:async'; -import 'dart:html' as html; -import 'dart:html'; +import 'dart:js_interop'; import 'dart:typed_data'; +import 'package:intl/intl.dart'; +import 'dart:js_interop_unsafe'; +import 'package:web/web.dart'; + import 'package:dragon_logs/src/storage/input_output_mixin.dart'; import 'package:dragon_logs/src/storage/log_storage.dart'; import 'package:dragon_logs/src/storage/queue_mixin.dart'; -import 'package:file_system_access_api/file_system_access_api.dart'; -import 'package:intl/intl.dart'; -import 'package:js/js.dart'; -import 'package:js/js_util.dart' as js; - -/// Declare navigator like in a Web Worker context. -@JS() -external dynamic get navigator; class WebLogStorage with QueueMixin, CommonLogStorageOperations @@ -21,38 +16,48 @@ class WebLogStorage // TODO: Multi-day support // final List _logHandles = []; FileSystemDirectoryHandle? _logDirectory; - - FileSystemFileHandle? _currentLogFile; + FileSystemDirectoryReader? _logDirectoryReader; FileSystemWritableFileStream? _currentLogStream; String _currentLogFileName = ""; late Timer _flushTimer; - late final StorageManager? storage = - js.getProperty(navigator, "storage") as StorageManager?; + late final StorageManager? storage; @override Future init() async { - if (!FileSystemAccess.supported) { - throw Exception( - "FileSystemAccess not supported for log storage on this browser", - ); - } + storage = window.navigator.storage; final now = DateTime.now(); _currentLogFileName = logFileNameOfDate(now); - FileSystemDirectoryHandle? root = await storage?.getDirectory(); + FileSystemDirectoryHandle? root; - if (root != null) { - _logDirectory = - await root.getDirectoryHandle("dragon_logs", create: true); + if (storage == null) { + throw Exception("Could not get storage manager"); + } + try { + root = await storage!.getDirectory().toDart; + } catch (e) { + throw Exception("Error getting directory handle: $e"); + } - // await initWriteDate(now); - } else { - throw Exception("Could not get root directory"); + try { + _logDirectory = await root + .getDirectoryHandle( + "dragon_logs", + FileSystemGetDirectoryOptions(create: true), + ) + .toDart; + } catch (e) { + throw Exception("Error getting directory handle"); } + // // Call the JS method to get the directory reader + // _logDirectoryReader = + + await initWriteDate(now); + initQueueFlusher(); } @@ -63,7 +68,8 @@ class WebLogStorage } try { - await _currentLogStream!.writeAsText(logs + '\n'); + String content = logs + '\n'; + await _currentLogStream!.write(content.toJS).toDart; await closeLogFile(); await initWriteDate(DateTime.now()); @@ -74,7 +80,10 @@ class WebLogStorage @override // TODO: implement so that we don't have to delete the whole file + @override Future deleteOldLogs(int size) async { + if (_logDirectory == null) return; + await startFlush(); try { @@ -99,7 +108,13 @@ class WebLogStorage return aDate.compareTo(bDate); }); - await sortedFiles.first.remove(); + + if (sortedFiles.isNotEmpty) { + await sortedFiles.first.getFile().toDart.then((file) async { + // TODO: Implement file.remove + // await file.remove().toDart; + }); + } } } catch (e) { rethrow; @@ -109,30 +124,62 @@ class WebLogStorage } Future initWriteDate(DateTime date) async { - await closeLogFile(); + await closeLogFile(); // Ensure any previous log file is closed _currentLogFileName = logFileNameOfDate(date); - _currentLogFile ??= await _logDirectory?.getFileHandle( - _currentLogFileName, - create: true, - ); + if (_logDirectory == null) return; - final sizeBytes = (await _currentLogFile?.getFile())?.size ?? 0; + FileSystemFileHandle _currentLogFile; + try { + // Get the file handle, create the file if it doesn't exist + _currentLogFile = await _logDirectory! + .getFileHandle( + _currentLogFileName, + FileSystemGetFileOptions(create: true), + ) + .toDart; + } catch (e) { + throw Exception("Error getting file handle: $e"); + } - _currentLogStream = await _currentLogFile?.createWritable( - keepExistingData: true, - ); + try { + // Open a writable stream for the file, allowing for data to be appended + _currentLogStream = await _currentLogFile + .createWritable( + FileSystemCreateWritableOptions(keepExistingData: true), + ) + .toDart; + } catch (e) { + throw Exception("Error creating writable file stream: $e"); + } + + if (_currentLogStream == null) return; + + // Move the write pointer to the end of the file + int sizeBytes = 0; + try { + final file = await _currentLogFile.getFile().toDart; + sizeBytes = file.size; // Get the size of the file in bytes + } catch (e) { + throw Exception("Error getting file size: $e"); + } - await _currentLogStream?.seek(sizeBytes); + try { + // Seek to the end of the file to append data + await _currentLogStream!.seek(sizeBytes).toDart; + } catch (e) { + throw Exception("Error seeking file stream: $e"); + } } @override Future getLogFolderSize() async { final files = await _getLogFiles(); - final htmlFileObjects = - await Future.wait(files.map((e) => e.getFile())); + final htmlFileObjects = await Future.wait( + files.map((e) => e.getFile().toDart), + ); final int totalSize = htmlFileObjects.fold( 0, @@ -147,8 +194,7 @@ class WebLogStorage @override Future closeLogFile() async { if (_currentLogStream != null) { - await _currentLogStream!.close(); - + await _currentLogStream!.close().toDart; _currentLogStream = null; } } @@ -156,7 +202,9 @@ class WebLogStorage @override Stream exportLogsStream() async* { for (final file in await _getLogFiles()) { - String content = await _readFileContent(await file.getFile()); + String content = await _readFileContent( + await file.getFile().toDart, + ); yield content; } } @@ -164,42 +212,87 @@ class WebLogStorage /// Returns a list of OPFS file handles for all log files EXCLUDING any /// temporary write file (if it exists) identified by the `.crswap` extension. Future> _getLogFiles() async { - final files = await _logDirectory?.values - .where((handle) => handle.kind == FileSystemKind.file) - .cast() - .where((handle) => !handle.name.endsWith('.crswap')) - .toList() ?? - []; - - print('_getLogFiles: ${files.map((e) => e.name).join(',\n')}'); - - return files - ..sort( - (a, b) => a.name.compareTo(b.name), - ); - } + final List logFiles = []; - Future _readFileContent(html.File file) async { - final completer = Completer(); - final reader = html.FileReader(); + if (_logDirectory == null) { + throw Exception("Log directory is not initialized"); + } - StreamSubscription? loadEndSubscription; - StreamSubscription? errorSubscription; + // Retrieve the entries iterator from the directory + final entriesAsyncIterator = + _logDirectory!.callMethod('entries'.toJS); - loadEndSubscription = reader.onLoadEnd.listen((event) { - completer.complete(reader.result as String); - }); + final entriesCompleter = Completer(); - errorSubscription = reader.onError.listen((error) { - completer.completeError("Error reading file: $error"); - }); + try { + // Loop over the iterator asynchronously to process the entries + while (true) { + final result = await entriesAsyncIterator + .callMethod('next'.toJS) + .toDart as JSObject; + + final done = result.getProperty('done'.toJS) as bool; + final value = result.getProperty('value'.toJS) as List?; + + // If the iteration is done, break the loop + if (done) { + break; + } + + // Get the key and value from the iterator result (value is the [key, entry] pair) + final entry = value?[1]; + + // Check if the entry is a file and not a temporary file + if (entry is FileSystemFileHandle && !entry.name.endsWith('.crswap')) { + logFiles.add(entry); + } + } + + // Mark the completer as complete when done + if (!entriesCompleter.isCompleted) { + entriesCompleter.complete(); + } + } catch (e) { + if (!entriesCompleter.isCompleted) { + entriesCompleter.completeError( + Exception("Error reading log directory entries: $e"), + ); + } + } - reader.readAsText(file); + // Wait for the completion of the directory read before returning the list + await entriesCompleter.future; - return completer.future.whenComplete(() { - loadEndSubscription?.cancel(); - errorSubscription?.cancel(); - }); + // Sort files by their names + logFiles.sort((a, b) => a.name.compareTo(b.name)); + + print("Log files: ${logFiles.map((e) => e.name)}"); + + return logFiles; + } + + Future _readFileContent(File file) async { + final completer = Completer(); + final reader = FileReader(); + + try { + // Read the file content as text + reader.readAsText(file); + + // Listen for the load end event to complete the completer + reader.onLoadEnd.listen((event) { + if (reader.error == null) { + completer.complete(reader.result as String); + } else { + completer.completeError(reader.error!); + } + }); + } catch (e) { + // Handle any synchronous errors that may occur + completer.completeError(Exception("Error reading file: $e")); + } + + return completer.future; } @override @@ -219,18 +312,22 @@ class WebLogStorage final filename = 'log_${formatter.format(DateTime.now())}.txt'; List bytes = await bytesStream.toList(); - final blob = html.Blob([Uint8List.fromList(bytes)]); - final url = html.Url.createObjectUrlFromBlob(blob); + // final blob = Blob(Uint8List.fromList(bytes).); + final url = Uri.dataFromBytes(Uint8List.fromList(bytes)); // ignore: unused_local_variable - final anchor = html.AnchorElement(href: url) + // Download the file + + final anchor = HTMLAnchorElement() + ..href = url.toString() ..target = 'blank' ..download = filename ..click(); - html.Url.revokeObjectUrl(url); + + anchor.remove(); } void dispose() async { - _flushTimer.cancel(); + _flushTimer?.cancel(); // Safeguard for null timer await closeLogFile(); // Close the log file once during the dispose method } } diff --git a/pubspec.lock b/pubspec.lock index e5e2f46..85b949a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -185,7 +185,7 @@ packages: source: hosted version: "1.0.4" js: - dependency: "direct main" + dependency: transitive description: name: js sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 @@ -220,18 +220,18 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.15.0" mime: dependency: transitive description: @@ -557,6 +557,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + web: + dependency: "direct main" + description: + name: web + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb + url: "https://pub.dev" + source: hosted + version: "1.1.0" web_socket_channel: dependency: transitive description: @@ -598,5 +606,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.3.0-0 <4.0.0" + dart: ">=3.4.0 <4.0.0" flutter: ">=3.13.0" diff --git a/pubspec.yaml b/pubspec.yaml index c87dc5c..f93eb9a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -46,4 +46,4 @@ dependencies: intl: ^0.19.0 path_provider: ^2.1.1 path: ^1.8.3 - js: ^0.6.7 + web: ^1.1.0