[cross_file] Improve dart:io saveTo (#404)
Changes the `saveTo` method for the dart:io-based implementation to copy
path-based instances rather than read them into memory and writing them
out again. This makes it *much* more efficient, especially for large
files.
Also adjusts the unit tests to write to a temporary directory rather
than in-tree, since they are using the actual filesystem.
Fixes https://github.com/flutter/flutter/issues/83665
diff --git a/packages/cross_file/CHANGELOG.md b/packages/cross_file/CHANGELOG.md
index a6ace9a..fe50d36 100644
--- a/packages/cross_file/CHANGELOG.md
+++ b/packages/cross_file/CHANGELOG.md
@@ -1,3 +1,8 @@
+## 0.3.1+4
+
+* The `dart:io` implementation of `saveTo` now does a file copy for path-based
+ `XFile` instances, rather than reading the contents into memory.
+
## 0.3.1+3
* Fix example in README
diff --git a/packages/cross_file/lib/src/types/io.dart b/packages/cross_file/lib/src/types/io.dart
index 930b283..2965cc9 100644
--- a/packages/cross_file/lib/src/types/io.dart
+++ b/packages/cross_file/lib/src/types/io.dart
@@ -13,6 +13,10 @@
/// A CrossFile backed by a dart:io File.
class XFile extends XFileBase {
/// Construct a CrossFile object backed by a dart:io File.
+ ///
+ /// [bytes] is ignored; the parameter exists only to match the web version of
+ /// the constructor. To construct a dart:io XFile from bytes, use
+ /// [XFile.fromData].
XFile(
String path, {
this.mimeType,
@@ -62,9 +66,12 @@
@override
Future<void> saveTo(String path) async {
- final File fileToSave = File(path);
- await fileToSave.writeAsBytes(_bytes ?? (await readAsBytes()));
- await fileToSave.create();
+ if (_bytes == null) {
+ await _file.copy(path);
+ } else {
+ final File fileToSave = File(path);
+ await fileToSave.writeAsBytes(_bytes!);
+ }
}
@override
diff --git a/packages/cross_file/pubspec.yaml b/packages/cross_file/pubspec.yaml
index b8b62ca..371658c 100644
--- a/packages/cross_file/pubspec.yaml
+++ b/packages/cross_file/pubspec.yaml
@@ -2,7 +2,7 @@
description: An abstraction to allow working with files across multiple platforms.
repository: https://github.com/flutter/packages/tree/master/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.1+3
+version: 0.3.1+4
environment:
sdk: ">=2.12.0 <3.0.0"
diff --git a/packages/cross_file/test/x_file_io_test.dart b/packages/cross_file/test/x_file_io_test.dart
index 4affc85..eb42871 100644
--- a/packages/cross_file/test/x_file_io_test.dart
+++ b/packages/cross_file/test/x_file_io_test.dart
@@ -21,36 +21,54 @@
void main() {
group('Create with a path', () {
- final XFile file = XFile(textFilePath);
-
test('Can be read as a string', () async {
+ final XFile file = XFile(textFilePath);
expect(await file.readAsString(), equals(expectedStringContents));
});
test('Can be read as bytes', () async {
+ final XFile file = XFile(textFilePath);
expect(await file.readAsBytes(), equals(bytes));
});
test('Can be read as a stream', () async {
+ final XFile file = XFile(textFilePath);
expect(await file.openRead().first, equals(bytes));
});
test('Stream can be sliced', () async {
+ final XFile file = XFile(textFilePath);
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();
+ final XFile file = XFile(textFilePath);
+ final Directory tempDir = Directory.systemTemp.createTempSync();
+ final File targetFile = File('${tempDir.path}/newFilePath.txt');
+ if (targetFile.existsSync()) {
+ await targetFile.delete();
}
- await file.saveTo(pathPrefix + 'newFilePath.txt');
- final File newFile = File(pathPrefix + 'newFilePath.txt');
+ await file.saveTo(targetFile.path);
- expect(newFile.existsSync(), isTrue);
- expect(newFile.readAsStringSync(), 'Hello, world!');
+ expect(targetFile.existsSync(), isTrue);
+ expect(targetFile.readAsStringSync(), 'Hello, world!');
- await newFile.delete();
+ await tempDir.delete(recursive: true);
+ });
+
+ test('saveTo(..) does not load the file into memory', () async {
+ final TestXFile file = TestXFile(textFilePath);
+ 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(file.hasBeenRead, isFalse);
+
+ await tempDir.delete(recursive: true);
});
});
@@ -73,18 +91,31 @@
});
test('Function saveTo(..) creates file', () async {
- final File removeBeforeTest = File(pathPrefix + 'newFileData.txt');
- if (removeBeforeTest.existsSync()) {
- await removeBeforeTest.delete();
+ final Directory tempDir = Directory.systemTemp.createTempSync();
+ final File targetFile = File('${tempDir.path}/newFilePath.txt');
+ if (targetFile.existsSync()) {
+ await targetFile.delete();
}
- await file.saveTo(pathPrefix + 'newFileData.txt');
- final File newFile = File(pathPrefix + 'newFileData.txt');
+ await file.saveTo(targetFile.path);
- expect(newFile.existsSync(), isTrue);
- expect(newFile.readAsStringSync(), 'Hello, world!');
+ expect(targetFile.existsSync(), isTrue);
+ expect(targetFile.readAsStringSync(), 'Hello, world!');
- await newFile.delete();
+ await tempDir.delete(recursive: true);
});
});
}
+
+/// An XFile subclass that tracks reads, for testing purposes.
+class TestXFile extends XFile {
+ TestXFile(String path) : super(path);
+
+ bool hasBeenRead = false;
+
+ @override
+ Future<Uint8List> readAsBytes() {
+ hasBeenRead = true;
+ return super.readAsBytes();
+ }
+}