Added 'type' and 'start-offset' attributes to <archive> elements.
[zeroinstall.git] / zeroinstall / injector / iface_cache.py
blob9d3d824856ec70c8a236dbc7f64e33bda0185766
1 # Copyright (C) 2006, Thomas Leonard
2 # See the README file for details, or visit http://0install.net.
4 """The interface cache stores downloaded and verified interfaces in ~/.cache/0install.net/interfaces (by default).
5 There are methods to query the cache, add to it, check signatures, etc."""
7 import os, sys, time
8 from logging import debug, info, warn
9 from cStringIO import StringIO
11 from zeroinstall.injector import reader, basedir
12 from zeroinstall.injector.namespaces import *
13 from zeroinstall.injector.model import *
14 from zeroinstall import zerostore
16 def _pretty_time(t):
17 assert isinstance(t, (int, long))
18 return time.strftime('%Y-%m-%d %H:%M:%S UTC', time.localtime(t))
20 class IfaceCache(object):
21 __slots__ = ['watchers', '_interfaces', 'stores']
23 def __init__(self):
24 self.watchers = []
25 self._interfaces = {}
27 self.stores = zerostore.Stores()
29 def add_watcher(self, w):
30 assert w not in self.watchers
31 self.watchers.append(w)
33 def check_signed_data(self, interface, signed_data, handler):
34 """Downloaded data is a GPG-signed message. Check that the signature is trusted
35 and call self.update_interface_from_network() when done.
36 Calls handler.confirm_trust_keys() if keys are not trusted.
37 """
38 assert isinstance(interface, Interface)
39 import gpg
40 data, sigs = gpg.check_stream(signed_data)
42 new_keys = False
43 import_error = None
44 for x in sigs:
45 need_key = x.need_key()
46 if need_key:
47 try:
48 self.download_key(interface, need_key)
49 except SafeException, ex:
50 import_error = ex
51 new_keys = True
53 if new_keys:
54 signed_data.seek(0)
55 data, sigs = gpg.check_stream(signed_data)
56 # If we got an error importing the keys, then report it now.
57 # If we still have missing keys, raise it as an exception, but
58 # if the keys got imported, just print and continue...
59 if import_error:
60 for x in sigs:
61 if x.need_key():
62 raise import_error
63 print >>sys.stderr, str(ex)
65 iface_xml = data.read()
66 data.close()
68 if not sigs:
69 raise SafeException('No signature on %s!\n'
70 'Possible reasons:\n'
71 '- You entered the interface URL incorrectly.\n'
72 '- The server delivered an error; try viewing the URL in a web browser.\n'
73 '- The developer gave you the URL of the unsigned interface by mistake.'
74 % interface.uri)
76 if not self.update_interface_if_trusted(interface, sigs, iface_xml):
77 handler.confirm_trust_keys(interface, sigs, iface_xml)
79 def update_interface_if_trusted(self, interface, sigs, xml):
80 for s in sigs:
81 if s.is_trusted():
82 self.update_interface_from_network(interface, xml, s.get_timestamp())
83 return True
84 return False
86 def download_key(self, interface, key_id):
87 assert interface
88 assert key_id
89 import urlparse, urllib2, shutil, tempfile
90 key_url = urlparse.urljoin(interface.uri, '%s.gpg' % key_id)
91 info("Fetching key from %s", key_url)
92 try:
93 stream = urllib2.urlopen(key_url)
94 # Python2.4: can't call fileno() on stream, so save to tmp file instead
95 tmpfile = tempfile.TemporaryFile(prefix = 'injector-dl-data-')
96 shutil.copyfileobj(stream, tmpfile)
97 tmpfile.flush()
98 stream.close()
99 except Exception, ex:
100 raise SafeException("Failed to download key from '%s': %s" % (key_url, str(ex)))
102 import gpg
104 tmpfile.seek(0)
105 gpg.import_key(tmpfile)
106 tmpfile.close()
108 def update_interface_from_network(self, interface, new_xml, modified_time):
109 """xml is the new XML (after the signature has been checked and
110 removed). modified_time will be set as an attribute on the root."""
111 debug("Updating '%s' from network; modified at %s" %
112 (interface.name or interface.uri, _pretty_time(modified_time)))
114 from xml.dom import minidom
115 doc = minidom.parseString(new_xml)
116 doc.documentElement.setAttribute('last-modified', str(modified_time))
117 new_xml = StringIO()
118 doc.writexml(new_xml)
120 self.import_new_interface(interface, new_xml.getvalue())
122 import writer
123 interface.last_checked = long(time.time())
124 writer.save_interface(interface)
126 info("Updated interface cache entry for %s (modified %s)",
127 interface.get_name(), _pretty_time(modified_time))
129 for w in self.watchers:
130 w.interface_changed(interface)
132 def import_new_interface(self, interface, new_xml):
133 upstream_dir = basedir.save_cache_path(config_site, 'interfaces')
134 cached = os.path.join(upstream_dir, escape(interface.uri))
136 if os.path.exists(cached):
137 old_xml = file(cached).read()
138 if old_xml == new_xml:
139 debug("No change")
140 return
142 stream = file(cached + '.new', 'w')
143 stream.write(new_xml)
144 stream.close()
145 new_mtime = reader.check_readable(interface.uri, cached + '.new')
146 assert new_mtime
147 if interface.last_modified:
148 if new_mtime < interface.last_modified:
149 raise SafeException("New interface's modification time is before old "
150 "version!"
151 "\nOld time: " + _pretty_time(interface.last_modified) +
152 "\nNew time: " + _pretty_time(new_mtime) +
153 "\nRefusing update (leaving new copy as " +
154 cached + ".new)")
155 if new_mtime == interface.last_modified:
156 raise SafeException("Interface has changed, but modification time "
157 "hasn't! Refusing update.")
158 os.rename(cached + '.new', cached)
159 debug("Saved as " + cached)
161 reader.update_from_cache(interface)
163 def get_interface(self, uri):
164 """Get the interface for uri, creating a new one if required.
165 New interfaces are initialised from the disk cache, but not from
166 the network."""
167 if type(uri) == str:
168 uri = unicode(uri)
169 assert isinstance(uri, unicode)
171 if uri in self._interfaces:
172 return self._interfaces[uri]
174 debug("Initialising new interface object for %s", uri)
175 self._interfaces[uri] = Interface(uri)
176 reader.update_from_cache(self._interfaces[uri])
177 return self._interfaces[uri]
179 def list_all_interfaces(self):
180 all = {}
181 for d in basedir.load_cache_paths(config_site, 'interfaces'):
182 for leaf in os.listdir(d):
183 if not leaf.startswith('.'):
184 all[leaf] = True
185 for d in basedir.load_config_paths(config_site, config_prog, 'user_overrides'):
186 for leaf in os.listdir(d):
187 if not leaf.startswith('.'):
188 all[leaf] = True
189 return map(unescape, all.keys())
191 def add_to_cache(self, source, data):
192 assert isinstance(source, DownloadSource)
193 required_digest = source.implementation.id
194 url = source.url
195 self.stores.add_archive_to_cache(required_digest, data, source.url, source.extract,
196 type = source.type, start_offset = source.start_offset or 0)
198 def get_icon_path(self, iface):
199 "Get the path of the cached icon for this interface, or None if not cached."
200 return basedir.load_first_cache(config_site, 'interface_icons',
201 escape(iface.uri))
203 iface_cache = IfaceCache()