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."""
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
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']
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.
38 assert isinstance(interface
, Interface
)
40 data
, sigs
= gpg
.check_stream(signed_data
)
45 need_key
= x
.need_key()
48 self
.download_key(interface
, need_key
)
49 except SafeException
, ex
:
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...
63 print >>sys
.stderr
, str(ex
)
65 iface_xml
= data
.read()
69 raise SafeException('No signature on %s!\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.'
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
):
82 self
.update_interface_from_network(interface
, xml
, s
.get_timestamp())
86 def download_key(self
, interface
, 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
)
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
)
100 raise SafeException("Failed to download key from '%s': %s" % (key_url
, str(ex
)))
105 gpg
.import_key(tmpfile
)
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
))
118 doc
.writexml(new_xml
)
120 self
.import_new_interface(interface
, new_xml
.getvalue())
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
:
142 stream
= file(cached
+ '.new', 'w')
143 stream
.write(new_xml
)
145 new_mtime
= reader
.check_readable(interface
.uri
, cached
+ '.new')
147 if interface
.last_modified
:
148 if new_mtime
< interface
.last_modified
:
149 raise SafeException("New interface's modification time is before old "
151 "\nOld time: " + _pretty_time(interface
.last_modified
) +
152 "\nNew time: " + _pretty_time(new_mtime
) +
153 "\nRefusing update (leaving new copy as " +
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
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
):
181 for d
in basedir
.load_cache_paths(config_site
, 'interfaces'):
182 for leaf
in os
.listdir(d
):
183 if not leaf
.startswith('.'):
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('.'):
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
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',
203 iface_cache
= IfaceCache()