blob: 214e240509669825dfe776a6bcdbd558ef393a7b [file] [log] [blame]
// Copyright 2014 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:io';
import 'dart:math' as math;
import 'package:image/image.dart';
import 'package:path/path.dart';
String authorizationToken;
class UploadError extends Error {
UploadError(this.message);
final String message;
@override
String toString() => 'UploadError($message)';
}
void logMessage(String s) { print(s); }
class Upload {
Upload(this.fromPath, this.largeName, this.smallName);
static math.Random random;
static const String uriAuthority = 'www.googleapis.com';
static const String uriPath = 'upload/storage/v1/b/flutter-catalog/o';
final String fromPath;
final String largeName;
final String smallName;
List<int> largeImage;
List<int> smallImage;
bool largeImageSaved = false;
int retryCount = 0;
bool isComplete = false;
// Exponential backoff per https://cloud.google.com/storage/docs/exponential-backoff
Duration get timeLimit {
if (retryCount == 0)
return const Duration(milliseconds: 1000);
random ??= math.Random();
return Duration(milliseconds: random.nextInt(1000) + (math.pow(2, retryCount) as int) * 1000);
}
Future<bool> save(HttpClient client, String name, List<int> content) async {
try {
final Uri uri = Uri.https(uriAuthority, uriPath, <String, String>{
'uploadType': 'media',
'name': name,
});
final HttpClientRequest request = await client.postUrl(uri);
request
..headers.contentType = ContentType('image', 'png')
..headers.add('Authorization', 'Bearer $authorizationToken')
..add(content);
final HttpClientResponse response = await request.close().timeout(timeLimit);
if (response.statusCode == HttpStatus.ok) {
logMessage('Saved $name');
await response.drain<void>();
} else {
// TODO(hansmuller): only retry on 5xx and 429 responses
logMessage('Request to save "$name" (length ${content.length}) failed with status ${response.statusCode}, will retry');
logMessage(await response.cast<List<int>>().transform<String>(utf8.decoder).join());
}
return response.statusCode == HttpStatus.ok;
} on TimeoutException catch (_) {
logMessage('Request to save "$name" (length ${content.length}) timed out, will retry');
return false;
}
}
Future<bool> run(HttpClient client) async {
assert(!isComplete);
if (retryCount > 2)
throw UploadError('upload of "$fromPath" to "$largeName" and "$smallName" failed after 2 retries');
largeImage ??= await File(fromPath).readAsBytes();
smallImage ??= encodePng(copyResize(decodePng(largeImage), width: 300));
if (!largeImageSaved)
largeImageSaved = await save(client, largeName, largeImage);
isComplete = largeImageSaved && await save(client, smallName, smallImage);
retryCount += 1;
return isComplete;
}
static bool isNotComplete(Upload upload) => !upload.isComplete;
}
Future<void> saveScreenshots(List<String> fromPaths, List<String> largeNames, List<String> smallNames) async {
assert(fromPaths.length == largeNames.length);
assert(fromPaths.length == smallNames.length);
List<Upload> uploads = List<Upload>(fromPaths.length);
for (int index = 0; index < uploads.length; index += 1)
uploads[index] = Upload(fromPaths[index], largeNames[index], smallNames[index]);
while (uploads.any(Upload.isNotComplete)) {
final HttpClient client = HttpClient();
uploads = uploads.where(Upload.isNotComplete).toList();
await Future.wait<bool>(uploads.map<Future<bool>>((Upload upload) => upload.run(client)));
client.close(force: true);
}
}
// If path is lib/foo.png then screenshotName is foo.
String screenshotName(String path) => basenameWithoutExtension(path);
Future<void> saveCatalogScreenshots({
Directory directory, // Where the *.png screenshots are.
String commit, // The commit hash to be used as a cloud storage "directory".
String token, // Cloud storage authorization token.
String prefix, // Prefix for all file names.
}) async {
final List<String> screenshots = <String>[
for (final FileSystemEntity entity in directory.listSync())
if (entity is File && entity.path.endsWith('.png'))
entity.path,
];
final List<String> largeNames = <String>[]; // Cloud storage names for the full res screenshots.
final List<String> smallNames = <String>[]; // Likewise for the scaled down screenshots.
for (final String path in screenshots) {
final String name = screenshotName(path);
largeNames.add('$commit/$prefix$name.png');
smallNames.add('$commit/$prefix${name}_small.png');
}
authorizationToken = token;
await saveScreenshots(screenshots, largeNames, smallNames);
}