Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[cross_file] Adding custom sources to feed data to XFile #6625

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/cross_file/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
63 changes: 53 additions & 10 deletions packages/cross_file/lib/src/types/html.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand All @@ -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);
Expand All @@ -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)
Expand All @@ -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.
Expand All @@ -111,22 +127,39 @@ 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 {
if ((_path ?? _source!.path) == null) {
_path = URL.createObjectURL(_browserBlob!);
}

return _path ?? _source!.path!;
}

@override
Future<DateTime> lastModified() async => _lastModified;
Future<DateTime> lastModified() async =>
_lastModified ?? await _source!.lastModified();

Future<Blob> get _blob async {
if (_browserBlob != null) {
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) {
Expand Down Expand Up @@ -157,20 +190,30 @@ class XFile extends XFileBase {

@override
Future<Uint8List> readAsBytes() async {
return _blob.then(_blobToByteBuffer);
return _source?.openRead().toList().then((List<Uint8List> chunks) {
return Uint8List.fromList(
chunks.expand((Uint8List chunk) => chunk).toList());
}) ??
_blob.then(_blobToByteBuffer);
}

@override
Future<int> length() async => _length ?? (await _blob).size;
Future<int> length() async =>
_length ?? await _source?.length() ?? (await _blob).size;

@override
Future<String> readAsString({Encoding encoding = utf8}) async {
Future<String> readAsString({Encoding encoding = utf8}) {
return readAsBytes().then(encoding.decode);
}

// TODO(dit): https://github.com/flutter/flutter/issues/91867 Implement openRead properly.
@override
Stream<Uint8List> 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);
Expand Down
14 changes: 14 additions & 0 deletions packages/cross_file/lib/src/types/interface.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -47,6 +49,18 @@ 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, {
@visibleForTesting CrossFileTestOverrides? overrides,
}) : super(null) {
throw UnimplementedError(
'CrossFile is not available in your current platform.');
}
}

/// Overrides some functions of CrossFile for testing purposes
Expand Down
108 changes: 85 additions & 23 deletions packages/cross_file/lib/src/types/io.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -35,6 +36,7 @@ class XFile extends XFileBase {
}) : _mimeType = mimeType,
_file = File(path),
_bytes = null,
_source = null,
_lastModified = lastModified,
super(path);

Expand All @@ -51,6 +53,7 @@ class XFile extends XFileBase {
DateTime? lastModified,
}) : _mimeType = mimeType,
_bytes = bytes,
_source = null,
_file = File(path ?? ''),
_length = length,
_lastModified = lastModified,
Expand All @@ -60,82 +63,141 @@ 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<DateTime> lastModified() {
if (_lastModified != null) {
return Future<DateTime>.value(_lastModified);
}

if (_source != null) {
return _source.lastModified();
}
// ignore: avoid_slow_async_io
return _file.lastModified();
return _file!.lastModified();
}

@override
Future<void> 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(<int>[], flush: true);

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<int> length() {
if (_length != null) {
return Future<int>.value(_length);
}
return _file.length();

if (_file != null) {
return _file.length();
}

return _source!.length();
}

@override
Future<String> 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<String>.value(String.fromCharCodes(_bytes!));
return Future<String>.value(encoding.decode(_bytes));
}

if (_file != null) {
return _file.readAsString(encoding: encoding);
}
return _file.readAsString(encoding: encoding);

return readAsBytes().then(encoding.decode);
}

@override
Future<Uint8List> readAsBytes() {
if (_bytes != null) {
return Future<Uint8List>.value(_bytes);
}
return _file.readAsBytes();
}

Stream<Uint8List> _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<Uint8List> chunks) {
return Uint8List.fromList(
chunks.expand((Uint8List chunk) => chunk).toList());
});
}

@override
Stream<Uint8List> openRead([int? start, int? end]) {
if (_bytes != null) {
return _getBytes(start, end);
} else {
return Stream<Uint8List>.value(
_bytes.sublist(start ?? 0, end ?? _bytes.length));
}

if (_file != null) {
return _file
.openRead(start ?? 0, end)
.map((List<int> chunk) => Uint8List.fromList(chunk));
}

return _source!.openRead(start, end);
}
}