Make artifact downloading more robust for flaky networks (#13984)
* First version
* Add a clarifying comment
* Add verify to ZipDecoder.decodeBytes call
* Review comments
* Need OS Error for missing files
* FileSystemException, not OSError.
diff --git a/packages/flutter_tools/lib/src/base/os.dart b/packages/flutter_tools/lib/src/base/os.dart
index 476cae1..a1c7d8a 100644
--- a/packages/flutter_tools/lib/src/base/os.dart
+++ b/packages/flutter_tools/lib/src/base/os.dart
@@ -47,8 +47,14 @@
void unzip(File file, Directory targetDirectory);
+ /// Returns true if the ZIP is not corrupt.
+ bool verifyZip(File file);
+
void unpack(File gzippedTarFile, Directory targetDirectory);
+ /// Returns true if the gzip is not corrupt (does not check tar).
+ bool verifyGzip(File gzippedFile);
+
/// Returns a pretty name string for the current operating system.
///
/// If available, the detailed version of the OS is included.
@@ -99,6 +105,9 @@
runSync(<String>['unzip', '-o', '-q', file.path, '-d', targetDirectory.path]);
}
+ @override
+ bool verifyZip(File zipFile) => exitsHappy(<String>['zip', '-T', zipFile.path]);
+
// tar -xzf tarball -C dest
@override
void unpack(File gzippedTarFile, Directory targetDirectory) {
@@ -106,6 +115,9 @@
}
@override
+ bool verifyGzip(File gzippedFile) => exitsHappy(<String>['gzip', '-t', gzippedFile.path]);
+
+ @override
File makePipe(String path) {
runSync(<String>['mkfifo', path]);
return fs.file(path);
@@ -179,6 +191,18 @@
}
@override
+ bool verifyZip(File zipFile) {
+ try {
+ new ZipDecoder().decodeBytes(zipFile.readAsBytesSync(), verify: true);
+ } on FileSystemException catch (_) {
+ return false;
+ } on ArchiveException catch (_) {
+ return false;
+ }
+ return true;
+ }
+
+ @override
void unpack(File gzippedTarFile, Directory targetDirectory) {
final Archive archive = new TarDecoder().decodeBytes(
new GZipDecoder().decodeBytes(gzippedTarFile.readAsBytesSync()),
@@ -186,6 +210,18 @@
_unpackArchive(archive, targetDirectory);
}
+ @override
+ bool verifyGzip(File gzipFile) {
+ try {
+ new GZipDecoder().decodeBytes(gzipFile.readAsBytesSync(), verify: true);
+ } on FileSystemException catch (_) {
+ return false;
+ } on ArchiveException catch (_) {
+ return false;
+ }
+ return true;
+ }
+
void _unpackArchive(Archive archive, Directory targetDirectory) {
for (ArchiveFile archiveFile in archive.files) {
// The archive package doesn't correctly set isFile.
diff --git a/packages/flutter_tools/lib/src/cache.dart b/packages/flutter_tools/lib/src/cache.dart
index fd9dcb9..503ffbd 100644
--- a/packages/flutter_tools/lib/src/cache.dart
+++ b/packages/flutter_tools/lib/src/cache.dart
@@ -162,6 +162,9 @@
return dir;
}
+ /// Return the top-level directory for artifact downloads.
+ Directory getDownloadDir() => getCacheDir('downloads');
+
/// Return the top-level mutable directory in the cache; this is `bin/cache/artifacts`.
Directory getCacheArtifacts() => getCacheDir('artifacts');
@@ -232,6 +235,12 @@
Directory get location => cache.getArtifactDirectory(name);
String get version => cache.getVersionFor(name);
+ /// Keep track of the files we've downloaded for this execution so we
+ /// can delete them after completion. We don't delete them right after
+ /// extraction in case [update] is interrupted, so we can restart without
+ /// starting from scratch.
+ final List<File> _downloadedFiles = <File>[];
+
bool isUpToDate() {
if (!location.existsSync())
return false;
@@ -246,6 +255,14 @@
location.createSync(recursive: true);
await updateInner();
cache.setStampFor(name, version);
+ _removeDownloadedFiles();
+ }
+
+ /// Clear any zip/gzip files downloaded.
+ void _removeDownloadedFiles() {
+ for (File f in _downloadedFiles) {
+ f.deleteSync();
+ }
}
/// Hook method for extra checks for being up-to-date.
@@ -263,6 +280,36 @@
}
Uri _toStorageUri(String path) => Uri.parse('$_storageBaseUrl/$path');
+
+ /// Download a zip archive from the given [url] and unzip it to [location].
+ Future<Null> _downloadZipArchive(Uri url, Directory location) {
+ return _withDownloadFile('${_flattenName(url)}.zip', (File tempFile) async {
+ if (!os.verifyZip(tempFile)) {
+ await _downloadFile(url, tempFile);
+ }
+ _ensureExists(location);
+ os.unzip(tempFile, location);
+ });
+ }
+
+ /// Download a gzipped tarball from the given [url] and unpack it to [location].
+ Future<Null> _downloadZippedTarball(Uri url, Directory location) {
+ return _withDownloadFile('${_flattenName(url)}.tgz', (File tempFile) async {
+ if (!os.verifyGzip(tempFile)) {
+ await _downloadFile(url, tempFile);
+ }
+ _ensureExists(location);
+ os.unpack(tempFile, location);
+ });
+ }
+
+ /// Create a temporary file and invoke [onTemporaryFile] with the file as
+ /// argument, then add the temporary file to the [_downloadedFiles].
+ Future<Null> _withDownloadFile(String name, Future<Null> onTemporaryFile(File file)) async {
+ final File tempFile = fs.file(fs.path.join(cache.getDownloadDir().path, name));
+ _downloadedFiles.add(tempFile);
+ await onTemporaryFile(tempFile);
+ }
}
bool _hasWarnedAboutStorageOverride = false;
@@ -446,6 +493,32 @@
}
}
+// Many characters are problematic in filenames, especially on Windows.
+final Map<int, List<int>> _flattenNameSubstitutions = <int, List<int>>{
+ r'@'.codeUnitAt(0): '@@'.codeUnits,
+ r'/'.codeUnitAt(0): '@s@'.codeUnits,
+ r'\'.codeUnitAt(0): '@bs@'.codeUnits,
+ r':'.codeUnitAt(0): '@c@'.codeUnits,
+ r'%'.codeUnitAt(0): '@per@'.codeUnits,
+ r'*'.codeUnitAt(0): '@ast@'.codeUnits,
+ r'<'.codeUnitAt(0): '@lt@'.codeUnits,
+ r'>'.codeUnitAt(0): '@gt@'.codeUnits,
+ r'"'.codeUnitAt(0): '@q@'.codeUnits,
+ r'|'.codeUnitAt(0): '@pip@'.codeUnits,
+ r'?'.codeUnitAt(0): '@ques@'.codeUnits
+};
+
+/// Given a name containing slashes, colons, and backslashes, expand it into
+/// something that doesn't.
+String _flattenName(Uri url) {
+ final String urlName = url.toString();
+ final List<int> replacedCodeUnits = <int>[];
+ for (int codeUnit in urlName.codeUnits) {
+ replacedCodeUnits.addAll(_flattenNameSubstitutions[codeUnit] ?? <int>[codeUnit]);
+ }
+ return new String.fromCharCodes(replacedCodeUnits);
+}
+
/// Download a file from the given [url] and write it to [location].
Future<Null> _downloadFile(Uri url, File location) async {
_ensureExists(location.parent);
@@ -453,35 +526,6 @@
location.writeAsBytesSync(fileBytes, flush: true);
}
-/// Download a zip archive from the given [url] and unzip it to [location].
-Future<Null> _downloadZipArchive(Uri url, Directory location) {
- return _withTemporaryFile('download.zip', (File tempFile) async {
- await _downloadFile(url, tempFile);
- _ensureExists(location);
- os.unzip(tempFile, location);
- });
-}
-
-/// Download a gzipped tarball from the given [url] and unpack it to [location].
-Future<Null> _downloadZippedTarball(Uri url, Directory location) {
- return _withTemporaryFile('download.tgz', (File tempFile) async {
- await _downloadFile(url, tempFile);
- _ensureExists(location);
- os.unpack(tempFile, location);
- });
-}
-
-/// Create a file with the given name in a new temporary directory, invoke
-/// [onTemporaryFile] with the file as argument, then delete the temporary
-/// directory.
-Future<Null> _withTemporaryFile(String name, Future<Null> onTemporaryFile(File file)) async {
- final Directory tempDir = fs.systemTempDirectory.createTempSync();
- final File tempFile = fs.file(fs.path.join(tempDir.path, name));
- await onTemporaryFile(tempFile).whenComplete(() {
- tempDir.delete(recursive: true);
- });
-}
-
/// Create the given [directory] and parents, as necessary.
void _ensureExists(Directory directory) {
if (!directory.existsSync())