Chromium Code Reviews

Unified Diff: sdk/lib/_internal/pub/lib/src/source/hosted.dart

Issue 228703006: Add “pub cache repair” to forcibly re-install previously cached packages. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Revise. Created 6 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
View side-by-side diff with in-line comments
Index: sdk/lib/_internal/pub/lib/src/source/hosted.dart
diff --git a/sdk/lib/_internal/pub/lib/src/source/hosted.dart b/sdk/lib/_internal/pub/lib/src/source/hosted.dart
index 781ab0c9084fa47aa168413070edbb606cd75a9a..470f28ad090dbfb9054fcecf91af1dfc387bbeae 100644
--- a/sdk/lib/_internal/pub/lib/src/source/hosted.dart
+++ b/sdk/lib/_internal/pub/lib/src/source/hosted.dart
@@ -78,28 +78,8 @@ class HostedSource extends Source {
/// Downloads a package from the site and unpacks it.
Future<bool> get(PackageId id, String destPath) {
- return syncFuture(() {
- var url = _makeVersionUrl(id, (server, package, version) =>
- "$server/packages/$package/versions/$version.tar.gz");
- log.io("Get package from $url.");
-
- log.message('Downloading ${id.name} ${id.version}...');
-
- // Download and extract the archive to a temp directory.
- var tempDir = systemCache.createTempDir();
- return httpClient.send(new http.Request("GET", url))
- .then((response) => response.stream)
- .then((stream) {
- return timeout(extractTarGz(stream, tempDir), HTTP_TIMEOUT,
- 'fetching URL "$url"');
- }).then((_) {
- // Now that the get has succeeded, move it to the real location in the
- // cache. This ensures that we don't leave half-busted ghost
- // directories in the user's pub cache if a get fails.
- renameDir(tempDir, destPath);
- return true;
- });
- });
+ var parsed = _parseDescription(id.description);
+ return _download(parsed.last, parsed.first, id.version, destPath);
}
/// The system cache directory for the hosted source contains subdirectories
@@ -108,7 +88,7 @@ class HostedSource extends Source {
/// from that site.
Future<String> systemCacheDirectory(PackageId id) {
var parsed = _parseDescription(id.description);
- var dir = _getSourceDirectory(parsed.last);
+ var dir = _urlToDirectory(parsed.last);
return new Future.value(
path.join(systemCacheRoot, dir, "${parsed.first}-${id.version}"));
@@ -130,25 +110,77 @@ class HostedSource extends Source {
return description;
}
- List<Package> getCachedPackages([String url]) {
- if (url == null) url = defaultUrl;
+ /// Re-downloads all packages that have been previously downloaded into the
+ /// system cache from any server.
+ Future<Pair<int, int>> repairCachedPackages() {
+ if (!dirExists(systemCacheRoot)) return new Future.value(new Pair(0, 0));
+
+ var successes = 0;
+ var failures = 0;
+
+ return Future.wait(listDir(systemCacheRoot).map((serverDir) {
+ var url = _directoryToUrl(path.basename(serverDir));
+ var packages = _getCachedPackagesInDirectory(path.basename(serverDir));
+ packages.sort(Package.orderByNameAndVersion);
+ return Future.wait(packages.map((package) {
+ return _download(url, package.name, package.version, package.dir).then((_) {
nweiz 2014/04/15 00:59:26 Long line.
Bob Nystrom 2014/04/16 17:07:44 Done.
+ successes++;
+ }).catchError((error, stackTrace) {
+ failures++;
+ log.error("Failed to repair ${log.bold(package.name)} "
+ "${package.version} from $url. Error:\n$error");
nweiz 2014/04/15 00:59:26 I'd omit "from $url" if it's pub.dartlang.org.
Bob Nystrom 2014/04/16 17:07:44 Done.
+ log.fine(stackTrace);
+ });
+ }));
+ })).then((_) => new Pair(successes, failures));
+ }
- var cacheDir = path.join(systemCacheRoot,
- _getSourceDirectory(url));
+ /// Gets all of the packages that have been downloaded into the system cache
+ /// from the default server.
+ List<Package> getCachedPackages() {
+ return _getCachedPackagesInDirectory(_urlToDirectory(defaultUrl));
+ }
+
+ /// Gets all of the packages that have been downloaded into the system cache
+ /// into [dir].
+ List<Package> _getCachedPackagesInDirectory(String dir) {
+ var cacheDir = path.join(systemCacheRoot, dir);
if (!dirExists(cacheDir)) return [];
- return listDir(path.join(cacheDir)).map((entry) {
- // TODO(keertip): instead of catching exception in pubspec parse with
- // sdk dependency, fix to parse and report usage of sdk dependency.
- // dartbug.com/10190
- try {
- return new Package.load(null, entry, systemCache.sources);
- } on ArgumentError catch (e) {
- log.error(e);
- }
- }).where((package) => package != null).toList();
+ return listDir(cacheDir)
+ .map((entry) => new Package.load(null, entry, systemCache.sources))
+ .toList();
}
+ /// Downloads package [package] at [version] from [server], and unpacks it
+ /// into [destPath].
+ Future<bool> _download(String server, String package, Version version,
+ String destPath) {
+ return syncFuture(() {
+ var url = Uri.parse("$server/packages/$package/versions/$version.tar.gz");
+ log.io("Get package from $url.");
+ log.message('Downloading ${log.bold(package)} ${version}...');
+
+ // Download and extract the archive to a temp directory.
+ var tempDir = systemCache.createTempDir();
+ return httpClient.send(new http.Request("GET", url))
+ .then((response) => response.stream)
+ .then((stream) {
+ return timeout(extractTarGz(stream, tempDir), HTTP_TIMEOUT,
+ 'fetching URL "$url"');
+ }).then((_) {
+ // Remove the existing directory if it exists. This will happen if
+ // we're forcing a download to repair the cache.
+ if (dirExists(destPath)) deleteEntry(destPath);
+
+ // Now that the get has succeeded, move it to the real location in the
+ // cache. This ensures that we don't leave half-busted ghost
+ // directories in the user's pub cache if a get fails.
+ renameDir(tempDir, destPath);
+ return true;
+ });
+ });
+ }
/// When an error occurs trying to read something about [package] from [url],
/// this tries to translate into a more user friendly error message. Always
/// throws an error, either the original one or a better one.
@@ -185,8 +217,8 @@ class OfflineHostedSource extends HostedSource {
var parsed = _parseDescription(description);
var server = parsed.last;
log.io("Finding versions of $name in "
- "${systemCache.rootDir}/${_getSourceDirectory(server)}");
- return getCachedPackages(server)
+ "$systemCacheRoot/${_urlToDirectory(server)}");
+ return _getCachedPackagesInDirectory(_urlToDirectory(server))
.where((package) => package.name == name)
.map((package) => package.version)
.toList();
@@ -212,12 +244,58 @@ class OfflineHostedSource extends HostedSource {
}
}
-String _getSourceDirectory(String url) {
+/// Given a URL, returns a "normalized" string to be used as a directory name
+/// for packages downloaded from the server at that URL.
+///
+/// This normalization strips off the scheme (which is presumed to be HTTP or
+/// HTTPS) and *sort of* URL-encodes it. I say "sort of" because it does it
+/// incorrectly: it uses the character's *decimal* ASCII value instead of hex.
+///
+/// This could cause an ambiguity since some characters get encoded as three
+/// digits and others two. It's possible for one to be a prefix of the other.
+/// In practice, the set of characters that are encoded don't happen to have
+/// any collisions, so the encoding is reversible.
+///
+/// This behavior is a bug, but is being preserved for compatibility.
+String _urlToDirectory(String url) {
url = url.replaceAll(new RegExp(r"^https?://"), "");
return replace(url, new RegExp(r'[<>:"\\/|?*%]'),
(match) => '%${match[0].codeUnitAt(0)}');
}
+/// Given a directory name in the system cache, returns the URL of the server
+/// whose packages it contains.
+///
+/// See [_urlToDirectory] for details on the mapping. Note that because the
+/// directory name does not preserve the scheme, this has to guess at it. It
+/// chooses "http" for loopback URLs (mainly to support the pub tests) and
+/// "https" for all others.
+String _directoryToUrl(String url) {
+ // Decode the pseudo-URL-encoded characters.
+ var chars = '<>:"\\/|?*%';
+ for (var i = 0; i < chars.length; i++) {
+ var c = chars.substring(i, i + 1);
+ url = url.replaceAll("%${c.codeUnitAt(0)}", c);
+ }
+
+ // Figure out the scheme.
+ var scheme = "https";
+
+ // See if it's a loopback IP address.
+ try {
+ var urlWithoutPort = url.replaceAll(new RegExp(":.*"), "");
+ var address = new io.InternetAddress(urlWithoutPort);
+ if (address.isLoopback) scheme = "http";
+ } on ArgumentError catch(error) {
+ // If we got here, it's not a raw IP address, so it's probably a regular
+ // URL.
+ }
+
+ if (url == "localhost") scheme = "http";
+
+ return "$scheme://$url";
+}
+
/// Parses [description] into its server and package name components, then
/// converts that to a Uri given [pattern]. Ensures the package name is
/// properly URL encoded.

Powered by Google App Engine