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.
8 from zeroinstall
import _
10 from logging
import info
, debug
, warn
12 from zeroinstall
.support
import tasks
, basedir
13 from zeroinstall
.injector
.namespaces
import XMLNS_IFACE
, config_site
14 from zeroinstall
.injector
.model
import SafeException
, escape
, DistributionSource
15 from zeroinstall
.injector
.iface_cache
import PendingFeed
, ReplayAttack
16 from zeroinstall
.injector
.handler
import NoTrustedKeys
17 from zeroinstall
.injector
import download
19 def _escape_slashes(path
):
20 return path
.replace('/', '%23')
22 def _get_feed_dir(feed
):
23 """The algorithm from 0mirror."""
25 raise SafeException(_("Invalid URL '%s'") % feed
)
26 scheme
, rest
= feed
.split('://', 1)
27 assert '/' in rest
, "Missing / in %s" % feed
28 domain
, rest
= rest
.split('/', 1)
29 for x
in [scheme
, domain
, rest
]:
30 if not x
or x
.startswith(','):
31 raise SafeException(_("Invalid URL '%s'") % feed
)
32 return os
.path
.join('feeds', scheme
, domain
, _escape_slashes(rest
))
35 """Fetches information about a GPG key from a key-info server.
36 See L{Fetcher.fetch_key_info} for details.
41 >>> kf = KeyInfoFetcher(handler, 'https://server', fingerprint)
44 if kf.blocker is None: break
48 def __init__(self
, handler
, server
, fingerprint
):
49 self
.fingerprint
= fingerprint
53 if server
is None: return
55 self
.status
= _('Fetching key information from %s...') % server
57 dl
= handler
.get_download(server
+ '/key/' + fingerprint
)
59 from xml
.dom
import minidom
64 tempfile
= dl
.tempfile
67 tasks
.check(dl
.downloaded
)
69 doc
= minidom
.parse(tempfile
)
70 if doc
.documentElement
.localName
!= 'key-lookup':
71 raise SafeException(_('Expected <key-lookup>, not <%s>') % doc
.documentElement
.localName
)
72 self
.info
+= doc
.documentElement
.childNodes
73 except Exception as ex
:
74 doc
= minidom
.parseString('<item vote="bad"/>')
75 root
= doc
.documentElement
76 root
.appendChild(doc
.createTextNode(_('Error getting key information: %s') % ex
))
77 self
.info
.append(root
)
79 self
.blocker
= fetch_key_info()
81 class Fetcher(object):
82 """Downloads and stores various things.
83 @ivar config: used to get handler, iface_cache and stores
84 @type config: L{config.Config}
85 @ivar key_info: caches information about GPG keys
86 @type key_info: {str: L{KeyInfoFetcher}}
88 __slots__
= ['config', 'key_info']
90 def __init__(self
, config
):
91 assert config
.handler
, "API change!"
97 return self
.config
.handler
99 def cook(self
, required_digest
, recipe
, stores
, force
= False, impl_hint
= None):
101 @deprecated: use impl.retrieve() instead
102 @param impl_hint: the Implementation this is for (if any) as a hint for the GUI
103 @see: L{download_impl} uses this method when appropriate"""
104 # Maybe we're taking this metaphor too far?
106 return impl_hint
.retrieve(self
, recipe
, stores
, force
)
109 def get_feed_mirror(self
, url
):
110 """Return the URL of a mirror for this feed."""
111 if self
.config
.feed_mirror
is None:
114 if urlparse
.urlparse(url
).hostname
== 'localhost':
116 return '%s/%s/latest.xml' % (self
.config
.feed_mirror
, _get_feed_dir(url
))
119 def get_packagekit_feed(self
, feed_url
):
120 """Send a query to PackageKit (if available) for information about this package.
121 On success, the result is added to iface_cache.
123 assert feed_url
.startswith('distribution:'), feed_url
124 master_feed
= self
.config
.iface_cache
.get_feed(feed_url
.split(':', 1)[1])
126 fetch
= self
.config
.iface_cache
.distro
.fetch_candidates(master_feed
)
131 # Force feed to be regenerated with the new information
132 self
.config
.iface_cache
.get_feed(feed_url
, force
= True)
134 def download_and_import_feed(self
, feed_url
, iface_cache
= None, force
= False):
135 """Download the feed, download any required keys, confirm trust if needed and import.
136 @param feed_url: the feed to be downloaded
138 @param iface_cache: (deprecated)
139 @param force: whether to abort and restart an existing download"""
140 from .download
import DownloadAborted
142 assert iface_cache
is None or iface_cache
is self
.config
.iface_cache
144 debug(_("download_and_import_feed %(url)s (force = %(force)d)"), {'url': feed_url
, 'force': force
})
145 assert not os
.path
.isabs(feed_url
)
147 if feed_url
.startswith('distribution:'):
148 return self
.get_packagekit_feed(feed_url
)
150 primary
= self
._download
_and
_import
_feed
(feed_url
, force
, use_mirror
= False)
152 @tasks.named_async("monitor feed downloads for " + feed_url
)
153 def wait_for_downloads(primary
):
154 # Download just the upstream feed, unless it takes too long...
155 timeout
= tasks
.TimeoutBlocker(5, 'Mirror timeout') # 5 seconds
157 yield primary
, timeout
163 return # OK, primary succeeded!
164 # OK, maybe it's just being slow...
165 info("Feed download from %s is taking a long time.", feed_url
)
167 except NoTrustedKeys
as ex
:
168 raise # Don't bother trying the mirror if we have a trust problem
169 except ReplayAttack
as ex
:
170 raise # Don't bother trying the mirror if we have a replay attack
171 except DownloadAborted
as ex
:
172 raise # Don't bother trying the mirror if the user cancelled
173 except SafeException
as ex
:
177 warn(_("Feed download from %(url)s failed: %(exception)s"), {'url': feed_url
, 'exception': ex
})
179 # Start downloading from mirror...
180 mirror
= self
._download
_and
_import
_feed
(feed_url
, force
, use_mirror
= True)
182 # Wait until both mirror and primary tasks are complete...
184 blockers
= filter(None, [primary
, mirror
])
194 # No point carrying on with the mirror once the primary has succeeded
196 info(_("Primary feed download succeeded; aborting mirror download for %s") % feed_url
)
198 except SafeException
as ex
:
201 info(_("Feed download from %(url)s failed; still trying mirror: %(exception)s"), {'url': feed_url
, 'exception': ex
})
209 # We already warned; no need to raise an exception too,
210 # as the mirror download succeeded.
212 except ReplayAttack
as ex
:
213 info(_("Version from mirror is older than cached version; ignoring it: %s"), ex
)
216 except SafeException
as ex
:
217 info(_("Mirror download failed: %s"), ex
)
223 return wait_for_downloads(primary
)
225 def _download_and_import_feed(self
, feed_url
, force
, use_mirror
):
226 """Download and import a feed.
227 @param use_mirror: False to use primary location; True to use mirror."""
229 url
= self
.get_feed_mirror(feed_url
)
230 if url
is None: return None
231 info(_("Trying mirror server for feed %s") % feed_url
)
235 dl
= self
.handler
.get_download(url
, force
= force
, hint
= feed_url
)
238 @tasks.named_async("fetch_feed " + url
)
241 tasks
.check(dl
.downloaded
)
243 pending
= PendingFeed(feed_url
, stream
)
246 # If we got the feed from a mirror, get the key from there too
247 key_mirror
= self
.config
.feed_mirror
+ '/keys/'
251 keys_downloaded
= tasks
.Task(pending
.download_keys(self
.handler
, feed_hint
= feed_url
, key_mirror
= key_mirror
), _("download keys for %s") % feed_url
)
252 yield keys_downloaded
.finished
253 tasks
.check(keys_downloaded
.finished
)
255 if not self
.config
.iface_cache
.update_feed_if_trusted(pending
.url
, pending
.sigs
, pending
.new_xml
):
256 blocker
= self
.config
.trust_mgr
.confirm_keys(pending
)
260 if not self
.config
.iface_cache
.update_feed_if_trusted(pending
.url
, pending
.sigs
, pending
.new_xml
):
261 raise NoTrustedKeys(_("No signing keys trusted; not importing"))
267 def fetch_key_info(self
, fingerprint
):
269 return self
.key_info
[fingerprint
]
271 self
.key_info
[fingerprint
] = key_info
= KeyInfoFetcher(self
.handler
,
272 self
.config
.key_info_server
, fingerprint
)
275 def download_impl(self
, impl
, retrieval_method
, stores
, force
= False):
276 """Download an implementation.
277 @deprecated: use impl.retrieve(...) instead
278 @param impl: the selected implementation
279 @type impl: L{model.ZeroInstallImplementation}
280 @param retrieval_method: a way of getting the implementation (e.g. an Archive or a Recipe)
281 @type retrieval_method: L{model.RetrievalMethod}
282 @param stores: where to store the downloaded implementation
283 @type stores: L{zerostore.Stores}
284 @param force: whether to abort and restart an existing download
285 @rtype: L{tasks.Blocker}"""
287 assert retrieval_method
288 return impl
.retrieve(self
, retrieval_method
, stores
, force
)
290 def download_archive(self
, download_source
, force
= False, impl_hint
= None):
291 """Fetch an archive. You should normally call L{download_impl}
292 instead, since it handles other kinds of retrieval method too.
293 @deprecated: use download_source.download instead"""
295 return download_source
.download(self
, force
, impl_hint
)
297 def download_icon(self
, interface
, force
= False):
298 """Download an icon for this interface and add it to the
299 icon cache. If the interface has no icon do nothing.
300 @return: the task doing the import, or None
301 @rtype: L{tasks.Task}"""
302 debug("download_icon %(interface)s (force = %(force)d)", {'interface': interface
, 'force': force
})
304 modification_time
= None
305 existing_icon
= self
.config
.iface_cache
.get_icon_path(interface
)
307 file_mtime
= os
.stat(existing_icon
).st_mtime
308 from email
.utils
import formatdate
309 modification_time
= formatdate(timeval
= file_mtime
, localtime
= False, usegmt
= True)
311 # Find a suitable icon to download
312 for icon
in interface
.get_metadata(XMLNS_IFACE
, 'icon'):
313 type = icon
.getAttribute('type')
314 if type != 'image/png':
315 debug(_('Skipping non-PNG icon'))
317 source
= icon
.getAttribute('href')
320 warn(_('Missing "href" attribute on <icon> in %s'), interface
)
322 info(_('No PNG icons found in %s'), interface
)
326 dl
= self
.handler
.monitored_downloads
[source
]
331 dl
= download
.Download(source
, hint
= interface
, modification_time
= modification_time
)
332 self
.handler
.monitor_download(dl
)
335 def download_and_add_icon():
339 tasks
.check(dl
.downloaded
)
340 if dl
.unmodified
: return
344 icons_cache
= basedir
.save_cache_path(config_site
, 'interface_icons')
345 icon_file
= file(os
.path
.join(icons_cache
, escape(interface
.uri
)), 'w')
346 shutil
.copyfileobj(stream
, icon_file
)
347 except Exception as ex
:
348 self
.handler
.report_error(ex
)
350 return download_and_add_icon()
352 def download_impls(self
, implementations
, stores
):
353 """Download the given implementations, choosing a suitable retrieval method for each.
354 If any of the retrieval methods are DistributionSources and
355 need confirmation, handler.confirm is called to check that the
356 installation should proceed.
361 for impl
in implementations
:
362 debug(_("start_downloading_impls: for %(feed)s get %(implementation)s"), {'feed': impl
.feed
, 'implementation': impl
})
363 source
= self
.get_best_source(impl
)
365 raise SafeException(_("Implementation %(implementation_id)s of interface %(interface)s"
366 " cannot be downloaded (no download locations given in "
367 "interface!)") % {'implementation_id': impl
.id, 'interface': impl
.feed
.get_name()})
368 to_download
.append((impl
, source
))
370 if isinstance(source
, DistributionSource
) and source
.needs_confirmation
:
371 unsafe_impls
.append(source
.package_id
)
374 def download_impls():
376 confirm
= self
.handler
.confirm_install(_('The following components need to be installed using native packages. '
377 'These come from your distribution, and should therefore be trustworthy, but they also '
378 'run with extra privileges. In particular, installing them may run extra services on your '
379 'computer or affect other users. You may be asked to enter a password to confirm. The '
380 'packages are:\n\n') + ('\n'.join('- ' + x
for x
in unsafe_impls
)))
386 for impl
, source
in to_download
:
387 blockers
.append(self
.download_impl(impl
, source
, stores
))
389 # Record the first error log the rest
391 def dl_error(ex
, tb
= None):
393 self
.handler
.report_error(ex
)
398 tasks
.check(blockers
, dl_error
)
400 blockers
= [b
for b
in blockers
if not b
.happened
]
407 return download_impls()
409 def get_best_source(self
, impl
):
410 """Return the best download source for this implementation.
411 @rtype: L{model.RetrievalMethod}
412 @deprecated: use impl.best_download_source instead
414 return impl
.best_download_source