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
)
41 data
, sigs
= gpg
.check_stream(signed_data
)
44 info("Failed to check GPG signature. Data received was:\n" + `signed_data
.read()`
)
50 need_key
= x
.need_key()
53 self
.download_key(interface
, need_key
)
54 except SafeException
, ex
:
60 data
, sigs
= gpg
.check_stream(signed_data
)
61 # If we got an error importing the keys, then report it now.
62 # If we still have missing keys, raise it as an exception, but
63 # if the keys got imported, just print and continue...
68 print >>sys
.stderr
, str(ex
)
70 iface_xml
= data
.read()
74 raise SafeException('No signature on %s!\n'
76 '- You entered the interface URL incorrectly.\n'
77 '- The server delivered an error; try viewing the URL in a web browser.\n'
78 '- The developer gave you the URL of the unsigned interface by mistake.'
81 if not self
.update_interface_if_trusted(interface
, sigs
, iface_xml
):
82 handler
.confirm_trust_keys(interface
, sigs
, iface_xml
)
84 def update_interface_if_trusted(self
, interface
, sigs
, xml
):
87 self
.update_interface_from_network(interface
, xml
, s
.get_timestamp())
91 def download_key(self
, interface
, key_id
):
94 import urlparse
, urllib2
, shutil
, tempfile
95 key_url
= urlparse
.urljoin(interface
.uri
, '%s.gpg' % key_id
)
96 info("Fetching key from %s", key_url
)
98 stream
= urllib2
.urlopen(key_url
)
99 # Python2.4: can't call fileno() on stream, so save to tmp file instead
100 tmpfile
= tempfile
.TemporaryFile(prefix
= 'injector-dl-data-')
101 shutil
.copyfileobj(stream
, tmpfile
)
104 except Exception, ex
:
105 raise SafeException("Failed to download key from '%s': %s" % (key_url
, str(ex
)))
110 gpg
.import_key(tmpfile
)
113 def update_interface_from_network(self
, interface
, new_xml
, modified_time
):
114 """xml is the new XML (after the signature has been checked and
115 removed). modified_time will be set as an attribute on the root."""
116 debug("Updating '%s' from network; modified at %s" %
117 (interface
.name
or interface
.uri
, _pretty_time(modified_time
)))
119 from xml
.dom
import minidom
120 doc
= minidom
.parseString(new_xml
)
121 doc
.documentElement
.setAttribute('last-modified', str(modified_time
))
123 doc
.writexml(new_xml
)
125 self
.import_new_interface(interface
, new_xml
.getvalue())
128 interface
.last_checked
= long(time
.time())
129 writer
.save_interface(interface
)
131 info("Updated interface cache entry for %s (modified %s)",
132 interface
.get_name(), _pretty_time(modified_time
))
134 for w
in self
.watchers
:
135 w
.interface_changed(interface
)
137 def import_new_interface(self
, interface
, new_xml
):
138 upstream_dir
= basedir
.save_cache_path(config_site
, 'interfaces')
139 cached
= os
.path
.join(upstream_dir
, escape(interface
.uri
))
141 if os
.path
.exists(cached
):
142 old_xml
= file(cached
).read()
143 if old_xml
== new_xml
:
147 stream
= file(cached
+ '.new', 'w')
148 stream
.write(new_xml
)
150 new_mtime
= reader
.check_readable(interface
.uri
, cached
+ '.new')
152 if interface
.last_modified
:
153 if new_mtime
< interface
.last_modified
:
154 raise SafeException("New interface's modification time is before old "
156 "\nOld time: " + _pretty_time(interface
.last_modified
) +
157 "\nNew time: " + _pretty_time(new_mtime
) +
158 "\nRefusing update (leaving new copy as " +
160 if new_mtime
== interface
.last_modified
:
161 raise SafeException("Interface has changed, but modification time "
162 "hasn't! Refusing update.")
163 os
.rename(cached
+ '.new', cached
)
164 debug("Saved as " + cached
)
166 reader
.update_from_cache(interface
)
168 def get_interface(self
, uri
):
169 """Get the interface for uri, creating a new one if required.
170 New interfaces are initialised from the disk cache, but not from
174 assert isinstance(uri
, unicode)
176 if uri
in self
._interfaces
:
177 return self
._interfaces
[uri
]
179 debug("Initialising new interface object for %s", uri
)
180 self
._interfaces
[uri
] = Interface(uri
)
181 reader
.update_from_cache(self
._interfaces
[uri
])
182 return self
._interfaces
[uri
]
184 def list_all_interfaces(self
):
186 for d
in basedir
.load_cache_paths(config_site
, 'interfaces'):
187 for leaf
in os
.listdir(d
):
188 if not leaf
.startswith('.'):
190 for d
in basedir
.load_config_paths(config_site
, config_prog
, 'user_overrides'):
191 for leaf
in os
.listdir(d
):
192 if not leaf
.startswith('.'):
194 return map(unescape
, all
.keys())
196 def add_to_cache(self
, source
, data
):
197 assert isinstance(source
, DownloadSource
)
198 required_digest
= source
.implementation
.id
200 self
.stores
.add_archive_to_cache(required_digest
, data
, source
.url
, source
.extract
,
201 type = source
.type, start_offset
= source
.start_offset
or 0)
203 def get_icon_path(self
, iface
):
204 "Get the path of the cached icon for this interface, or None if not cached."
205 return basedir
.load_first_cache(config_site
, 'interface_icons',
208 iface_cache
= IfaceCache()