From 01fe0f983a92e90a57a2c16e083614c767a43fc8 Mon Sep 17 00:00:00 2001 From: Haroun El Omri Date: Sat, 27 Apr 2024 20:55:04 +0200 Subject: [PATCH 1/5] First draft for option 2 implementation with tests (web tests to improve) --- packages/cross_file/lib/src/types/html.dart | 51 ++++- .../cross_file/lib/src/types/interface.dart | 19 ++ packages/cross_file/lib/src/types/io.dart | 108 ++++++++--- .../lib/src/types/x_file_source.dart | 40 ++++ packages/cross_file/lib/src/x_file.dart | 1 + .../cross_file/test/x_file_html_test.dart | 177 ++++++++++++++---- packages/cross_file/test/x_file_io_test.dart | 61 ++++++ 7 files changed, 390 insertions(+), 67 deletions(-) create mode 100644 packages/cross_file/lib/src/types/x_file_source.dart diff --git a/packages/cross_file/lib/src/types/html.dart b/packages/cross_file/lib/src/types/html.dart index 8bc5361bbfb..67689781dfe 100644 --- a/packages/cross_file/lib/src/types/html.dart +++ b/packages/cross_file/lib/src/types/html.dart @@ -12,6 +12,7 @@ import 'package:web/web.dart'; import '../web_helpers/web_helpers.dart'; import 'base.dart'; +import 'x_file_source.dart'; // Four Gigabytes, in bytes. const int _fourGigabytes = 4 * 1024 * 1024 * 1024; @@ -42,6 +43,7 @@ class XFile extends XFileBase { _overrides = overrides, _lastModified = lastModified ?? DateTime.fromMillisecondsSinceEpoch(0), _name = name ?? '', + _source = null, super(path) { // Cache `bytes` as Blob, if passed. if (bytes != null) { @@ -63,6 +65,7 @@ class XFile extends XFileBase { _overrides = overrides, _lastModified = lastModified ?? DateTime.fromMillisecondsSinceEpoch(0), _name = name ?? '', + _source = null, super(path) { if (path == null) { _browserBlob = _createBlobFromBytes(bytes, mimeType); @@ -72,6 +75,18 @@ class XFile extends XFileBase { } } + /// Construct a CrossFile object from a custom source. + XFile.fromCustomSource( + XFileSource source, { + @visibleForTesting CrossFileTestOverrides? overrides, + }) : _mimeType = null, + _length = null, + _overrides = overrides, + _lastModified = null, + _name = null, + _source = source, + super(null); + // Initializes a Blob from a bunch of `bytes` and an optional `mimeType`. Blob _createBlobFromBytes(Uint8List bytes, String? mimeType) { return (mimeType == null) @@ -85,19 +100,20 @@ class XFile extends XFileBase { // MimeType of the file (eg: "image/gif"). final String? _mimeType; // Name (with extension) of the file (eg: "anim.gif") - final String _name; + final String? _name; // Path of the file (must be a valid Blob URL, when set manually!) - late String _path; + String? _path; // The size of the file (in bytes). final int? _length; // The time the file was last modified. - final DateTime _lastModified; + final DateTime? _lastModified; // The link to the binary object in the browser memory (Blob). // This can be passed in (as `bytes` in the constructor) or derived from // [_path] with a fetch request. // (Similar to a (read-only) dart:io File.) Blob? _browserBlob; + final XFileSource? _source; // An html Element that will be used to trigger a "save as" dialog later. // TODO(dit): https://github.com/flutter/flutter/issues/91400 Remove this _target. @@ -111,16 +127,21 @@ class XFile extends XFileBase { bool get _hasTestOverrides => _overrides != null; @override - String? get mimeType => _mimeType; + String? get mimeType { + return _mimeType ?? _source?.mimeType; + } @override - String get name => _name; + String get name { + return _name ?? _source!.name; + } @override - String get path => _path; + String get path => _path ?? _source!.path; @override - Future lastModified() async => _lastModified; + Future lastModified() async => + _lastModified ?? await _source!.lastModified(); Future get _blob async { if (_browserBlob != null) { @@ -157,20 +178,30 @@ class XFile extends XFileBase { @override Future readAsBytes() async { - return _blob.then(_blobToByteBuffer); + return _source?.openRead().toList().then((List chunks) { + return Uint8List.fromList( + chunks.expand((Uint8List chunk) => chunk).toList()); + }) ?? + _blob.then(_blobToByteBuffer); } @override - Future length() async => _length ?? (await _blob).size; + Future length() async => + _length ?? await _source?.length() ?? (await _blob).size; @override - Future readAsString({Encoding encoding = utf8}) async { + Future readAsString({Encoding encoding = utf8}) { return readAsBytes().then(encoding.decode); } // TODO(dit): https://github.com/flutter/flutter/issues/91867 Implement openRead properly. @override Stream openRead([int? start, int? end]) async* { + if (_source != null) { + yield* _source.openRead(start, end); + return; + } + final Blob blob = await _blob; final Blob slice = blob.slice(start ?? 0, end ?? blob.size, blob.type); diff --git a/packages/cross_file/lib/src/types/interface.dart b/packages/cross_file/lib/src/types/interface.dart index c83e5734d32..8ddbd16c7d9 100644 --- a/packages/cross_file/lib/src/types/interface.dart +++ b/packages/cross_file/lib/src/types/interface.dart @@ -3,9 +3,11 @@ // found in the LICENSE file. import 'dart:typed_data'; + import 'package:meta/meta.dart'; import './base.dart'; +import 'x_file_source.dart'; // ignore_for_file: avoid_unused_constructor_parameters @@ -47,6 +49,23 @@ class XFile extends XFileBase { throw UnimplementedError( 'CrossFile is not available in your current platform.'); } + + /// Construct a CrossFile object from an instance of `XFileSource`. + /// + /// All exceptions thrown by any member of the implementation of the source + /// won't be altered or caught by this `XFile`. + XFile.fromCustomSource( + XFileSource source, { + String? mimeType, + String? name, + int? length, + DateTime? lastModified, + String? path, + @visibleForTesting CrossFileTestOverrides? overrides, + }) : super(path) { + throw UnimplementedError( + 'CrossFile is not available in your current platform.'); + } } /// Overrides some functions of CrossFile for testing purposes diff --git a/packages/cross_file/lib/src/types/io.dart b/packages/cross_file/lib/src/types/io.dart index a6979e53498..9707322eb36 100644 --- a/packages/cross_file/lib/src/types/io.dart +++ b/packages/cross_file/lib/src/types/io.dart @@ -7,6 +7,7 @@ import 'dart:io'; import 'dart:typed_data'; import './base.dart'; +import 'x_file_source.dart'; // ignore_for_file: avoid_unused_constructor_parameters @@ -35,6 +36,7 @@ class XFile extends XFileBase { }) : _mimeType = mimeType, _file = File(path), _bytes = null, + _source = null, _lastModified = lastModified, super(path); @@ -51,6 +53,7 @@ class XFile extends XFileBase { DateTime? lastModified, }) : _mimeType = mimeType, _bytes = bytes, + _source = null, _file = File(path ?? ''), _length = length, _lastModified = lastModified, @@ -60,59 +63,110 @@ class XFile extends XFileBase { } } - final File _file; + /// Construct a CrossFile object from an instance of `XFileSource`. + /// + /// Exceptions thrown by any member of the implementation of the source + /// won't be altered or caught by this `XFile`. + XFile.fromCustomSource(XFileSource source) + : _mimeType = null, + _bytes = null, + _file = null, + _length = null, + _lastModified = null, + _source = source, + super(null); + + final File? _file; final String? _mimeType; final DateTime? _lastModified; int? _length; final Uint8List? _bytes; + final XFileSource? _source; @override Future lastModified() { if (_lastModified != null) { return Future.value(_lastModified); } + + if (_source != null) { + return _source.lastModified(); + } // ignore: avoid_slow_async_io - return _file.lastModified(); + return _file!.lastModified(); } @override Future saveTo(String path) async { - if (_bytes == null) { - await _file.copy(path); - } else { - final File fileToSave = File(path); - // TODO(kevmoo): Remove ignore and fix when the MIN Dart SDK is 3.3 - // ignore: unnecessary_non_null_assertion - await fileToSave.writeAsBytes(_bytes!); + final File fileToSave = File(path); + + if (_bytes != null) { + await fileToSave.writeAsBytes(_bytes); + + return; } + + if (_source != null) { + // Clear the file before writing to it + await fileToSave.writeAsBytes([]); + + await _source.openRead().forEach((Uint8List chunk) { + fileToSave.writeAsBytesSync(chunk, mode: FileMode.append); + }); + return; + } + + await _file!.copy(path); } @override String? get mimeType => _mimeType; @override - String get path => _file.path; + String get path { + if (_file != null) { + return _file.path; + } + + return _source!.path; + } @override - String get name => _file.path.split(Platform.pathSeparator).last; + String get name { + if (_file != null) { + return _file.path.split(Platform.pathSeparator).last; + } + + // Name could be different from the basename of the path on Android + // as the full path may not be available. + return _source!.name; + } @override Future length() { if (_length != null) { return Future.value(_length); } - return _file.length(); + + if (_file != null) { + return _file.length(); + } + + return _source!.length(); } @override Future readAsString({Encoding encoding = utf8}) { if (_bytes != null) { - // TODO(kevmoo): Remove ignore and fix when the MIN Dart SDK is 3.3 - // ignore: unnecessary_non_null_assertion - return Future.value(String.fromCharCodes(_bytes!)); + return Future.value(encoding.decode(_bytes)); + } + + if (_file != null) { + return _file.readAsString(encoding: encoding); } - return _file.readAsString(encoding: encoding); + + return readAsBytes().then(encoding.decode); } @override @@ -120,22 +174,30 @@ class XFile extends XFileBase { if (_bytes != null) { return Future.value(_bytes); } - return _file.readAsBytes(); - } - Stream _getBytes(int? start, int? end) async* { - final Uint8List bytes = _bytes!; - yield bytes.sublist(start ?? 0, end ?? bytes.length); + if (_file != null) { + return _file.readAsBytes(); + } + + return openRead().toList().then((List chunks) { + return Uint8List.fromList( + chunks.expand((Uint8List chunk) => chunk).toList()); + }); } @override Stream openRead([int? start, int? end]) { if (_bytes != null) { - return _getBytes(start, end); - } else { + return Stream.value( + _bytes.sublist(start ?? 0, end ?? _bytes.length)); + } + + if (_file != null) { return _file .openRead(start ?? 0, end) .map((List chunk) => Uint8List.fromList(chunk)); } + + return _source!.openRead(start, end); } } diff --git a/packages/cross_file/lib/src/types/x_file_source.dart b/packages/cross_file/lib/src/types/x_file_source.dart new file mode 100644 index 00000000000..b482200d473 --- /dev/null +++ b/packages/cross_file/lib/src/types/x_file_source.dart @@ -0,0 +1,40 @@ +import 'dart:typed_data'; + +/// Access to a file controlled by the creator of the object. +/// +/// An implementation of this class can back +/// all of the operations made on an XFile. +abstract class XFileSource { + /// The MIME type of the source. + String? get mimeType; + + /// The location of the source in the file system + /// + /// This should not be trusted to always be valid, if not at all. + /// + /// For the web implementation, this should be a blob URL. + String get path; + + /// The name of the file as it was selected by the user in their device. + /// + /// This represents most of the time the basename of `path`. + String get name; + + /// Get the last-modified time for the CrossFile + /// + /// This should not be trusted to always be valid, if not at all. + Future lastModified(); + + /// Get the length of the file. + /// + /// This should not be trusted to always be valid, if not at all. + Future length(); + + /// Create a new independent [Stream] for the contents of this source. + /// If `start` is present, the source will be read from byte-offset `start`. Otherwise from the beginning (index 0). + /// + /// If `end` is present, only up to byte-index `end` will be read. Otherwise, until end of file. + /// + /// In order to make sure that system resources are freed, the stream must be read to completion or the subscription on the stream must be cancelled. + Stream openRead([int? start, int? end]); +} diff --git a/packages/cross_file/lib/src/x_file.dart b/packages/cross_file/lib/src/x_file.dart index 00dda82f024..ef4ad445f5f 100644 --- a/packages/cross_file/lib/src/x_file.dart +++ b/packages/cross_file/lib/src/x_file.dart @@ -5,3 +5,4 @@ export 'types/interface.dart' if (dart.library.js_interop) 'types/html.dart' if (dart.library.io) 'types/io.dart'; +export 'types/x_file_source.dart'; diff --git a/packages/cross_file/test/x_file_html_test.dart b/packages/cross_file/test/x_file_html_test.dart index dedd8806f01..ed0dbe78737 100644 --- a/packages/cross_file/test/x_file_html_test.dart +++ b/packages/cross_file/test/x_file_html_test.dart @@ -63,6 +63,29 @@ void main() { }); }); + group('Create with a custom source', () { + final XFile file = XFile.fromCustomSource( + TestXFileSource( + DateTime.now(), 'text/plain', bytes, textFileUrl, textFile.name), + ); + + test('Can be read as a string', () async { + expect(await file.readAsString(), equals(expectedStringContents)); + }); + + test('Can be read as bytes', () async { + expect(await file.readAsBytes(), equals(bytes)); + }); + + test('Can be read as a stream', () async { + expect(await file.openRead().first, equals(bytes)); + }); + + test('Stream can be sliced', () async { + expect(await file.openRead(2, 5).first, equals(bytes.sublist(2, 5))); + }); + }); + group('Blob backend', () { final XFile file = XFile(textFileUrl); @@ -89,57 +112,143 @@ void main() { const String crossFileDomElementId = '__x_file_dom_element'; group('CrossFile saveTo(..)', () { - test('creates a DOM container', () async { - final XFile file = XFile.fromData(bytes); + group('From data', () { + test('creates a DOM container', () async { + final XFile file = XFile.fromData(bytes); - await file.saveTo(''); + await file.saveTo(''); - final html.Element? container = - html.document.querySelector('#$crossFileDomElementId'); + final html.Element? container = + html.document.querySelector('#$crossFileDomElementId'); - expect(container, isNotNull); - }); + expect(container, isNotNull); + }); - test('create anchor element', () async { - final XFile file = XFile.fromData(bytes, name: textFile.name); + test('create anchor element', () async { + final XFile file = XFile.fromData(bytes, name: textFile.name); - await file.saveTo('path'); + await file.saveTo('path'); - final html.Element container = - html.document.querySelector('#$crossFileDomElementId')!; + final html.Element container = + html.document.querySelector('#$crossFileDomElementId')!; - late html.HTMLAnchorElement element; - for (int i = 0; i < container.childNodes.length; i++) { - final html.Element test = container.children.item(i)!; - if (test.tagName == 'A') { - element = test as html.HTMLAnchorElement; - break; + late html.HTMLAnchorElement element; + for (int i = 0; i < container.childNodes.length; i++) { + final html.Element test = container.children.item(i)!; + if (test.tagName == 'A') { + element = test as html.HTMLAnchorElement; + break; + } } - } - // if element is not found, the `firstWhere` call will throw StateError. - expect(element.href, file.path); - expect(element.download, file.name); + // if element is not found, the `firstWhere` call will throw StateError. + expect(element.href, file.path); + expect(element.download, file.name); + }); + + test('anchor element is clicked', () async { + final html.HTMLAnchorElement mockAnchor = + html.document.createElement('a') as html.HTMLAnchorElement; + + final CrossFileTestOverrides overrides = CrossFileTestOverrides( + createAnchorElement: (_, __) => mockAnchor, + ); + + final XFile file = + XFile.fromData(bytes, name: textFile.name, overrides: overrides); + + bool clicked = false; + mockAnchor.onClick.listen((html.MouseEvent event) => clicked = true); + + await file.saveTo('path'); + + expect(clicked, true); + }); }); - test('anchor element is clicked', () async { - final html.HTMLAnchorElement mockAnchor = - html.document.createElement('a') as html.HTMLAnchorElement; + group('from a custom source', () { + test('creates a DOM container', () async { + final XFile file = XFile.fromCustomSource(TestXFileSource( + DateTime.now(), 'text/plain', bytes, textFileUrl, '')); + + await file.saveTo(''); + + final html.Element? container = + html.document.querySelector('#$crossFileDomElementId'); + + expect(container, isNotNull); + }); - final CrossFileTestOverrides overrides = CrossFileTestOverrides( - createAnchorElement: (_, __) => mockAnchor, - ); + test('create anchor element', () async { + final XFile file = XFile.fromCustomSource(TestXFileSource( + DateTime.now(), 'text/plain', bytes, textFileUrl, '')); - final XFile file = - XFile.fromData(bytes, name: textFile.name, overrides: overrides); + await file.saveTo('path'); - bool clicked = false; - mockAnchor.onClick.listen((html.MouseEvent event) => clicked = true); + final html.Element container = + html.document.querySelector('#$crossFileDomElementId')!; - await file.saveTo('path'); + late html.HTMLAnchorElement element; + for (int i = 0; i < container.childNodes.length; i++) { + final html.Element test = container.children.item(i)!; + if (test.tagName == 'A') { + element = test as html.HTMLAnchorElement; + break; + } + } + + // if element is not found, the `firstWhere` call will throw StateError. + expect(element.href, file.path); + expect(element.download, file.name); + }); + + test('anchor element is clicked', () async { + final html.HTMLAnchorElement mockAnchor = + html.document.createElement('a') as html.HTMLAnchorElement; + + final CrossFileTestOverrides overrides = CrossFileTestOverrides( + createAnchorElement: (_, __) => mockAnchor, + ); + + final XFile file = XFile.fromCustomSource( + TestXFileSource(DateTime.now(), 'text/plain', bytes, textFileUrl, + textFile.name), + overrides: overrides); - expect(clicked, true); + bool clicked = false; + mockAnchor.onClick.listen((html.MouseEvent event) => clicked = true); + + await file.saveTo('path'); + + expect(clicked, true); + }); }); }); }); } + +/// An XFileSource that uses a fixed last modified time and byte contents. +class TestXFileSource extends XFileSource { + TestXFileSource( + this._lastModified, this.mimeType, this.bytes, this.path, this.name); + + final DateTime _lastModified; + @override + final String? mimeType; + final Uint8List bytes; + @override + final String path; + @override + final String name; + + @override + Future lastModified() => Future.value(_lastModified); + + @override + Future length() => Future.value(bytes.length); + + @override + Stream openRead([int? start, int? end]) { + return Stream.value(bytes.sublist(start ?? 0, end)); + } +} diff --git a/packages/cross_file/test/x_file_io_test.dart b/packages/cross_file/test/x_file_io_test.dart index 92dfc885ef8..0af97eecb6f 100644 --- a/packages/cross_file/test/x_file_io_test.dart +++ b/packages/cross_file/test/x_file_io_test.dart @@ -110,6 +110,41 @@ void main() { await tempDir.delete(recursive: true); }); }); + + group('Create with a custom source', () { + final XFile file = XFile.fromCustomSource( + TestXFileSource(DateTime.now(), 'text/plain', bytes, textFilePath)); + + test('Can be read as a string', () async { + expect(await file.readAsString(), equals(expectedStringContents)); + }); + test('Can be read as bytes', () async { + expect(await file.readAsBytes(), equals(bytes)); + }); + + test('Can be read as a stream', () async { + expect(await file.openRead().first, equals(bytes)); + }); + + test('Stream can be sliced', () async { + expect(await file.openRead(2, 5).first, equals(bytes.sublist(2, 5))); + }); + + test('Function saveTo(..) creates file', () async { + final Directory tempDir = Directory.systemTemp.createTempSync(); + final File targetFile = File('${tempDir.path}/newFilePath.txt'); + if (targetFile.existsSync()) { + await targetFile.delete(); + } + + await file.saveTo(targetFile.path); + + expect(targetFile.existsSync(), isTrue); + expect(targetFile.readAsStringSync(), 'Hello, world!'); + + await tempDir.delete(recursive: true); + }); + }); } // This is to create an analysis error if the version of XFile in @@ -131,3 +166,29 @@ class TestXFile extends XFile { return super.readAsBytes(); } } + +/// An XFileSource that uses a fixed last modified time and byte contents. +class TestXFileSource extends XFileSource { + TestXFileSource(this._lastModified, this.mimeType, this.bytes, this.path); + + final DateTime _lastModified; + @override + final String? mimeType; + final Uint8List bytes; + @override + final String path; + + @override + Future lastModified() => Future.value(_lastModified); + + @override + Future length() => Future.value(bytes.length); + + @override + String get name => path.split(Platform.pathSeparator).last; + + @override + Stream openRead([int? start, int? end]) { + return Stream.value(bytes.sublist(start ?? 0, end)); + } +} From e5f1fe847986c5bea9b5cf0c3c20807f5c2eda00 Mon Sep 17 00:00:00 2001 From: Haroun El Omri Date: Sun, 28 Apr 2024 14:50:32 +0200 Subject: [PATCH 2/5] - Made all getters nullable in XFileSource - Added lazy-loading of a blob for the file content which fixes saveTo with html.dart when using a custom source - Fixed incorrect constructor in interface.dart - Moved TextXFileSource to common.dart file --- packages/cross_file/lib/src/types/html.dart | 16 +++++++-- .../cross_file/lib/src/types/interface.dart | 7 +--- packages/cross_file/lib/src/types/io.dart | 6 ++-- .../lib/src/types/x_file_source.dart | 6 ++-- packages/cross_file/test/common.dart | 29 +++++++++++++++ .../cross_file/test/x_file_html_test.dart | 35 +++---------------- packages/cross_file/test/x_file_io_test.dart | 32 +++-------------- 7 files changed, 58 insertions(+), 73 deletions(-) create mode 100644 packages/cross_file/test/common.dart diff --git a/packages/cross_file/lib/src/types/html.dart b/packages/cross_file/lib/src/types/html.dart index 67689781dfe..0510f26b91b 100644 --- a/packages/cross_file/lib/src/types/html.dart +++ b/packages/cross_file/lib/src/types/html.dart @@ -133,11 +133,17 @@ class XFile extends XFileBase { @override String get name { - return _name ?? _source!.name; + return _name ?? _source!.name ?? ''; } @override - String get path => _path ?? _source!.path; + String get path { + if ((_path ?? _source!.path) == null) { + _path = URL.createObjectURL(_browserBlob!); + } + + return _path ?? _source!.path!; + } @override Future lastModified() async => @@ -148,6 +154,12 @@ class XFile extends XFileBase { return _browserBlob!; } + // We lazy-load the blob into memory as it could not be used at all during the lifetime of the XFile. + if (_source != null) { + _browserBlob = _createBlobFromBytes(await readAsBytes(), mimeType); + return _browserBlob!; + } + // Attempt to re-hydrate the blob from the `path` via a (local) HttpRequest. // Note that safari hangs if the Blob is >=4GB, so bail out in that case. if (isSafari() && _length != null && _length >= _fourGigabytes) { diff --git a/packages/cross_file/lib/src/types/interface.dart b/packages/cross_file/lib/src/types/interface.dart index 8ddbd16c7d9..aff5e5ef50c 100644 --- a/packages/cross_file/lib/src/types/interface.dart +++ b/packages/cross_file/lib/src/types/interface.dart @@ -56,13 +56,8 @@ class XFile extends XFileBase { /// won't be altered or caught by this `XFile`. XFile.fromCustomSource( XFileSource source, { - String? mimeType, - String? name, - int? length, - DateTime? lastModified, - String? path, @visibleForTesting CrossFileTestOverrides? overrides, - }) : super(path) { + }) : super(null) { throw UnimplementedError( 'CrossFile is not available in your current platform.'); } diff --git a/packages/cross_file/lib/src/types/io.dart b/packages/cross_file/lib/src/types/io.dart index 9707322eb36..6b18cf10e92 100644 --- a/packages/cross_file/lib/src/types/io.dart +++ b/packages/cross_file/lib/src/types/io.dart @@ -109,7 +109,7 @@ class XFile extends XFileBase { if (_source != null) { // Clear the file before writing to it - await fileToSave.writeAsBytes([]); + await fileToSave.writeAsBytes([], flush: true); await _source.openRead().forEach((Uint8List chunk) { fileToSave.writeAsBytesSync(chunk, mode: FileMode.append); @@ -129,7 +129,7 @@ class XFile extends XFileBase { return _file.path; } - return _source!.path; + return _source!.path ?? ''; } @override @@ -140,7 +140,7 @@ class XFile extends XFileBase { // Name could be different from the basename of the path on Android // as the full path may not be available. - return _source!.name; + return _source!.name ?? ''; } @override diff --git a/packages/cross_file/lib/src/types/x_file_source.dart b/packages/cross_file/lib/src/types/x_file_source.dart index b482200d473..cb1be717911 100644 --- a/packages/cross_file/lib/src/types/x_file_source.dart +++ b/packages/cross_file/lib/src/types/x_file_source.dart @@ -13,12 +13,12 @@ abstract class XFileSource { /// This should not be trusted to always be valid, if not at all. /// /// For the web implementation, this should be a blob URL. - String get path; + String? get path; /// The name of the file as it was selected by the user in their device. /// - /// This represents most of the time the basename of `path`. - String get name; + /// This represents most of the time the basename of `path` excepted on web. + String? get name; /// Get the last-modified time for the CrossFile /// diff --git a/packages/cross_file/test/common.dart b/packages/cross_file/test/common.dart new file mode 100644 index 00000000000..9431c7d3661 --- /dev/null +++ b/packages/cross_file/test/common.dart @@ -0,0 +1,29 @@ +import 'dart:typed_data'; + +import 'package:cross_file/cross_file.dart'; + +/// An XFileSource that uses a fixed last modified time and byte contents. +class TestXFileSource extends XFileSource { + TestXFileSource( + this._lastModified, this.mimeType, this.bytes, this.path, this.name); + + final DateTime _lastModified; + @override + final String? mimeType; + final Uint8List bytes; + @override + final String? path; + @override + final String? name; + + @override + Future lastModified() => Future.value(_lastModified); + + @override + Future length() => Future.value(bytes.length); + + @override + Stream openRead([int? start, int? end]) { + return Stream.value(bytes.sublist(start ?? 0, end)); + } +} diff --git a/packages/cross_file/test/x_file_html_test.dart b/packages/cross_file/test/x_file_html_test.dart index ed0dbe78737..210542c61e1 100644 --- a/packages/cross_file/test/x_file_html_test.dart +++ b/packages/cross_file/test/x_file_html_test.dart @@ -13,14 +13,13 @@ import 'package:cross_file/cross_file.dart'; import 'package:test/test.dart'; import 'package:web/web.dart' as html; +import 'common.dart'; + const String expectedStringContents = 'Hello, world! I ❤ ñ! 空手'; final Uint8List bytes = Uint8List.fromList(utf8.encode(expectedStringContents)); final html.File textFile = html.File([bytes.toJS].toJS, 'hello.txt'); -final String textFileUrl = - // TODO(kevmoo): drop ignore when pkg:web constraint excludes v0.3 - // ignore: unnecessary_cast - html.URL.createObjectURL(textFile as JSObject); +final String textFileUrl = html.URL.createObjectURL(textFile as JSObject); void main() { group('Create with an objectUrl', () { @@ -112,7 +111,7 @@ void main() { const String crossFileDomElementId = '__x_file_dom_element'; group('CrossFile saveTo(..)', () { - group('From data', () { + group('from data', () { test('creates a DOM container', () async { final XFile file = XFile.fromData(bytes); @@ -226,29 +225,3 @@ void main() { }); }); } - -/// An XFileSource that uses a fixed last modified time and byte contents. -class TestXFileSource extends XFileSource { - TestXFileSource( - this._lastModified, this.mimeType, this.bytes, this.path, this.name); - - final DateTime _lastModified; - @override - final String? mimeType; - final Uint8List bytes; - @override - final String path; - @override - final String name; - - @override - Future lastModified() => Future.value(_lastModified); - - @override - Future length() => Future.value(bytes.length); - - @override - Stream openRead([int? start, int? end]) { - return Stream.value(bytes.sublist(start ?? 0, end)); - } -} diff --git a/packages/cross_file/test/x_file_io_test.dart b/packages/cross_file/test/x_file_io_test.dart index 0af97eecb6f..c6b12da12af 100644 --- a/packages/cross_file/test/x_file_io_test.dart +++ b/packages/cross_file/test/x_file_io_test.dart @@ -12,6 +12,8 @@ import 'dart:typed_data'; import 'package:cross_file/cross_file.dart'; import 'package:test/test.dart'; +import 'common.dart'; + final String pathPrefix = Directory.current.path.endsWith('test') ? './assets/' : './test/assets/'; final String path = '${pathPrefix}hello.txt'; @@ -112,8 +114,8 @@ void main() { }); group('Create with a custom source', () { - final XFile file = XFile.fromCustomSource( - TestXFileSource(DateTime.now(), 'text/plain', bytes, textFilePath)); + final XFile file = XFile.fromCustomSource(TestXFileSource( + DateTime.now(), 'text/plain', bytes, textFilePath, null)); test('Can be read as a string', () async { expect(await file.readAsString(), equals(expectedStringContents)); @@ -166,29 +168,3 @@ class TestXFile extends XFile { return super.readAsBytes(); } } - -/// An XFileSource that uses a fixed last modified time and byte contents. -class TestXFileSource extends XFileSource { - TestXFileSource(this._lastModified, this.mimeType, this.bytes, this.path); - - final DateTime _lastModified; - @override - final String? mimeType; - final Uint8List bytes; - @override - final String path; - - @override - Future lastModified() => Future.value(_lastModified); - - @override - Future length() => Future.value(bytes.length); - - @override - String get name => path.split(Platform.pathSeparator).last; - - @override - Stream openRead([int? start, int? end]) { - return Stream.value(bytes.sublist(start ?? 0, end)); - } -} From d5a4265f6b892a47fdbc1abbb37658da5b6f133e Mon Sep 17 00:00:00 2001 From: Haroun El Omri Date: Sun, 28 Apr 2024 15:54:04 +0200 Subject: [PATCH 3/5] Version bump and changelog update (ready for PR) --- packages/cross_file/CHANGELOG.md | 5 +++++ packages/cross_file/pubspec.yaml | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/cross_file/CHANGELOG.md b/packages/cross_file/CHANGELOG.md index 2c4e4939615..e5b91214986 100644 --- a/packages/cross_file/CHANGELOG.md +++ b/packages/cross_file/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.3.5 + +* Adds a new constructor (`.fromCustomSource`) to add custom sources + for XFiles' content and metadata using an `XFileSource` implementation. + ## 0.3.4+1 * Removes a few deprecated API usages. diff --git a/packages/cross_file/pubspec.yaml b/packages/cross_file/pubspec.yaml index cf8f9ca7d44..adb21f58f2b 100644 --- a/packages/cross_file/pubspec.yaml +++ b/packages/cross_file/pubspec.yaml @@ -2,7 +2,7 @@ name: cross_file description: An abstraction to allow working with files across multiple platforms. repository: https://github.com/flutter/packages/tree/main/packages/cross_file issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+cross_file%22 -version: 0.3.4+1 +version: 0.3.5 environment: sdk: ^3.3.0 From f6613a146b53756db8cb6c1acd45d0d7ba1d9474 Mon Sep 17 00:00:00 2001 From: Haroun El Omri Date: Mon, 29 Apr 2024 13:13:02 +0200 Subject: [PATCH 4/5] Added missing copyright headers --- packages/cross_file/lib/src/types/x_file_source.dart | 4 ++++ packages/cross_file/test/common.dart | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/packages/cross_file/lib/src/types/x_file_source.dart b/packages/cross_file/lib/src/types/x_file_source.dart index cb1be717911..fc165d20e29 100644 --- a/packages/cross_file/lib/src/types/x_file_source.dart +++ b/packages/cross_file/lib/src/types/x_file_source.dart @@ -1,3 +1,7 @@ +// Copyright 2024 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + import 'dart:typed_data'; /// Access to a file controlled by the creator of the object. diff --git a/packages/cross_file/test/common.dart b/packages/cross_file/test/common.dart index 9431c7d3661..aad2c1e90bf 100644 --- a/packages/cross_file/test/common.dart +++ b/packages/cross_file/test/common.dart @@ -1,3 +1,7 @@ +// Copyright 2024 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + import 'dart:typed_data'; import 'package:cross_file/cross_file.dart'; From b4c03ceb4ebb1c2a24ea24c5f3eb3d3dca6f9443 Mon Sep 17 00:00:00 2001 From: Haroun El Omri Date: Mon, 29 Apr 2024 13:20:46 +0200 Subject: [PATCH 5/5] Reverted incorrect copyright year update... --- packages/cross_file/lib/src/types/x_file_source.dart | 2 +- packages/cross_file/test/common.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cross_file/lib/src/types/x_file_source.dart b/packages/cross_file/lib/src/types/x_file_source.dart index fc165d20e29..00abcd537e3 100644 --- a/packages/cross_file/lib/src/types/x_file_source.dart +++ b/packages/cross_file/lib/src/types/x_file_source.dart @@ -1,4 +1,4 @@ -// Copyright 2024 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. diff --git a/packages/cross_file/test/common.dart b/packages/cross_file/test/common.dart index aad2c1e90bf..d19ae0249a1 100644 --- a/packages/cross_file/test/common.dart +++ b/packages/cross_file/test/common.dart @@ -1,4 +1,4 @@ -// Copyright 2024 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file.