Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

[cross_file] An abstraction to allow working with files across multiple platforms. #3260

Merged
merged 13 commits into from
Nov 18, 2020
1 change: 1 addition & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ packages/android_intent/** @mklim @matthew-carroll
packages/battery/** @amirh @matthew-carroll
packages/camera/** @bparrishMines
packages/connectivity/** @cyanglaz @matthew-carroll
packages/cross_file/** @ditman @mvanbeusekom
packages/device_info/** @matthew-carroll
packages/espresso/** @collinjackson @adazh
packages/file_selector/** @ditman
Expand Down
3 changes: 3 additions & 0 deletions packages/cross_file/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 0.1.0

- Initial open-source release
25 changes: 25 additions & 0 deletions packages/cross_file/LICENSE
Original file line number Diff line number Diff line change
@@ -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.
34 changes: 34 additions & 0 deletions packages/cross_file/README.md
Original file line number Diff line number Diff line change
@@ -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.dartlang.org/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).
5 changes: 5 additions & 0 deletions packages/cross_file/lib/cross_file.dart
Original file line number Diff line number Diff line change
@@ -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';
86 changes: 86 additions & 0 deletions packages/cross_file/lib/src/types/base.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// 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
XFileBase(String path);

/// Save the CrossFile at the indicated file path.
void saveTo(String path) async {
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.');
}
}
136 changes: 136 additions & 0 deletions packages/cross_file/lib/src/types/html.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// 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';

import 'package:http/http.dart' as http show readBytes;
import 'package:meta/meta.dart';
import 'dart:html';

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 {
String path;

final String mimeType;
final Uint8List _data;
final int _length;
final String name;
final DateTime _lastModified;
Element _target;

final CrossFileTestOverrides _overrides;

bool get _hasTestOverrides => _overrides != null;

/// 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,
this.name,
int length,
Uint8List bytes,
DateTime lastModified,
@visibleForTesting CrossFileTestOverrides overrides,
}) : _data = bytes,
_length = length,
_overrides = overrides,
_lastModified = lastModified,
super(path);

/// Construct an CrossFile from its data
XFile.fromData(
Uint8List bytes, {
this.mimeType,
this.name,
int length,
DateTime lastModified,
this.path,
@visibleForTesting CrossFileTestOverrides overrides,
}) : _data = bytes,
_length = length,
_overrides = overrides,
_lastModified = lastModified,
super(path) {
if (path == null) {
final blob = (mimeType == null) ? Blob([bytes]) : Blob([bytes], mimeType);
this.path = Url.createObjectUrl(blob);
}
}

@override
Future<DateTime> lastModified() async {
if (_lastModified != null) {
return Future.value(_lastModified);
}
return null;
}

Future<Uint8List> get _bytes async {
if (_data != null) {
return Future.value(UnmodifiableUint8ListView(_data));
}
return http.readBytes(path);
}

@override
Future<int> length() async {
return _length ?? (await _bytes).length;
}

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

@override
Future<Uint8List> readAsBytes() async {
return Future.value(await _bytes);
}

@override
Stream<Uint8List> openRead([int start, int end]) async* {
final 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.
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 != null)
? _overrides.createAnchorElement(this.path, this.name)
: createAnchorElement(this.path, this.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 {
/// For overriding the creation of the file input element.
Element Function(String href, String suggestedName) createAnchorElement;

/// Default constructor for overrides
CrossFileTestOverrides({this.createAnchorElement});
}
58 changes: 58 additions & 0 deletions packages/cross_file/lib/src/types/interface.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// 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';

/// 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 {
/// For overriding the creation of the file input element.
dynamic Function(String href, String suggestedName) createAnchorElement;

/// Default constructor for overrides
CrossFileTestOverrides({this.createAnchorElement});
}
Loading