[cross_file] Move from flutter/plugins. (#305)

This commit is contained in:
David Iglesias Teixeira
2021-03-10 11:29:38 -08:00
14 changed files with 769 additions and 0 deletions

View File

@ -0,0 +1,34 @@
## 0.3.1+1
* Rehomed to `flutter/packages` repository.
## 0.3.1
* Fix nullability of `XFileBase`'s `path` and `name` to match the
implementations to avoid potential analyzer issues.
## 0.3.0
* Migrated package to null-safety.
* **breaking change** According to our unit tests, the API should be backwards-compatible. Some relevant changes were made, however:
* Web: `lastModified` returns the epoch time as a default value, to maintain the `Future<DateTime>` return type (and not `null`)
## 0.2.1
* Prepare for breaking `package:http` change.
## 0.2.0
* **breaking change** Make sure the `saveTo` method returns a `Future` so it can be awaited and users are sure the file has been written to disk.
## 0.1.0+2
* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276))
## 0.1.0+1
* Update Flutter SDK constraint.
## 0.1.0
* Initial open-source release.

View File

@ -0,0 +1,25 @@
Copyright 2020 The Flutter Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -0,0 +1,34 @@
# cross_file
An abstraction to allow working with files across multiple platforms.
# Usage
Import `package:cross/cross_info.dart`, instantiate a `CrossFile`
using a path or byte array and use its methods and properties to
access the file and its metadata.
Example:
```dart
import 'package:cross_file/cross_file.dart';
final file = CrossFile('assets/hello.txt');
print('File information:');
print('- Path: ${file.path}');
print('- Name: ${file.name}');
print('- MIME type: ${file.mimeType}');
final fileContent = await file.readAsString();
print('Content of the file: ${fileContent}'); // e.g. "Moto G (4)"
```
You will find links to the API docs on the [pub page](https://pub.dev/packages/cross_file).
## Getting Started
For help getting started with Flutter, view our online
[documentation](http://flutter.io/).
For help on editing plugin code, view the [documentation](https://flutter.io/platform-plugins/#edit-code).

View File

@ -0,0 +1,5 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
export 'src/x_file.dart';

View File

@ -0,0 +1,87 @@
// Copyright 2018 The Chromium 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';
/// The interface for a CrossFile.
///
/// A CrossFile is a container that wraps the path of a selected
/// file by the user and (in some platforms, like web) the bytes
/// with the contents of the file.
///
/// This class is a very limited subset of dart:io [File], so all
/// the methods should seem familiar.
abstract class XFileBase {
/// Construct a CrossFile
// ignore: avoid_unused_constructor_parameters
XFileBase(String? path);
/// Save the CrossFile at the indicated file path.
Future<void> saveTo(String path) {
throw UnimplementedError('saveTo has not been implemented.');
}
/// Get the path of the picked file.
///
/// This should only be used as a backwards-compatibility clutch
/// for mobile apps, or cosmetic reasons only (to show the user
/// the path they've picked).
///
/// Accessing the data contained in the picked file by its path
/// is platform-dependant (and won't work on web), so use the
/// byte getters in the CrossFile instance instead.
String get path {
throw UnimplementedError('.path has not been implemented.');
}
/// The name of the file as it was selected by the user in their device.
///
/// Use only for cosmetic reasons, do not try to use this as a path.
String get name {
throw UnimplementedError('.name has not been implemented.');
}
/// For web, it may be necessary for a file to know its MIME type.
String? get mimeType {
throw UnimplementedError('.mimeType has not been implemented.');
}
/// Get the length of the file. Returns a `Future<int>` that completes with the length in bytes.
Future<int> length() {
throw UnimplementedError('.length() has not been implemented.');
}
/// Synchronously read the entire file contents as a string using the given [Encoding].
///
/// By default, `encoding` is [utf8].
///
/// Throws Exception if the operation fails.
Future<String> readAsString({Encoding encoding = utf8}) {
throw UnimplementedError('readAsString() has not been implemented.');
}
/// Synchronously read the entire file contents as a list of bytes.
///
/// Throws Exception if the operation fails.
Future<Uint8List> readAsBytes() {
throw UnimplementedError('readAsBytes() has not been implemented.');
}
/// Create a new independent [Stream] for the contents of this file.
///
/// If `start` is present, the file 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<Uint8List> openRead([int? start, int? end]) {
throw UnimplementedError('openRead() has not been implemented.');
}
/// Get the last-modified time for the CrossFile
Future<DateTime> lastModified() {
throw UnimplementedError('openRead() has not been implemented.');
}
}

View File

@ -0,0 +1,143 @@
// Copyright 2018 The Chromium 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:html';
import 'dart:typed_data';
import 'package:meta/meta.dart';
import '../web_helpers/web_helpers.dart';
import './base.dart';
/// A CrossFile that works on web.
///
/// It wraps the bytes of a selected file.
class XFile extends XFileBase {
/// Construct a CrossFile object from its ObjectUrl.
///
/// Optionally, this can be initialized with `bytes` and `length`
/// so no http requests are performed to retrieve files later.
///
/// `name` needs to be passed from the outside, since we only have
/// access to it while we create the ObjectUrl.
XFile(
this.path, {
this.mimeType,
String? name,
int? length,
Uint8List? bytes,
DateTime? lastModified,
@visibleForTesting CrossFileTestOverrides? overrides,
}) : _data = bytes,
_length = length,
_overrides = overrides,
_lastModified = lastModified ?? DateTime.fromMillisecondsSinceEpoch(0),
name = name ?? '',
super(path);
/// Construct an CrossFile from its data
XFile.fromData(
Uint8List bytes, {
this.mimeType,
String? name,
int? length,
DateTime? lastModified,
String? path,
@visibleForTesting CrossFileTestOverrides? overrides,
}) : _data = bytes,
_length = length,
_overrides = overrides,
_lastModified = lastModified ?? DateTime.fromMillisecondsSinceEpoch(0),
name = name ?? '',
super(path) {
if (path == null) {
final Blob blob = (mimeType == null)
? Blob(<dynamic>[bytes])
: Blob(<dynamic>[bytes], mimeType);
this.path = Url.createObjectUrl(blob);
} else {
this.path = path;
}
}
@override
final String? mimeType;
@override
final String name;
@override
late String path;
final Uint8List? _data;
final int? _length;
final DateTime? _lastModified;
late Element _target;
final CrossFileTestOverrides? _overrides;
bool get _hasTestOverrides => _overrides != null;
@override
Future<DateTime> lastModified() async =>
Future<DateTime>.value(_lastModified);
Future<Uint8List> get _bytes async {
if (_data != null) {
return Future<Uint8List>.value(UnmodifiableUint8ListView(_data!));
}
// We can force 'response' to be a byte buffer by passing responseType:
final ByteBuffer? response =
(await HttpRequest.request(path, responseType: 'arraybuffer')).response;
return response?.asUint8List() ?? Uint8List(0);
}
@override
Future<int> length() async => _length ?? (await _bytes).length;
@override
Future<String> readAsString({Encoding encoding = utf8}) async {
return encoding.decode(await _bytes);
}
@override
Future<Uint8List> readAsBytes() async =>
Future<Uint8List>.value(await _bytes);
@override
Stream<Uint8List> openRead([int? start, int? end]) async* {
final Uint8List bytes = await _bytes;
yield bytes.sublist(start ?? 0, end ?? bytes.length);
}
/// Saves the data of this CrossFile at the location indicated by path.
/// For the web implementation, the path variable is ignored.
@override
Future<void> saveTo(String path) async {
// Create a DOM container where we can host the anchor.
_target = ensureInitialized('__x_file_dom_element');
// Create an <a> tag with the appropriate download attributes and click it
// May be overridden with CrossFileTestOverrides
final AnchorElement element = _hasTestOverrides
? _overrides!.createAnchorElement(this.path, name) as AnchorElement
: createAnchorElement(this.path, name);
// Clear the children in our container so we can add an element to click
_target.children.clear();
addElementToContainerAndClick(_target, element);
}
}
/// Overrides some functions to allow testing
@visibleForTesting
class CrossFileTestOverrides {
/// Default constructor for overrides
CrossFileTestOverrides({required this.createAnchorElement});
/// For overriding the creation of the file input element.
Element Function(String href, String suggestedName) createAnchorElement;
}

View File

@ -0,0 +1,60 @@
// Copyright 2018 The Chromium 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:meta/meta.dart';
import './base.dart';
// ignore_for_file: avoid_unused_constructor_parameters
/// A CrossFile is a cross-platform, simplified File abstraction.
///
/// It wraps the bytes of a selected file, and its (platform-dependant) path.
class XFile extends XFileBase {
/// Construct a CrossFile object from its path.
///
/// Optionally, this can be initialized with `bytes` and `length`
/// so no http requests are performed to retrieve data later.
///
/// `name` may be passed from the outside, for those cases where the effective
/// `path` of the file doesn't match what the user sees when selecting it
/// (like in web)
XFile(
String path, {
String? mimeType,
String? name,
int? length,
Uint8List? bytes,
DateTime? lastModified,
@visibleForTesting CrossFileTestOverrides? overrides,
}) : super(path) {
throw UnimplementedError(
'CrossFile is not available in your current platform.');
}
/// Construct a CrossFile object from its data
XFile.fromData(
Uint8List bytes, {
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
@visibleForTesting
class CrossFileTestOverrides {
/// Default constructor for overrides
CrossFileTestOverrides({required this.createAnchorElement});
/// For overriding the creation of the file input element.
dynamic Function(String href, String suggestedName) createAnchorElement;
}

View File

@ -0,0 +1,119 @@
// Copyright 2018 The Chromium 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:io';
import 'dart:typed_data';
import './base.dart';
// ignore_for_file: avoid_unused_constructor_parameters
/// A CrossFile backed by a dart:io File.
class XFile extends XFileBase {
/// Construct a CrossFile object backed by a dart:io File.
XFile(
String path, {
this.mimeType,
String? name,
int? length,
Uint8List? bytes,
DateTime? lastModified,
}) : _file = File(path),
_bytes = null,
_lastModified = lastModified,
super(path);
/// Construct an CrossFile from its data
XFile.fromData(
Uint8List bytes, {
this.mimeType,
String? path,
String? name,
int? length,
DateTime? lastModified,
}) : _bytes = bytes,
_file = File(path ?? ''),
_length = length,
_lastModified = lastModified,
super(path) {
if (length == null) {
_length = bytes.length;
}
}
final File _file;
@override
final String? mimeType;
final DateTime? _lastModified;
int? _length;
final Uint8List? _bytes;
@override
Future<DateTime> lastModified() {
if (_lastModified != null) {
return Future<DateTime>.value(_lastModified);
}
// ignore: avoid_slow_async_io
return _file.lastModified();
}
@override
Future<void> saveTo(String path) async {
final File fileToSave = File(path);
await fileToSave.writeAsBytes(_bytes ?? (await readAsBytes()));
await fileToSave.create();
}
@override
String get path {
return _file.path;
}
@override
String get name {
return _file.path.split(Platform.pathSeparator).last;
}
@override
Future<int> length() {
if (_length != null) {
return Future<int>.value(_length);
}
return _file.length();
}
@override
Future<String> readAsString({Encoding encoding = utf8}) {
if (_bytes != null) {
return Future<String>.value(String.fromCharCodes(_bytes!));
}
return _file.readAsString(encoding: encoding);
}
@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);
}
@override
Stream<Uint8List> openRead([int? start, int? end]) {
if (_bytes != null) {
return _getBytes(start, end);
} else {
return _file
.openRead(start ?? 0, end)
.map((List<int> chunk) => Uint8List.fromList(chunk));
}
}
}

View File

@ -0,0 +1,38 @@
// Copyright 2018 The Chromium 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:html';
/// Create anchor element with download attribute
AnchorElement createAnchorElement(String href, String? suggestedName) {
final AnchorElement element = AnchorElement(href: href);
if (suggestedName == null) {
element.download = 'download';
} else {
element.download = suggestedName;
}
return element;
}
/// Add an element to a container and click it
void addElementToContainerAndClick(Element container, Element element) {
// Add the element and click it
// All previous elements will be removed before adding the new one
container.children.add(element);
element.click();
}
/// Initializes a DOM container where we can host elements.
Element ensureInitialized(String id) {
Element? target = querySelector('#$id');
if (target == null) {
final Element targetElement = Element.tag('flt-x-file')..id = id;
querySelector('body')!.children.add(targetElement);
target = targetElement;
}
return target;
}

View File

@ -0,0 +1,7 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
export 'types/interface.dart'
if (dart.library.html) 'types/html.dart'
if (dart.library.io) 'types/io.dart';

View File

@ -0,0 +1,18 @@
name: cross_file
description: An abstraction to allow working with files across multiple platforms.
repository: https://github.com/flutter/packages/tree/master/packages/cross_file
version: 0.3.1+1
dependencies:
flutter:
sdk: flutter
meta: ^1.3.0
dev_dependencies:
flutter_test:
sdk: flutter
pedantic: ^1.10.0
environment:
sdk: ">=2.12.0 <3.0.0"
flutter: ">=1.22.0"

View File

@ -0,0 +1 @@
Hello, world!

View File

@ -0,0 +1,108 @@
// Copyright 2020 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.
@TestOn('chrome') // Uses web-only Flutter SDK
import 'dart:convert';
import 'dart:html' as html;
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:cross_file/cross_file.dart';
const String expectedStringContents = 'Hello, world!';
final Uint8List bytes = Uint8List.fromList(utf8.encode(expectedStringContents));
final html.File textFile = html.File(<Object>[bytes], 'hello.txt');
final String textFileUrl = html.Url.createObjectUrl(textFile);
void main() {
group('Create with an objectUrl', () {
final XFile file = XFile(textFileUrl);
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('Create from data', () {
final XFile file = XFile.fromData(bytes);
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('saveTo(..)', () {
const String crossFileDomElementId = '__x_file_dom_element';
group('CrossFile saveTo(..)', () {
test('creates a DOM container', () async {
final XFile file = XFile.fromData(bytes);
await file.saveTo('');
final html.Element? container =
html.querySelector('#$crossFileDomElementId');
expect(container, isNotNull);
});
test('create anchor element', () async {
final XFile file = XFile.fromData(bytes, name: textFile.name);
await file.saveTo('path');
final html.Element? container =
html.querySelector('#$crossFileDomElementId');
final html.AnchorElement element = container?.children
.firstWhere((html.Element element) => element.tagName == 'A')
as html.AnchorElement;
// 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.AnchorElement mockAnchor = html.AnchorElement();
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);
});
});
});
}

View File

@ -0,0 +1,90 @@
// Copyright 2020 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.
@TestOn('vm') // Uses dart:io
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:cross_file/cross_file.dart';
final String pathPrefix =
Directory.current.path.endsWith('test') ? './assets/' : './test/assets/';
final String path = pathPrefix + 'hello.txt';
const String expectedStringContents = 'Hello, world!';
final Uint8List bytes = Uint8List.fromList(utf8.encode(expectedStringContents));
final File textFile = File(path);
final String textFilePath = textFile.path;
void main() {
group('Create with a path', () {
final XFile file = XFile(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('saveTo(..) creates file', () async {
final File removeBeforeTest = File(pathPrefix + 'newFilePath.txt');
if (removeBeforeTest.existsSync()) {
await removeBeforeTest.delete();
}
await file.saveTo(pathPrefix + 'newFilePath.txt');
final File newFile = File(pathPrefix + 'newFilePath.txt');
expect(newFile.existsSync(), isTrue);
expect(newFile.readAsStringSync(), 'Hello, world!');
await newFile.delete();
});
});
group('Create with data', () {
final XFile file = XFile.fromData(bytes);
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 File removeBeforeTest = File(pathPrefix + 'newFileData.txt');
if (removeBeforeTest.existsSync()) {
await removeBeforeTest.delete();
}
await file.saveTo(pathPrefix + 'newFileData.txt');
final File newFile = File(pathPrefix + 'newFileData.txt');
expect(newFile.existsSync(), isTrue);
expect(newFile.readAsStringSync(), 'Hello, world!');
await newFile.delete();
});
});
}