2 Downloads feeds, keys, packages and icons.
5 # Copyright (C) 2009, Thomas Leonard
6 # See the README file for details, or visit http://0install.net.
9 from logging
import info
, debug
, warn
11 from zeroinstall
.support
import tasks
, basedir
12 from zeroinstall
.injector
.namespaces
import XMLNS_IFACE
, config_site
13 from zeroinstall
.injector
.model
import DownloadSource
, Recipe
, SafeException
, escape
14 from zeroinstall
.injector
.iface_cache
import PendingFeed
, ReplayAttack
15 from zeroinstall
.injector
.handler
import NoTrustedKeys
16 from zeroinstall
.injector
import download
18 def _escape_slashes(path
):
19 return path
.replace('/', '%23')
21 def _get_feed_dir(feed
):
22 """The algorithm from 0mirror."""
24 raise SafeException("Invalid URL '%s'" % feed
)
25 scheme
, rest
= feed
.split('://', 1)
26 domain
, rest
= rest
.split('/', 1)
27 for x
in [scheme
, domain
, rest
]:
28 if not x
or x
.startswith(','):
29 raise SafeException("Invalid URL '%s'" % feed
)
30 return os
.path
.join('feeds', scheme
, domain
, _escape_slashes(rest
))
32 class Fetcher(object):
33 """Downloads and stores various things.
34 @ivar handler: handler to use for user-interaction
35 @type handler: L{handler.Handler}
36 @ivar feed_mirror: the base URL of a mirror site for keys and feeds
37 @type feed_mirror: str
39 __slots__
= ['handler', 'feed_mirror']
41 def __init__(self
, handler
):
42 self
.handler
= handler
43 self
.feed_mirror
= "http://roscidus.com/0mirror"
46 def cook(self
, required_digest
, recipe
, stores
, force
= False, impl_hint
= None):
48 @param impl_hint: the Implementation this is for (if any) as a hint for the GUI
49 @see: L{download_impl} uses this method when appropriate"""
50 # Maybe we're taking this metaphor too far?
52 # Start downloading all the ingredients.
53 downloads
= {} # Downloads that are not yet successful
54 streams
= {} # Streams collected from successful downloads
56 # Start a download for each ingredient
58 for step
in recipe
.steps
:
59 blocker
, stream
= self
.download_archive(step
, force
= force
, impl_hint
= impl_hint
)
61 blockers
.append(blocker
)
62 streams
[step
] = stream
67 blockers
= [b
for b
in blockers
if not b
.happened
]
69 from zeroinstall
.zerostore
import unpack
71 # Create an empty directory for the new implementation
72 store
= stores
.stores
[0]
73 tmpdir
= store
.get_tmp_dir_for(required_digest
)
75 # Unpack each of the downloaded archives into it in turn
76 for step
in recipe
.steps
:
77 stream
= streams
[step
]
79 unpack
.unpack_archive_over(step
.url
, stream
, tmpdir
, step
.extract
)
80 # Check that the result is correct and store it in the cache
81 store
.check_manifest_and_rename(required_digest
, tmpdir
)
84 # If unpacking fails, remove the temporary directory
85 if tmpdir
is not None:
86 from zeroinstall
import support
87 support
.ro_rmtree(tmpdir
)
89 def get_feed_mirror(self
, url
):
90 """Return the URL of a mirror for this feed."""
92 if urlparse
.urlparse(url
).hostname
== 'localhost':
94 return '%s/%s/latest.xml' % (self
.feed_mirror
, _get_feed_dir(url
))
96 def download_and_import_feed(self
, feed_url
, iface_cache
, force
= False):
97 """Download the feed, download any required keys, confirm trust if needed and import.
98 @param feed_url: the feed to be downloaded
100 @param iface_cache: cache in which to store the feed
101 @type iface_cache: L{iface_cache.IfaceCache}
102 @param force: whether to abort and restart an existing download"""
103 from download
import DownloadAborted
105 debug("download_and_import_feed %s (force = %d)", feed_url
, force
)
106 assert not feed_url
.startswith('/')
108 primary
= self
._download
_and
_import
_feed
(feed_url
, iface_cache
, force
, use_mirror
= False)
110 @tasks.named_async("monitor feed downloads for " + feed_url
)
111 def wait_for_downloads(primary
):
112 # Download just the upstream feed, unless it takes too long...
113 timeout
= tasks
.TimeoutBlocker(5, 'Mirror timeout') # 5 seconds
115 yield primary
, timeout
121 return # OK, primary succeeded!
122 # OK, maybe it's just being slow...
123 info("Feed download from %s is taking a long time. Trying mirror too...", feed_url
)
125 except NoTrustedKeys
, ex
:
126 raise # Don't bother trying the mirror if we have a trust problem
127 except ReplayAttack
, ex
:
128 raise # Don't bother trying the mirror if we have a replay attack
129 except DownloadAborted
, ex
:
130 raise # Don't bother trying the mirror if the user cancelled
131 except SafeException
, ex
:
135 warn("Trying mirror, as feed download from %s failed: %s", feed_url
, ex
)
137 # Start downloading from mirror...
138 mirror
= self
._download
_and
_import
_feed
(feed_url
, iface_cache
, force
, use_mirror
= True)
140 # Wait until both mirror and primary tasks are complete...
142 blockers
= filter(None, [primary
, mirror
])
152 # No point carrying on with the mirror once the primary has succeeded
154 info("Primary feed download succeeded; aborting mirror download for " + feed_url
)
156 except SafeException
, ex
:
159 info("Feed download from %s failed; still trying mirror: %s", feed_url
, ex
)
167 # We already warned; no need to raise an exception too,
168 # as the mirror download succeeded.
170 except ReplayAttack
, ex
:
171 info("Version from mirror is older than cached version; ignoring it: %s", ex
)
174 except SafeException
, ex
:
175 info("Mirror download failed: %s", ex
)
181 return wait_for_downloads(primary
)
183 def _download_and_import_feed(self
, feed_url
, iface_cache
, force
, use_mirror
):
184 """Download and import a feed.
185 @param use_mirror: False to use primary location; True to use mirror."""
187 url
= self
.get_feed_mirror(feed_url
)
188 if url
is None: return None
192 dl
= self
.handler
.get_download(url
, force
= force
, hint
= feed_url
)
195 @tasks.named_async("fetch_feed " + url
)
198 tasks
.check(dl
.downloaded
)
200 pending
= PendingFeed(feed_url
, stream
)
203 # If we got the feed from a mirror, get the key from there too
204 key_mirror
= self
.feed_mirror
+ '/keys/'
208 keys_downloaded
= tasks
.Task(pending
.download_keys(self
.handler
, feed_hint
= feed_url
, key_mirror
= key_mirror
), "download keys for " + feed_url
)
209 yield keys_downloaded
.finished
210 tasks
.check(keys_downloaded
.finished
)
212 iface
= iface_cache
.get_interface(pending
.url
)
213 if not iface_cache
.update_interface_if_trusted(iface
, pending
.sigs
, pending
.new_xml
):
214 blocker
= self
.handler
.confirm_trust_keys(iface
, pending
.sigs
, pending
.new_xml
)
218 if not iface_cache
.update_interface_if_trusted(iface
, pending
.sigs
, pending
.new_xml
):
219 raise NoTrustedKeys("No signing keys trusted; not importing")
225 def download_impl(self
, impl
, retrieval_method
, stores
, force
= False):
226 """Download an implementation.
227 @param impl: the selected implementation
228 @type impl: L{model.ZeroInstallImplementation}
229 @param retrieval_method: a way of getting the implementation (e.g. an Archive or a Recipe)
230 @type retrieval_method: L{model.RetrievalMethod}
231 @param stores: where to store the downloaded implementation
232 @type stores: L{zerostore.Stores}
233 @param force: whether to abort and restart an existing download
234 @rtype: L{tasks.Blocker}"""
236 assert retrieval_method
238 from zeroinstall
.zerostore
import manifest
239 alg
= impl
.id.split('=', 1)[0]
240 if alg
not in manifest
.algorithms
:
241 raise SafeException("Unknown digest algorithm '%s' for '%s' version %s" %
242 (alg
, impl
.feed
.get_name(), impl
.get_version()))
246 if isinstance(retrieval_method
, DownloadSource
):
247 blocker
, stream
= self
.download_archive(retrieval_method
, force
= force
, impl_hint
= impl
)
252 self
._add
_to
_cache
(stores
, retrieval_method
, stream
)
253 elif isinstance(retrieval_method
, Recipe
):
254 blocker
= self
.cook(impl
.id, retrieval_method
, stores
, force
, impl_hint
= impl
)
258 raise Exception("Unknown download type for '%s'" % retrieval_method
)
260 self
.handler
.impl_added_to_store(impl
)
261 return download_impl()
263 def _add_to_cache(self
, stores
, retrieval_method
, stream
):
264 assert isinstance(retrieval_method
, DownloadSource
)
265 required_digest
= retrieval_method
.implementation
.id
266 url
= retrieval_method
.url
267 stores
.add_archive_to_cache(required_digest
, stream
, retrieval_method
.url
, retrieval_method
.extract
,
268 type = retrieval_method
.type, start_offset
= retrieval_method
.start_offset
or 0)
270 def download_archive(self
, download_source
, force
= False, impl_hint
= None):
271 """Fetch an archive. You should normally call L{download_impl}
272 instead, since it handles other kinds of retrieval method too."""
273 from zeroinstall
.zerostore
import unpack
275 url
= download_source
.url
276 if not (url
.startswith('http:') or url
.startswith('https:') or url
.startswith('ftp:')):
277 raise SafeException("Unknown scheme in download URL '%s'" % url
)
279 mime_type
= download_source
.type
281 mime_type
= unpack
.type_from_url(download_source
.url
)
283 raise SafeException("No 'type' attribute on archive, and I can't guess from the name (%s)" % download_source
.url
)
284 unpack
.check_type_ok(mime_type
)
285 dl
= self
.handler
.get_download(download_source
.url
, force
= force
, hint
= impl_hint
)
286 dl
.expected_size
= download_source
.size
+ (download_source
.start_offset
or 0)
287 return (dl
.downloaded
, dl
.tempfile
)
289 def download_icon(self
, interface
, force
= False, modification_time
= None):
290 """Download an icon for this interface and add it to the
291 icon cache. If the interface has no icon or we are offline, do nothing.
292 @return: the task doing the import, or None
293 @rtype: L{tasks.Task}"""
294 debug("download_icon %s (force = %d)", interface
, force
)
296 # Find a suitable icon to download
297 for icon
in interface
.get_metadata(XMLNS_IFACE
, 'icon'):
298 type = icon
.getAttribute('type')
299 if type != 'image/png':
300 debug('Skipping non-PNG icon')
302 source
= icon
.getAttribute('href')
305 warn('Missing "href" attribute on <icon> in %s', interface
)
307 info('No PNG icons found in %s', interface
)
311 dl
= self
.handler
.monitored_downloads
[source
]
316 dl
= download
.Download(source
, hint
= interface
, modification_time
= modification_time
)
317 self
.handler
.monitor_download(dl
)
320 def download_and_add_icon():
324 tasks
.check(dl
.downloaded
)
325 if dl
.unmodified
: return
329 icons_cache
= basedir
.save_cache_path(config_site
, 'interface_icons')
330 icon_file
= file(os
.path
.join(icons_cache
, escape(interface
.uri
)), 'w')
331 shutil
.copyfileobj(stream
, icon_file
)
332 except Exception, ex
:
333 self
.handler
.report_error(ex
)
335 return download_and_add_icon()
337 def download_impls(self
, implementations
, stores
):
338 """Download the given implementations, choosing a suitable retrieval method for each."""
342 for impl
in implementations
:
343 debug("start_downloading_impls: for %s get %s", impl
.feed
, impl
)
344 source
= self
.get_best_source(impl
)
346 raise SafeException("Implementation " + impl
.id + " of "
347 "interface " + impl
.feed
.get_name() + " cannot be "
348 "downloaded (no download locations given in "
350 to_download
.append((impl
, source
))
352 for impl
, source
in to_download
:
353 blockers
.append(self
.download_impl(impl
, source
, stores
))
359 def download_impls(blockers
):
360 # Record the first error log the rest
362 def dl_error(ex
, tb
= None):
364 self
.handler
.report_error(ex
)
369 tasks
.check(blockers
, dl_error
)
371 blockers
= [b
for b
in blockers
if not b
.happened
]
375 return download_impls(blockers
)
377 def get_best_source(self
, impl
):
378 """Return the best download source for this implementation.
379 @rtype: L{model.RetrievalMethod}"""
380 if impl
.download_sources
:
381 return impl
.download_sources
[0]