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

[RFC] [cross_file] New architecture. #7591

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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion packages/cross_file/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
## NEXT
## 0.3.5

* `XFile` is now a read-only `interface`.
* Added `XFileFactory` classes for `native` and `web` to create `XFile` instances.
* Deprecated the former `XFile` constructors (`XFile` and `XFile.fromData`)
* Updates minimum supported SDK version to Flutter 3.19/Dart 3.3.

## 0.3.4+2
Expand Down
39 changes: 29 additions & 10 deletions packages/cross_file/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,38 @@ An abstraction to allow working with files across multiple platforms.

## Usage

Import `package:cross_file/cross_file.dart`, instantiate a `XFile`
using a path or byte array and use its methods and properties to
access the file and its metadata.
Many packages use `XFile` as their return type. In order for your
application to consume those files, import
`package:cross_file/cross_file.dart`, and use its methods and properties
to access the file data and metadata.

Example:
In order to instantiate a new `XFile`, import the correct factory class,
either from `package:cross_file/native/factory.dart` (for native development) or
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small drive-by comment:

Since the two implementations now have platform-specific imports (dart:io or package:web)
users who take the imports at face value might run into compilation issues, because they lack a conditional import?

Can we provide guidance on how to avoid this? With the legacy API we had the File or Blob types as internals and not on the public interface, which allowed for the plugin to provide a conditional import shim.

`package:cross_file/web/factory.dart` (for web development), and use the factory
constructor more appropriate for the data that you need to handle.

The library currently supports factories for the
following source types:

|| **native** | **web** |
|-|------------|---------|
| `UInt8List`| `fromBytes` | `fromBytes` |
| `dart:io` [`File`][dart_file] | `fromFile` | ❌ |
| Filesystem path | `fromPath` | ❌ |
| Web [`File`][mdn_file] | ❌ | `fromFile` |
| Web [`Blob`][mdn_blob] | ❌ | `fromBlob` |
| `objectURL` | ❌ | `fromObjectUrl` |

[dart_file]: https://api.dart.dev/stable/3.5.2/dart-io/File-class.html
[mdn_file]: https://developer.mozilla.org/en-US/docs/Web/API/File
[mdn_blob]: https://developer.mozilla.org/en-US/docs/Web/API/Blob


### Example

<?code-excerpt "example/lib/readme_excerpts.dart (Instantiate)"?>
```dart
final XFile file = XFile('assets/hello.txt');
final XFile file = XFileFactory.fromPath('assets/hello.txt');

print('File information:');
print('- Path: ${file.path}');
Expand All @@ -27,16 +50,12 @@ You will find links to the API docs on the [pub page](https://pub.dev/packages/c

## Web Limitations

`XFile` on the web platform is backed by [Blob](https://api.dart.dev/be/180361/dart-html/Blob-class.html)
`XFile` on the web platform is backed by `Blob`
objects and their URLs.

It seems that Safari hangs when reading Blobs larger than 4GB (your app will stop
without returning any data, or throwing an exception).

This package will attempt to throw an `Exception` before a large file is accessed
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to be sure, was this comment removed, because we now refer to the browser's behavior? Or was this fixed in recent versions of Safari?

from Safari (if its size is known beforehand), so that case can be handled
programmatically.

### Browser compatibility

[![Data on Global support for Blob constructing](https://caniuse.bitsofco.de/image/blobbuilder.png)](https://caniuse.com/blobbuilder)
Expand Down
3 changes: 2 additions & 1 deletion packages/cross_file/example/lib/readme_excerpts.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@
// ignore_for_file: avoid_print

import 'package:cross_file/cross_file.dart';
import 'package:cross_file/native/factory.dart';

/// Demonstrate instantiating an XFile for the README.
Future<XFile> instantiateXFile() async {
// #docregion Instantiate
final XFile file = XFile('assets/hello.txt');
final XFile file = XFileFactory.fromPath('assets/hello.txt');

print('File information:');
print('- Path: ${file.path}');
Expand Down
3 changes: 3 additions & 0 deletions packages/cross_file/lib/cross_file.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/// Exposes the [XFile] interface (to use XFiles on any platform).
library;

export 'src/x_file.dart';
57 changes: 57 additions & 0 deletions packages/cross_file/lib/native/factory.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// 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.

import 'dart:io';
import 'dart:typed_data';

import '../cross_file.dart';
import '../src/implementations/io_bytes_x_file.dart';
import '../src/implementations/io_x_file.dart';

/// Creates [XFile] objects from different native sources.
abstract interface class XFileFactory {
/// Creates an [XFile] from a `dart:io` [File].
///
/// Allows passing the [mimeType] attribute of the file, if needed.
static XFile fromFile(
File file, {
String? mimeType,
}) {
return IOXFile(
file,
mimeType: mimeType,
);
}

/// Creates an [XFile] from a `dart:io` [File]'s [path].
///
/// Allows passing the [mimeType] attribute of the file, if needed.
static XFile fromPath(
String path, {
String? mimeType,
}) {
return IOXFile.fromPath(
path,
mimeType: mimeType,
);
}

/// Creates an [XFile] from an array of [bytes].
///
/// Allows passing the [mimeType], [displayName] and [lastModified] attributes
/// of the file, if needed.
static XFile fromBytes(
Uint8List bytes, {
String? mimeType,
String? displayName,
DateTime? lastModified,
}) {
return BytesXFile(
bytes,
mimeType: mimeType,
displayName: displayName,
lastModified: lastModified,
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// 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.

import 'dart:convert';
import 'dart:typed_data';

import '../x_file.dart';

/// The shared behavior for all byte-backed [XFile] implementations.
///
/// This is an almost complete XFile implementation, except for the `saveTo`
/// method, which is platform-dependent.
abstract class BaseBytesXFile implements XFile {
/// Construct an [XFile] from its data [bytes].
BaseBytesXFile(
this.bytes, {
String? mimeType,
String? displayName,
DateTime? lastModified,
}) : _mimeType = mimeType,
_displayName = displayName,
_lastModified = lastModified;

/// The binary contents of this [XFile].
final Uint8List bytes;
final String? _mimeType;
final String? _displayName;
final DateTime? _lastModified;

@override
Future<DateTime> lastModified() async {
return _lastModified ?? DateTime.now();
}

@override
String? get mimeType => _mimeType;

@override
String get path => '';

@override
String get name => _displayName ?? '';

@override
Future<int> length() async {
return bytes.length;
}

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

@override
Future<Uint8List> readAsBytes() async {
return bytes;
}

@override
Stream<Uint8List> openRead([int? start, int? end]) async* {
yield bytes.sublist(start ?? 0, end ?? bytes.length);
}
}
150 changes: 150 additions & 0 deletions packages/cross_file/lib/src/implementations/blob_x_file.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// 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.

import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';

import 'package:web/web.dart';

import '../web_helpers/web_helpers.dart';
import '../x_file.dart';

/// The metadata and shared behavior of a [blob]-backed [XFile].
abstract class BaseBlobXFile implements XFile {
/// Store the metadata of the [blob]-backed [XFile].
BaseBlobXFile({
String? mimeType,
String? displayName,
DateTime? lastModified,
}) : _mimeType = mimeType,
_lastModified = lastModified ?? DateTime.fromMillisecondsSinceEpoch(0),
_name = displayName;

final String? _mimeType;
final String? _name;
final DateTime _lastModified;

/// Asynchronously retrieve the [Blob] backing this [XFile].
///
/// Subclasses must implement this getter. All the blob accesses on this file
/// must be implemented off of it.
Future<Blob> get blob;

@override
String? get mimeType => _mimeType;

@override
String get name => _name ?? '';

@override
String get path => '';

@override
Future<DateTime> lastModified() async => _lastModified;

@override
Future<Uint8List> readAsBytes() async {
return blobToBytes(await blob);
}

@override
Future<int> length() async => (await blob).size;

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

// TODO(dit): https://github.com/flutter/flutter/issues/91867 Implement openRead properly.
@override
Stream<Uint8List> openRead([int? start, int? end]) async* {
final Blob browserBlob = await blob;
final Blob slice = browserBlob.slice(
start ?? 0,
end ?? browserBlob.size,
browserBlob.type,
);
yield await blobToBytes(slice);
}
}

/// Construct an [XFile] backed by [blob].
///
/// `name` needs to be passed from the outside, since it's only available
/// while handling [html.File]s (when the ObjectUrl is created).
class BlobXFile extends BaseBlobXFile {
/// Construct an [XFile] backed by [blob].
///
/// `name` needs to be passed from the outside, since it's only available
/// while handling [html.File]s (when the ObjectUrl is created).
BlobXFile(
Blob blob, {
super.mimeType,
super.displayName,
super.lastModified,
}) : _blob = blob;

/// Creates a [XFile] from a web [File].
factory BlobXFile.fromFile(File file) {
return BlobXFile(
file,
mimeType: file.type,
displayName: file.name,
lastModified: DateTime.fromMillisecondsSinceEpoch(file.lastModified),
);
}

Blob _blob;

// The Blob backing the file.
@override
Future<Blob> get blob async => _blob;

/// Attempts to save the data of this [XFile], using the passed-in `blob`.
///
/// The [path] variable is ignored.
@override
Future<void> saveTo(String path) async {
// Save a Blob to file...
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment seems a bit redundant.

await downloadBlob(_blob, name.isEmpty ? null : name);
}
}

/// Constructs an [XFile] from the [objectUrl] of a [Blob].
///
/// Important: the Object URL of a blob must have been created by the same JS
/// thread that is attempting to retrieve it. Otherwise, the blob will not be
/// accessible.
///
/// See: https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL_static
class ObjectUrlBlobXFile extends BaseBlobXFile {
/// Constructs an [XFile] from the [objectUrl] of a [Blob].
ObjectUrlBlobXFile(
String objectUrl, {
super.mimeType,
super.displayName,
super.lastModified,
}) : _objectUrl = objectUrl;

final String _objectUrl;

Blob? _cachedBlob;

// The Blob backing the file.
@override
Future<Blob> get blob async => _cachedBlob ??= await fetchBlob(_objectUrl);

/// Returns the [objectUrl] used to create this instance.
@override
String get path => _objectUrl;

/// Attempts to save the data of this [XFile], using the passed-in `objectUrl`.
///
/// The [path] variable is ignored.
@override
Future<void> saveTo(String path) async {
downloadObjectUrl(_objectUrl, name.isEmpty ? null : name);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// 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.

import 'dart:io';
import 'dart:typed_data';

import 'base_bytes_x_file.dart';

/// A CrossFile backed by an [Uint8List].
class BytesXFile extends BaseBytesXFile {
/// Construct an [XFile] from its data [bytes].
BytesXFile(
super.bytes, {
super.mimeType,
super.displayName,
super.lastModified,
});

@override
Future<void> saveTo(String path) async {
final File fileToSave = File(path);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is lifted from the old implementation but... should the File be created from path + this.name?

await fileToSave.writeAsBytes(bytes);
}
}
Loading