save_catalog_screenshots.dart 4.81 KB
Newer Older
1 2 3 4
// Copyright 2017 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.

5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
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;
  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 ??= new math.Random();
    return new Duration(milliseconds: random.nextInt(1000) + math.pow(2, retryCount) * 1000);
  }

  Future<bool> save(HttpClient client, String name, List<int> content) async {
    try {
      final Uri uri = new Uri.https(uriAuthority, uriPath, <String, String>{
        'uploadType': 'media',
        'name': name,
      });
      final HttpClientRequest request = await client.postUrl(uri);
      request
        ..headers.contentType = new ContentType('image', 'png')
        ..headers.add('Authorization', 'Bearer $authorizationToken')
        ..add(content);

      final HttpClientResponse response = await request.close().timeout(timeLimit);
      if (response.statusCode == HttpStatus.OK) {
63
        logMessage('Saved $name');
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
        await response.drain<Null>();
      } 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.transform(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 new UploadError('upload of "$fromPath" to "$largeName" and "$smallName" failed after 2 retries');

    largeImage ??= await new File(fromPath).readAsBytes();
83
    smallImage ??= encodePng(copyResize(decodePng(largeImage), 300));
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103

    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<Null> saveScreenshots(List<String> fromPaths, List<String> largeNames, List<String> smallNames) async {
  assert(fromPaths.length == largeNames.length);
  assert(fromPaths.length == smallNames.length);

  List<Upload> uploads = new List<Upload>(fromPaths.length);
  for (int index = 0; index < uploads.length; index += 1)
    uploads[index] = new Upload(fromPaths[index], largeNames[index], smallNames[index]);

104
  while (uploads.any(Upload.isNotComplete)) {
105
    final HttpClient client = new HttpClient();
106 107
    uploads = uploads.where(Upload.isNotComplete).toList();
    await Future.wait(uploads.map((Upload upload) => upload.run(client)));
108
    client.close(force: true);
109 110 111 112 113 114 115
  }
}


// If path is lib/foo.png then screenshotName is foo.
String screenshotName(String path) => basenameWithoutExtension(path);

116 117 118 119 120 121
Future<Null> 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 {
122
  final List<String> screenshots = <String>[];
123
  for (FileSystemEntity entity in directory.listSync()) {
124 125 126 127
    if (entity is File && entity.path.endsWith('.png')) {
      final File file = entity;
      screenshots.add(file.path);
    }
128
  }
129 130 131 132 133

  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 (String path in screenshots) {
    final String name = screenshotName(path);
134 135
    largeNames.add('$commit/$prefix$name.png');
    smallNames.add('$commit/$prefix${name}_small.png');
136 137
  }

138
  authorizationToken = token;
139 140
  await saveScreenshots(screenshots, largeNames, smallNames);
}