从源代码分析pub publish上传unpub server流程

557 阅读2分钟

pub publish --server=http://127.0.0.1:4000

pub server 和 pub、Google OAuth2 客户端交互关系

pub 工具源代码 github.com/dart-lang/p…

1、runProtected

 Future runProtected() async {
    if (argResults.wasParsed('server')) {
      await log.warningsOnlyUnlessTerminal(() {
        log.message(
          '''
The --server option is deprecated. Use `publish_to` in your pubspec.yaml or set
the $PUB_HOSTED_URL environment variable.''',
        );
      });
    }

    if (force && dryRun) {
      usageException('Cannot use both --force and --dry-run.');
    }

    if (entrypoint.root.pubspec.isPrivate) {
      dataError('A private package cannot be published.\n'
          'You can enable this by changing the "publish_to" field in your '
          'pubspec.');
    }

    var files = entrypoint.root.listFiles();
    log.fine('Archiving and publishing ${entrypoint.root.name}.');

    // Show the package contents so the user can verify they look OK.
    var package = entrypoint.root;
    log.message('Publishing ${package.name} ${package.version} to $host:\n'
        '${tree.fromFiles(files, baseDir: entrypoint.root.dir)}');

    var packageBytesFuture =
        createTarGz(files, baseDir: entrypoint.root.dir).toBytes();

    // Validate the package.
    var isValid = await _validate(
        packageBytesFuture.then((bytes) => bytes.length), files);
    if (!isValid) {
      overrideExitCode(exit_codes.DATA);
      return;
    } else if (dryRun) {
      log.message('The server may enforce additional checks.');
      return;
    } else {
      await _publish(await packageBytesFuture);
    }
  }

通过pubspec.yaml分析所有源代码文件。

2、_validate

Future<bool> _validate(Future<int> packageSize, List<String> files) async {
  final hints = <String>[];
  final warnings = <String>[];
  final errors = <String>[];

  await Validator.runAll(
    entrypoint,
    packageSize,
    host,
    files,
    hints: hints,
    warnings: warnings,
    errors: errors,
  );

  if (errors.isNotEmpty) {
    log.error('Sorry, your package is missing '
        "${(errors.length > 1) ? 'some requirements' : 'a requirement'} "
        "and can't be published yet.\nFor more information, see: "
        'https://dart.dev/tools/pub/cmd/pub-lish.\n');
    return false;
  }

  if (force) return true;

  String formatWarningCount() {
    final hs = hints.length == 1 ? '' : 's';
    final hintText = hints.isEmpty ? '' : ' and ${hints.length} hint$hs.';
    final ws = warnings.length == 1 ? '' : 's';
    return '\nPackage has ${warnings.length} warning$ws$hintText.';
  }

  if (dryRun) {
    log.warning(formatWarningCount());
    return warnings.isEmpty;
  }

  log.message('\nPublishing is forever; packages cannot be unpublished.'
      '\nPolicy details are available at https://pub.dev/policy');

  final package = entrypoint.root;
  var message = 'Do you want to publish ${package.name} ${package.version}';

  if (warnings.isNotEmpty || hints.isNotEmpty) {
    final warning = formatWarningCount();
    message = '${log.bold(log.red(warning))}. $message';
  }

  var confirmed = await confirm('\n$message');
  if (!confirmed) {
    log.error('Package upload canceled.');
    return false;
  }
  return true;
}

dart 代码校验。

3、 Validator

Validator.runAll( entrypoint, packageSize, host, files, hints: hints, warnings: warnings, errors: errors, );

static Future<void> runAll(Entrypoint entrypoint, Future<int> packageSize,
    Uri serverUrl, List<String> files,
    {required List<String> hints,
    required List<String> warnings,
    required List<String> errors}) async {
  var validators = [
    GitignoreValidator(),
    PubspecValidator(),
    LicenseValidator(),
    NameValidator(),
    PubspecFieldValidator(),
    DependencyValidator(),
    DependencyOverrideValidator(),
    DeprecatedFieldsValidator(),
    DirectoryValidator(),
    ExecutableValidator(),
    CompiledDartdocValidator(),
    ReadmeValidator(),
    ChangelogValidator(),
    SdkConstraintValidator(),
    StrictDependenciesValidator(),
    FlutterConstraintValidator(),
    FlutterPluginFormatValidator(),
    LanguageVersionValidator(),
    RelativeVersionNumberingValidator(),
    NullSafetyMixedModeValidator(),
    PubspecTypoValidator(),
    LeakDetectionValidator(),
    SizeValidator(),
  ];

  final context = ValidationContext(
    entrypoint,
    await packageSize,
    serverUrl,
    files,
  );
  return await Future.wait(validators.map((validator) async {
    validator.context = context;
    await validator.validate();
  })).then((_) {
    hints.addAll([for (final validator in validators) ...validator.hints]);
    warnings
        .addAll([for (final validator in validators) ...validator.warnings]);
    errors.addAll([for (final validator in validators) ...validator.errors]);

    if (errors.isNotEmpty) {
      final s = errors.length > 1 ? 's' : '';
      log.error('Package validation found the following error$s:');
      for (var error in errors) {
        log.error("* ${error.split('\n').join('\n  ')}");
      }
      log.error('');
    }

    if (warnings.isNotEmpty) {
      final s = warnings.length > 1 ? 's' : '';
      log.warning(
        'Package validation found the following potential issue$s:',
      );
      for (var warning in warnings) {
        log.warning("* ${warning.split('\n').join('\n  ')}");
      }
      log.warning('');
    }

    if (hints.isNotEmpty) {
      final s = hints.length > 1 ? 's' : '';
      log.warning(
        'Package validation found the following hint$s:',
      );
      for (var hint in hints) {
        log.warning("* ${hint.split('\n').join('\n  ')}");
      }
      log.warning('');
    }
  });
}

4、 RelativeVersionNumberingValidator()

Future<void> validate() async {
  final hostedSource = entrypoint.cache.hosted;
  List<PackageId> existingVersions;
  try {
    existingVersions = await entrypoint.cache.getVersions(
      hostedSource.refFor(entrypoint.root.name, url: serverUrl.toString()),
    );
  } on PackageNotFoundException {
    existingVersions = [];
  }
  existingVersions.sort((a, b) => a.version.compareTo(b.version));
  final previousVersion = existingVersions
      .lastWhereOrNull((id) => id.version < entrypoint.root.version);
  if (previousVersion == null) return;

  final previousPubspec = await entrypoint.cache.describe(previousVersion);

  final currentOptedIn =
      entrypoint.root.pubspec.languageVersion.supportsNullSafety;
  final previousOptedIn = previousPubspec.languageVersion.supportsNullSafety;

  if (currentOptedIn && !previousOptedIn) {
    hints.add(
        'You're about to publish a package that opts into null safety.\n'
        'The previous version (${previousVersion.version}) isn't opted in.\n'
        'See ${NullSafetyAnalysis.guideUrl} for best practices.');
  } else if (!currentOptedIn && previousOptedIn) {
    hints.add(
        'You're about to publish a package that doesn't opt into null safety,\n'
        'but the previous version (${previousVersion.version}) was opted in.\n'
        'This change is likely to be backwards incompatible.\n'
        'See $semverUrl for information about versioning.');
  }
}

请求pub server 对应包 版本号 请求server 接口为: @Route.get('/api/packages/<name>')

existingVersions = await entrypoint.cache.getVersions(
  hostedSource.refFor(entrypoint.root.name, url: serverUrl.toString()),

5、_publish

Future<void> _publish(List<int> packageBytes) async {
  try {
    final officialPubServers = {
      'https://pub.dartlang.org',
      // [validateAndNormalizeHostedUrl] normalizes https://pub.dev to
      // https://pub.dartlang.org, so we don't need to do allow that here.

      // Pub uses oauth2 credentials only for authenticating official pub
      // servers for security purposes (to not expose pub.dev access token to
      // 3rd party servers).
      // For testing publish command we're using mock servers hosted on
      // localhost address which is not a known pub server address. So we
      // explicitly have to define mock servers as official server to test
      // publish command with oauth2 credentials.
      if (runningFromTest &&
          Platform.environment.containsKey('_PUB_TEST_DEFAULT_HOSTED_URL'))
        Platform.environment['_PUB_TEST_DEFAULT_HOSTED_URL'],
    };

    // Using OAuth2 authentication client for the official pub servers
    final isOfficalServer = officialPubServers.contains(host.toString());
    if (isOfficalServer && !cache.tokenStore.hasCredential(host)) {
      // Using OAuth2 authentication client for the official pub servers, when
      // we don't have an explicit token from [TokenStore] to use instead.
      //
      // This allows us to use `dart pub token add` to inject a token for use
      // with the official servers.
      await oauth2.withClient(cache, (client) {
        return _publishUsingClient(packageBytes, client);
      });
    } else {
      // For third party servers using bearer authentication client
      await withAuthenticatedClient(cache, host, (client) {
        return _publishUsingClient(packageBytes, client);
      });
    }
  } on PubHttpException catch (error) {
    var url = error.response.request!.url;
    if (Uri.parse(url.origin) == Uri.parse(host.origin)) {
      handleJsonError(error.response);
    } else {
      rethrow;
    }
  }
}

因为这里发布到私有pub server, 没有使用OAuth2流程。

await withAuthenticatedClient(cache, host, (client) { return _publishUsingClient(packageBytes, client); });

withAuthenticatedClient

Future<T> withAuthenticatedClient<T>(
  SystemCache systemCache,
  Uri hostedUrl,
  Future<T> Function(http.Client) fn,
) async {
  final credential = systemCache.tokenStore.findCredential(hostedUrl);
  final client = _AuthenticatedClient(httpClient, credential);

  try {
    return await fn(client);
  } finally {
    if (client._detectInvalidCredentials) {
      // try to remove the credential, if we detected that it is invalid!
      final removed = systemCache.tokenStore.removeCredential(hostedUrl);
      if (removed) {
        log.warning('Invalid token for $hostedUrl deleted.');
      }
    }
  }
}

这里server 地址hostedUrl 为http://127.0.0.1:4000

_publishUsingClient

文件上传:

Future<void> _publishUsingClient(
  List<int> packageBytes,
  http.Client client,
) async {
  Uri? cloudStorageUrl;

  try {
    await log.progress('Uploading', () async {
      var newUri = host.resolve('api/packages/versions/new');
      var response = await client.get(newUri, headers: pubApiHeaders);
      var parameters = parseJsonResponse(response);

      var url = _expectField(parameters, 'url', response);
      if (url is! String) invalidServerResponse(response);
      cloudStorageUrl = Uri.parse(url);
      // TODO(nweiz): Cloud Storage can provide an XML-formatted error. We
      // should report that error and exit.
      var request = http.MultipartRequest('POST', cloudStorageUrl!);

      var fields = _expectField(parameters, 'fields', response);
      if (fields is! Map) invalidServerResponse(response);
      fields.forEach((key, value) {
        if (value is! String) invalidServerResponse(response);
        request.fields[key] = value;
      });

      request.followRedirects = false;
      request.files.add(http.MultipartFile.fromBytes('file', packageBytes,
          filename: 'package.tar.gz'));
      var postResponse =
          await http.Response.fromStream(await client.send(request));

      var location = postResponse.headers['location'];
      if (location == null) throw PubHttpException(postResponse);
      handleJsonSuccess(
          await client.get(Uri.parse(location), headers: pubApiHeaders));
    });
  } on AuthenticationException catch (error) {
    var msg = '';
    if (error.statusCode == 401) {
      msg += '$host package repository requested authentication!\n'
          'You can provide credentials using:\n'
          '    pub token add $host\n';
    }
    if (error.statusCode == 403) {
      msg += 'Insufficient permissions to the resource at the $host '
          'package repository.\nYou can modify credentials using:\n'
          '    pub token add $host\n';
    }
    if (error.serverMessage != null) {
      msg += '\n' + error.serverMessage! + '\n';
    }
    dataError(msg + log.red('Authentication failed!'));
  } on PubHttpException catch (error) {
    var url = error.response.request!.url;
    if (url == cloudStorageUrl) {
      // TODO(nweiz): the response may have XML-formatted information about
      // the error. Try to parse that out once we have an easily-accessible
      // XML parser.
      fail(log.red('Failed to upload the package.'));
    } else if (Uri.parse(url.origin) == Uri.parse(host.origin)) {
      handleJsonError(error.response);
    } else {
      rethrow;
    }
  }
}

请求pub server 过程为如下3步:

  1. 第一步:请求 接口http://127.0.0.1:4000/api/packages/versions/new

获取 上传地址,返回地址 http://127.0.0.1:4000/api/packages/versions/newUpload

对应pub server接口为 @Route.get('/api/packages/versions/new')

  1. 第二步, 请求http://127.0.0.1:4000/api/packages/versions/newUpload

文件流为之前第一步runProtected 中通过createTarGz 方法打包得到。

var packageBytesFuture =
    createTarGz(files, baseDir: entrypoint.root.dir).toBytes();

文件流传输 POST提交方法,通过MultipartFile格式进行封装。 http.MultipartRequest('POST', cloudStorageUrl!);方法调用文件流网络上传对象。 添加文件流二进制data 数据。

request.files.add(http.MultipartFile.fromBytes('file', packageBytes,
   filename: 'package.tar.gz'));

请求文件上传server接口: var postResponse = await http.Response.fromStream(await client.send(request));

对应pub server 端接口为 @Route.post('/api/packages/versions/newUpload')

server 做2个检查 从request请求中的header读区authorization字段,取出token。 检查token是否正确

var authHeader = req.headers[HttpHeaders.authorizationHeader];
if (authHeader == null) throw 'missing authorization header';

var token = authHeader.split(' ').last;

检查版本号。如果版本号相同,则返回上传失败: throw 'version invalid: $name@$version already exists.';

  1. 第三步,请求 http://127.0.0.1:4000/api/packages/versions/newUploadFinish

对应pub server端接口为: @Route.get('/api/packages/versions/newUploadFinish')

到此与pub server的交互完成。