OLD | NEW |
---|---|
1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file |
2 // for details. All rights reserved. Use of this source code is governed by a | 2 // for details. All rights reserved. Use of this source code is governed by a |
3 // BSD-style license that can be found in the LICENSE file. | 3 // BSD-style license that can be found in the LICENSE file. |
4 | 4 |
5 import 'dart:async'; | 5 import 'dart:async'; |
6 import 'dart:io' as io; | 6 import 'dart:io' as io; |
7 import "dart:convert"; | 7 import "dart:convert"; |
8 | 8 |
9 import 'package:http/http.dart' as http; | 9 import 'package:http/http.dart' as http; |
10 import 'package:path/path.dart' as p; | 10 import 'package:path/path.dart' as p; |
11 import 'package:pub_semver/pub_semver.dart'; | 11 import 'package:pub_semver/pub_semver.dart'; |
12 | 12 |
13 import '../exceptions.dart'; | 13 import '../exceptions.dart'; |
14 import '../http.dart'; | 14 import '../http.dart'; |
15 import '../io.dart'; | 15 import '../io.dart'; |
16 import '../log.dart' as log; | 16 import '../log.dart' as log; |
17 import '../package.dart'; | 17 import '../package.dart'; |
18 import '../pubspec.dart'; | 18 import '../pubspec.dart'; |
19 import '../source.dart'; | |
20 import '../system_cache.dart'; | |
19 import '../utils.dart'; | 21 import '../utils.dart'; |
20 import 'cached.dart'; | 22 import 'cached.dart'; |
21 | 23 |
22 /// A package source that gets packages from a package hosting site that uses | 24 /// A package source that gets packages from a package hosting site that uses |
23 /// the same API as pub.dartlang.org. | 25 /// the same API as pub.dartlang.org. |
24 class HostedSource extends CachedSource { | 26 class HostedSource extends Source { |
27 final name = "hosted"; | |
28 final hasMultipleVersions = true; | |
29 | |
30 LiveHostedSource bind(SystemCache systemCache, {bool isOffline: false}) => | |
31 isOffline | |
32 ? new _OfflineHostedSource(this, systemCache) | |
33 : new LiveHostedSource(this, systemCache); | |
34 | |
35 /// Gets the default URL for the package server for hosted dependencies. | |
36 String get defaultUrl { | |
37 var url = io.Platform.environment["PUB_HOSTED_URL"]; | |
38 if (url != null) return url; | |
39 | |
40 return "https://pub.dartlang.org"; | |
41 } | |
42 | |
25 /// Returns a reference to a hosted package named [name]. | 43 /// Returns a reference to a hosted package named [name]. |
26 /// | 44 /// |
27 /// If [url] is passed, it's the URL of the pub server from which the package | 45 /// If [url] is passed, it's the URL of the pub server from which the package |
28 /// should be downloaded. It can be a [Uri] or a [String]. | 46 /// should be downloaded. It can be a [Uri] or a [String]. |
29 static PackageRef refFor(String name, {url}) => | 47 PackageRef refFor(String name, {url}) => |
30 new PackageRef(name, 'hosted', _descriptionFor(name, url)); | 48 new PackageRef(name, 'hosted', _descriptionFor(name, url)); |
31 | 49 |
32 /// Returns an ID for a hosted package named [name] at [version]. | 50 /// Returns an ID for a hosted package named [name] at [version]. |
33 /// | 51 /// |
34 /// If [url] is passed, it's the URL of the pub server from which the package | 52 /// If [url] is passed, it's the URL of the pub server from which the package |
35 /// should be downloaded. It can be a [Uri] or a [String]. | 53 /// should be downloaded. It can be a [Uri] or a [String]. |
36 static PackageId idFor(String name, Version version, {url}) => | 54 PackageId idFor(String name, Version version, {url}) => |
37 new PackageId(name, 'hosted', version, _descriptionFor(name, url)); | 55 new PackageId(name, 'hosted', version, _descriptionFor(name, url)); |
38 | 56 |
39 /// Returns the description for a hosted package named [name] with the | 57 /// Returns the description for a hosted package named [name] with the |
40 /// given package server [url]. | 58 /// given package server [url]. |
41 static _descriptionFor(String name, [url]) { | 59 _descriptionFor(String name, [url]) { |
42 if (url == null) return name; | 60 if (url == null) return name; |
43 | 61 |
44 if (url is! String && url is! Uri) { | 62 if (url is! String && url is! Uri) { |
45 throw new ArgumentError.value(url, 'url', 'must be a Uri or a String.'); | 63 throw new ArgumentError.value(url, 'url', 'must be a Uri or a String.'); |
46 } | 64 } |
47 | 65 |
48 return {'name': name, 'url': url.toString()}; | 66 return {'name': name, 'url': url.toString()}; |
49 } | 67 } |
50 | 68 |
51 final name = "hosted"; | 69 bool descriptionsEqual(description1, description2) => |
52 final hasMultipleVersions = true; | 70 _parseDescription(description1) == _parseDescription(description2); |
53 | 71 |
54 /// Gets the default URL for the package server for hosted dependencies. | 72 /// Ensures that [description] is a valid hosted package description. |
55 static String get defaultUrl { | 73 /// |
56 var url = io.Platform.environment["PUB_HOSTED_URL"]; | 74 /// There are two valid formats. A plain string refers to a package with the |
57 if (url != null) return url; | 75 /// given name from the default host, while a map with keys "name" and "url" |
76 /// refers to a package with the given name from the host at the given URL. | |
77 PackageRef parseRef(String name, description, {String containingPath}) { | |
78 _parseDescription(description); | |
79 return new PackageRef(name, this.name, description); | |
80 } | |
58 | 81 |
59 return "https://pub.dartlang.org"; | 82 PackageId parseId(String name, Version version, description) { |
83 _parseDescription(description); | |
84 return new PackageId(name, this.name, version, description); | |
60 } | 85 } |
61 | 86 |
87 /// Parses the description for a package. | |
88 /// | |
89 /// If the package parses correctly, this returns a (name, url) pair. If not, | |
90 /// this throws a descriptive FormatException. | |
91 Pair<String, String> _parseDescription(description) { | |
92 if (description is String) { | |
93 return new Pair<String, String>(description, defaultUrl); | |
94 } | |
95 | |
96 if (description is! Map) { | |
97 throw new FormatException( | |
98 "The description must be a package name or map."); | |
99 } | |
100 | |
101 if (!description.containsKey("name")) { | |
102 throw new FormatException( | |
103 "The description map must contain a 'name' key."); | |
104 } | |
105 | |
106 var name = description["name"]; | |
107 if (name is! String) { | |
108 throw new FormatException("The 'name' key must have a string value."); | |
109 } | |
110 | |
111 return new Pair<String, String>(name, description["url"] ?? defaultUrl); | |
112 } | |
113 } | |
114 | |
115 /// The bound version of [HostedSource]. | |
Bob Nystrom
2016/06/14 23:21:55
"Bound" isn't used elsewhere to describe live sour
nweiz
2016/06/20 20:46:08
I don't like leaving classes undocumented :-/. Wha
| |
116 class LiveHostedSource extends CachedSource { | |
117 final HostedSource source; | |
118 | |
119 final SystemCache systemCache; | |
120 | |
121 LiveHostedSource(this.source, this.systemCache); | |
122 | |
62 /// Downloads a list of all versions of a package that are available from the | 123 /// Downloads a list of all versions of a package that are available from the |
63 /// site. | 124 /// site. |
64 Future<List<PackageId>> doGetVersions(PackageRef ref) async { | 125 Future<List<PackageId>> doGetVersions(PackageRef ref) async { |
65 var url = _makeUrl(ref.description, | 126 var url = _makeUrl(ref.description, |
66 (server, package) => "$server/api/packages/$package"); | 127 (server, package) => "$server/api/packages/$package"); |
67 | 128 |
68 log.io("Get versions from $url."); | 129 log.io("Get versions from $url."); |
69 | 130 |
70 var body; | 131 var body; |
71 try { | 132 try { |
72 body = await httpClient.read(url, headers: PUB_API_HEADERS); | 133 body = await httpClient.read(url, headers: PUB_API_HEADERS); |
73 } catch (error, stackTrace) { | 134 } catch (error, stackTrace) { |
74 var parsed = _parseDescription(ref.description); | 135 var parsed = source._parseDescription(ref.description); |
75 _throwFriendlyError(error, stackTrace, parsed.first, parsed.last); | 136 _throwFriendlyError(error, stackTrace, parsed.first, parsed.last); |
76 } | 137 } |
77 | 138 |
78 var doc = JSON.decode(body); | 139 var doc = JSON.decode(body); |
79 return doc['versions'].map((map) { | 140 return doc['versions'].map((map) { |
80 var pubspec = new Pubspec.fromMap( | 141 var pubspec = new Pubspec.fromMap( |
81 map['pubspec'], systemCache.sources, | 142 map['pubspec'], systemCache.sources, |
82 expectedName: ref.name, location: url); | 143 expectedName: ref.name, location: url); |
83 var id = idFor(ref.name, pubspec.version, | 144 var id = source.idFor(ref.name, pubspec.version, |
84 url: _serverFor(ref.description)); | 145 url: _serverFor(ref.description)); |
85 memoizePubspec(id, pubspec); | 146 memoizePubspec(id, pubspec); |
86 | 147 |
87 return id; | 148 return id; |
88 }).toList(); | 149 }).toList(); |
89 } | 150 } |
90 | 151 |
152 /// Parses [description] into its server and package name components, then | |
153 /// converts that to a Uri given [pattern]. | |
154 /// | |
155 /// Ensures the package name is properly URL encoded. | |
156 Uri _makeUrl(description, String pattern(String server, String package)) { | |
157 var parsed = source._parseDescription(description); | |
158 var server = parsed.last; | |
159 var package = Uri.encodeComponent(parsed.first); | |
160 return Uri.parse(pattern(server, package)); | |
161 } | |
162 | |
91 /// Downloads and parses the pubspec for a specific version of a package that | 163 /// Downloads and parses the pubspec for a specific version of a package that |
92 /// is available from the site. | 164 /// is available from the site. |
93 Future<Pubspec> describeUncached(PackageId id) async { | 165 Future<Pubspec> describeUncached(PackageId id) async { |
94 // Request it from the server. | 166 // Request it from the server. |
95 var url = _makeVersionUrl(id, (server, package, version) => | 167 var url = _makeVersionUrl(id, (server, package, version) => |
96 "$server/api/packages/$package/versions/$version"); | 168 "$server/api/packages/$package/versions/$version"); |
97 | 169 |
98 log.io("Describe package at $url."); | 170 log.io("Describe package at $url."); |
99 var version; | 171 var version; |
100 try { | 172 try { |
101 version = JSON.decode( | 173 version = JSON.decode( |
102 await httpClient.read(url, headers: PUB_API_HEADERS)); | 174 await httpClient.read(url, headers: PUB_API_HEADERS)); |
103 } catch (error, stackTrace) { | 175 } catch (error, stackTrace) { |
104 var parsed = _parseDescription(id.description); | 176 var parsed = source._parseDescription(id.description); |
105 _throwFriendlyError(error, stackTrace, id.name, parsed.last); | 177 _throwFriendlyError(error, stackTrace, id.name, parsed.last); |
106 } | 178 } |
107 | 179 |
108 return new Pubspec.fromMap( | 180 return new Pubspec.fromMap( |
109 version['pubspec'], systemCache.sources, | 181 version['pubspec'], systemCache.sources, |
110 expectedName: id.name, location: url); | 182 expectedName: id.name, location: url); |
111 } | 183 } |
112 | 184 |
113 /// Downloads the package identified by [id] to the system cache. | 185 /// Downloads the package identified by [id] to the system cache. |
114 Future<Package> downloadToSystemCache(PackageId id) async { | 186 Future<Package> downloadToSystemCache(PackageId id) async { |
115 if (!isInSystemCache(id)) { | 187 if (!isInSystemCache(id)) { |
116 var packageDir = getDirectory(id); | 188 var packageDir = getDirectory(id); |
117 ensureDir(p.dirname(packageDir)); | 189 ensureDir(p.dirname(packageDir)); |
118 var parsed = _parseDescription(id.description); | 190 var parsed = source._parseDescription(id.description); |
119 await _download(parsed.last, parsed.first, id.version, packageDir); | 191 await _download(parsed.last, parsed.first, id.version, packageDir); |
120 } | 192 } |
121 | 193 |
122 return new Package.load(id.name, getDirectory(id), systemCache.sources); | 194 return new Package.load(id.name, getDirectory(id), systemCache.sources); |
123 } | 195 } |
124 | 196 |
125 /// The system cache directory for the hosted source contains subdirectories | 197 /// The system cache directory for the hosted source contains subdirectories |
126 /// for each separate repository URL that's used on the system. | 198 /// for each separate repository URL that's used on the system. |
127 /// | 199 /// |
128 /// Each of these subdirectories then contains a subdirectory for each | 200 /// Each of these subdirectories then contains a subdirectory for each |
129 /// package downloaded from that site. | 201 /// package downloaded from that site. |
130 String getDirectory(PackageId id) { | 202 String getDirectory(PackageId id) { |
131 var parsed = _parseDescription(id.description); | 203 var parsed = source._parseDescription(id.description); |
132 var dir = _urlToDirectory(parsed.last); | 204 var dir = _urlToDirectory(parsed.last); |
133 return p.join(systemCacheRoot, dir, "${parsed.first}-${id.version}"); | 205 return p.join(systemCacheRoot, dir, "${parsed.first}-${id.version}"); |
134 } | 206 } |
135 | 207 |
136 String packageName(description) => _parseDescription(description).first; | |
137 | |
138 bool descriptionsEqual(description1, description2) => | |
139 _parseDescription(description1) == _parseDescription(description2); | |
140 | |
141 /// Ensures that [description] is a valid hosted package description. | |
142 /// | |
143 /// There are two valid formats. A plain string refers to a package with the | |
144 /// given name from the default host, while a map with keys "name" and "url" | |
145 /// refers to a package with the given name from the host at the given URL. | |
146 PackageRef parseRef(String name, description, {String containingPath}) { | |
147 _parseDescription(description); | |
148 return new PackageRef(name, this.name, description); | |
149 } | |
150 | |
151 PackageId parseId(String name, Version version, description) { | |
152 _parseDescription(description); | |
153 return new PackageId(name, this.name, version, description); | |
154 } | |
155 | |
156 /// Re-downloads all packages that have been previously downloaded into the | 208 /// Re-downloads all packages that have been previously downloaded into the |
157 /// system cache from any server. | 209 /// system cache from any server. |
158 Future<Pair<List<PackageId>, List<PackageId>>> repairCachedPackages() async { | 210 Future<Pair<List<PackageId>, List<PackageId>>> repairCachedPackages() async { |
159 if (!dirExists(systemCacheRoot)) return new Pair([], []); | 211 if (!dirExists(systemCacheRoot)) return new Pair([], []); |
160 | 212 |
161 var successes = []; | 213 var successes = []; |
162 var failures = []; | 214 var failures = []; |
163 | 215 |
164 for (var serverDir in listDir(systemCacheRoot)) { | 216 for (var serverDir in listDir(systemCacheRoot)) { |
165 var url = _directoryToUrl(p.basename(serverDir)); | 217 var url = _directoryToUrl(p.basename(serverDir)); |
166 var packages = _getCachedPackagesInDirectory(p.basename(serverDir)); | 218 var packages = _getCachedPackagesInDirectory(p.basename(serverDir)); |
167 packages.sort(Package.orderByNameAndVersion); | 219 packages.sort(Package.orderByNameAndVersion); |
168 | 220 |
169 for (var package in packages) { | 221 for (var package in packages) { |
170 var id = idFor(package.name, package.version, url: url); | 222 var id = source.idFor(package.name, package.version, url: url); |
171 | 223 |
172 try { | 224 try { |
173 await _download(url, package.name, package.version, package.dir); | 225 await _download(url, package.name, package.version, package.dir); |
174 successes.add(id); | 226 successes.add(id); |
175 } catch (error, stackTrace) { | 227 } catch (error, stackTrace) { |
176 failures.add(id); | 228 failures.add(id); |
177 var message = "Failed to repair ${log.bold(package.name)} " | 229 var message = "Failed to repair ${log.bold(package.name)} " |
178 "${package.version}"; | 230 "${package.version}"; |
179 if (url != defaultUrl) message += " from $url"; | 231 if (url != source.defaultUrl) message += " from $url"; |
180 log.error("$message. Error:\n$error"); | 232 log.error("$message. Error:\n$error"); |
181 log.fine(stackTrace); | 233 log.fine(stackTrace); |
182 | 234 |
183 tryDeleteEntry(package.dir); | 235 tryDeleteEntry(package.dir); |
184 } | 236 } |
185 } | 237 } |
186 } | 238 } |
187 | 239 |
188 return new Pair(successes, failures); | 240 return new Pair(successes, failures); |
189 } | 241 } |
190 | 242 |
191 /// Gets all of the packages that have been downloaded into the system cache | 243 /// Gets all of the packages that have been downloaded into the system cache |
192 /// from the default server. | 244 /// from the default server. |
193 List<Package> getCachedPackages() { | 245 List<Package> getCachedPackages() => |
194 return _getCachedPackagesInDirectory(_urlToDirectory(defaultUrl)); | 246 _getCachedPackagesInDirectory(_urlToDirectory(source.defaultUrl)); |
195 } | |
196 | 247 |
197 /// Gets all of the packages that have been downloaded into the system cache | 248 /// Gets all of the packages that have been downloaded into the system cache |
198 /// into [dir]. | 249 /// into [dir]. |
199 List<Package> _getCachedPackagesInDirectory(String dir) { | 250 List<Package> _getCachedPackagesInDirectory(String dir) { |
200 var cacheDir = p.join(systemCacheRoot, dir); | 251 var cacheDir = p.join(systemCacheRoot, dir); |
201 if (!dirExists(cacheDir)) return []; | 252 if (!dirExists(cacheDir)) return []; |
202 | 253 |
203 return listDir(cacheDir) | 254 return listDir(cacheDir) |
204 .map((entry) => new Package.load(null, entry, systemCache.sources)) | 255 .map((entry) => new Package.load(null, entry, systemCache.sources)) |
205 .toList(); | 256 .toList(); |
(...skipping 35 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
241 } | 292 } |
242 | 293 |
243 if (error is io.SocketException) { | 294 if (error is io.SocketException) { |
244 fail("Got socket error trying to find package $package at $url.", | 295 fail("Got socket error trying to find package $package at $url.", |
245 error, stackTrace); | 296 error, stackTrace); |
246 } | 297 } |
247 | 298 |
248 // Otherwise re-throw the original exception. | 299 // Otherwise re-throw the original exception. |
249 throw error; | 300 throw error; |
250 } | 301 } |
302 | |
303 /// Given a URL, returns a "normalized" string to be used as a directory name | |
304 /// for packages downloaded from the server at that URL. | |
305 /// | |
306 /// This normalization strips off the scheme (which is presumed to be HTTP or | |
307 /// HTTPS) and *sort of* URL-encodes it. I say "sort of" because it does it | |
308 /// incorrectly: it uses the character's *decimal* ASCII value instead of hex. | |
309 /// | |
310 /// This could cause an ambiguity since some characters get encoded as three | |
311 /// digits and others two. It's possible for one to be a prefix of the other. | |
312 /// In practice, the set of characters that are encoded don't happen to have | |
313 /// any collisions, so the encoding is reversible. | |
314 /// | |
315 /// This behavior is a bug, but is being preserved for compatibility. | |
316 String _urlToDirectory(String url) { | |
317 // Normalize all loopback URLs to "localhost". | |
318 url = url.replaceAllMapped( | |
319 new RegExp(r"^(https?://)(127\.0\.0\.1|\[::1\]|localhost)?"), | |
320 (match) { | |
321 // Don't include the scheme for HTTPS URLs. This makes the directory names | |
322 // nice for the default and most recommended scheme. We also don't include | |
323 // it for localhost URLs, since they're always known to be HTTP. | |
324 var localhost = match[2] == null ? '' : 'localhost'; | |
325 var scheme = match[1] == 'https://' || localhost.isNotEmpty ? '' : match[1 ]; | |
Bob Nystrom
2016/06/14 23:21:55
Long line.
nweiz
2016/06/20 20:46:08
Done.
| |
326 return "$scheme$localhost"; | |
327 }); | |
328 return replace(url, new RegExp(r'[<>:"\\/|?*%]'), | |
329 (match) => '%${match[0].codeUnitAt(0)}'); | |
330 } | |
331 | |
332 /// Given a directory name in the system cache, returns the URL of the server | |
333 /// whose packages it contains. | |
334 /// | |
335 /// See [_urlToDirectory] for details on the mapping. Note that because the | |
336 /// directory name does not preserve the scheme, this has to guess at it. It | |
337 /// chooses "http" for loopback URLs (mainly to support the pub tests) and | |
338 /// "https" for all others. | |
339 String _directoryToUrl(String url) { | |
340 // Decode the pseudo-URL-encoded characters. | |
341 var chars = '<>:"\\/|?*%'; | |
342 for (var i = 0; i < chars.length; i++) { | |
343 var c = chars.substring(i, i + 1); | |
344 url = url.replaceAll("%${c.codeUnitAt(0)}", c); | |
345 } | |
346 | |
347 // If the URL has an explicit scheme, use that. | |
348 if (url.contains("://")) return url; | |
349 | |
350 // Otherwise, default to http for localhost and https for everything else. | |
351 var scheme = | |
352 isLoopback(url.replaceAll(new RegExp(":.*"), "")) ? "http" : "https"; | |
353 return "$scheme://$url"; | |
354 } | |
355 | |
356 /// Returns the server URL for [description]. | |
357 Uri _serverFor(description) => | |
358 Uri.parse(source._parseDescription(description).last); | |
359 | |
360 /// Parses [id] into its server, package name, and version components, then | |
361 /// converts that to a Uri given [pattern]. | |
362 /// | |
363 /// Ensures the package name is properly URL encoded. | |
364 Uri _makeVersionUrl(PackageId id, | |
365 String pattern(String server, String package, String version)) { | |
366 var parsed = source._parseDescription(id.description); | |
367 var server = parsed.last; | |
368 var package = Uri.encodeComponent(parsed.first); | |
369 var version = Uri.encodeComponent(id.version.toString()); | |
370 return Uri.parse(pattern(server, package, version)); | |
371 } | |
251 } | 372 } |
252 | 373 |
253 /// This is the modified hosted source used when pub get or upgrade are run | 374 /// This is the modified hosted source used when pub get or upgrade are run |
254 /// with "--offline". | 375 /// with "--offline". |
255 /// | 376 /// |
256 /// This uses the system cache to get the list of available packages and does | 377 /// This uses the system cache to get the list of available packages and does |
257 /// no network access. | 378 /// no network access. |
258 class OfflineHostedSource extends HostedSource { | 379 class _OfflineHostedSource extends LiveHostedSource { |
380 _OfflineHostedSource(HostedSource source, SystemCache systemCache) | |
381 : super(source, systemCache); | |
382 | |
259 /// Gets the list of all versions of [ref] that are in the system cache. | 383 /// Gets the list of all versions of [ref] that are in the system cache. |
260 Future<List<PackageId>> doGetVersions(PackageRef ref) async { | 384 Future<List<PackageId>> doGetVersions(PackageRef ref) async { |
261 var parsed = _parseDescription(ref.description); | 385 var parsed = source._parseDescription(ref.description); |
262 var server = parsed.last; | 386 var server = parsed.last; |
263 log.io("Finding versions of ${ref.name} in " | 387 log.io("Finding versions of ${ref.name} in " |
264 "$systemCacheRoot/${_urlToDirectory(server)}"); | 388 "$systemCacheRoot/${_urlToDirectory(server)}"); |
265 | 389 |
266 var dir = p.join(systemCacheRoot, _urlToDirectory(server)); | 390 var dir = p.join(systemCacheRoot, _urlToDirectory(server)); |
267 | 391 |
268 var versions; | 392 var versions; |
269 if (dirExists(dir)) { | 393 if (dirExists(dir)) { |
270 versions = await listDir(dir).map((entry) { | 394 versions = await listDir(dir).map((entry) { |
271 var components = p.basename(entry).split("-"); | 395 var components = p.basename(entry).split("-"); |
272 if (components.first != ref.name) return null; | 396 if (components.first != ref.name) return null; |
273 return HostedSource.idFor( | 397 return source.idFor( |
274 ref.name, new Version.parse(components.skip(1).join("-")), | 398 ref.name, new Version.parse(components.skip(1).join("-")), |
275 url: _serverFor(ref.description)); | 399 url: _serverFor(ref.description)); |
276 }).where((id) => id != null).toList(); | 400 }).where((id) => id != null).toList(); |
277 } else { | 401 } else { |
278 versions = []; | 402 versions = []; |
279 } | 403 } |
280 | 404 |
281 // If there are no versions in the cache, report a clearer error. | 405 // If there are no versions in the cache, report a clearer error. |
282 if (versions.isEmpty) { | 406 if (versions.isEmpty) { |
283 throw new PackageNotFoundException( | 407 throw new PackageNotFoundException( |
284 "Could not find package ${ref.name} in cache."); | 408 "Could not find package ${ref.name} in cache."); |
285 } | 409 } |
286 | 410 |
287 return versions; | 411 return versions; |
288 } | 412 } |
289 | 413 |
290 Future _download(String server, String package, Version version, | 414 Future _download(String server, String package, Version version, |
291 String destPath) { | 415 String destPath) { |
292 // Since HostedSource is cached, this will only be called for uncached | 416 // Since HostedSource is cached, this will only be called for uncached |
293 // packages. | 417 // packages. |
294 throw new UnsupportedError("Cannot download packages when offline."); | 418 throw new UnsupportedError("Cannot download packages when offline."); |
295 } | 419 } |
296 | 420 |
297 Future<Pubspec> describeUncached(PackageId id) { | 421 Future<Pubspec> describeUncached(PackageId id) { |
298 throw new PackageNotFoundException( | 422 throw new PackageNotFoundException( |
299 "${id.name} ${id.version} is not available in your system cache."); | 423 "${id.name} ${id.version} is not available in your system cache."); |
300 } | 424 } |
301 } | 425 } |
302 | |
303 /// Given a URL, returns a "normalized" string to be used as a directory name | |
304 /// for packages downloaded from the server at that URL. | |
305 /// | |
306 /// This normalization strips off the scheme (which is presumed to be HTTP or | |
307 /// HTTPS) and *sort of* URL-encodes it. I say "sort of" because it does it | |
308 /// incorrectly: it uses the character's *decimal* ASCII value instead of hex. | |
309 /// | |
310 /// This could cause an ambiguity since some characters get encoded as three | |
311 /// digits and others two. It's possible for one to be a prefix of the other. | |
312 /// In practice, the set of characters that are encoded don't happen to have | |
313 /// any collisions, so the encoding is reversible. | |
314 /// | |
315 /// This behavior is a bug, but is being preserved for compatibility. | |
316 String _urlToDirectory(String url) { | |
317 // Normalize all loopback URLs to "localhost". | |
318 url = url.replaceAllMapped( | |
319 new RegExp(r"^(https?://)(127\.0\.0\.1|\[::1\]|localhost)?"), | |
320 (match) { | |
321 // Don't include the scheme for HTTPS URLs. This makes the directory names | |
322 // nice for the default and most recommended scheme. We also don't include | |
323 // it for localhost URLs, since they're always known to be HTTP. | |
324 var localhost = match[2] == null ? '' : 'localhost'; | |
325 var scheme = match[1] == 'https://' || localhost.isNotEmpty ? '' : match[1]; | |
326 return "$scheme$localhost"; | |
327 }); | |
328 return replace(url, new RegExp(r'[<>:"\\/|?*%]'), | |
329 (match) => '%${match[0].codeUnitAt(0)}'); | |
330 } | |
331 | |
332 /// Given a directory name in the system cache, returns the URL of the server | |
333 /// whose packages it contains. | |
334 /// | |
335 /// See [_urlToDirectory] for details on the mapping. Note that because the | |
336 /// directory name does not preserve the scheme, this has to guess at it. It | |
337 /// chooses "http" for loopback URLs (mainly to support the pub tests) and | |
338 /// "https" for all others. | |
339 String _directoryToUrl(String url) { | |
340 // Decode the pseudo-URL-encoded characters. | |
341 var chars = '<>:"\\/|?*%'; | |
342 for (var i = 0; i < chars.length; i++) { | |
343 var c = chars.substring(i, i + 1); | |
344 url = url.replaceAll("%${c.codeUnitAt(0)}", c); | |
345 } | |
346 | |
347 // If the URL has an explicit scheme, use that. | |
348 if (url.contains("://")) return url; | |
349 | |
350 // Otherwise, default to http for localhost and https for everything else. | |
351 var scheme = | |
352 isLoopback(url.replaceAll(new RegExp(":.*"), "")) ? "http" : "https"; | |
353 return "$scheme://$url"; | |
354 } | |
355 | |
356 /// Parses [description] into its server and package name components, then | |
357 /// converts that to a Uri given [pattern]. | |
358 /// | |
359 /// Ensures the package name is properly URL encoded. | |
360 Uri _makeUrl(description, String pattern(String server, String package)) { | |
361 var parsed = _parseDescription(description); | |
362 var server = parsed.last; | |
363 var package = Uri.encodeComponent(parsed.first); | |
364 return Uri.parse(pattern(server, package)); | |
365 } | |
366 | |
367 /// Returns the server URL for [description]. | |
368 Uri _serverFor(description) => Uri.parse(_parseDescription(description).last); | |
369 | |
370 /// Parses [id] into its server, package name, and version components, then | |
371 /// converts that to a Uri given [pattern]. | |
372 /// | |
373 /// Ensures the package name is properly URL encoded. | |
374 Uri _makeVersionUrl(PackageId id, | |
375 String pattern(String server, String package, String version)) { | |
376 var parsed = _parseDescription(id.description); | |
377 var server = parsed.last; | |
378 var package = Uri.encodeComponent(parsed.first); | |
379 var version = Uri.encodeComponent(id.version.toString()); | |
380 return Uri.parse(pattern(server, package, version)); | |
381 } | |
382 | |
383 /// Parses the description for a package. | |
384 /// | |
385 /// If the package parses correctly, this returns a (name, url) pair. If not, | |
386 /// this throws a descriptive FormatException. | |
387 Pair<String, String> _parseDescription(description) { | |
388 if (description is String) { | |
389 return new Pair<String, String>(description, HostedSource.defaultUrl); | |
390 } | |
391 | |
392 if (description is! Map) { | |
393 throw new FormatException( | |
394 "The description must be a package name or map."); | |
395 } | |
396 | |
397 if (!description.containsKey("name")) { | |
398 throw new FormatException( | |
399 "The description map must contain a 'name' key."); | |
400 } | |
401 | |
402 var name = description["name"]; | |
403 if (name is! String) { | |
404 throw new FormatException("The 'name' key must have a string value."); | |
405 } | |
406 | |
407 var url = description["url"]; | |
408 if (url == null) url = HostedSource.defaultUrl; | |
409 | |
410 return new Pair<String, String>(name, url); | |
411 } | |
OLD | NEW |