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步:
获取 上传地址,返回地址 http://127.0.0.1:4000/api/packages/versions/newUpload
对应pub server接口为
@Route.get('/api/packages/versions/new')
文件流为之前第一步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.';
-
如果上传失败 pub server返回: http://127.0.0.1:4000/api/packages/versions/newUploadFinish?error=missing%20authorization%20header
-
如果上传成功, pub server返回: http://127.0.0.1:4000/api/packages/versions/newUploadFinish
对应pub server端接口为:
@Route.get('/api/packages/versions/newUploadFinish')
到此与pub server的交互完成。