Implementation.id doesn't have to be path or digest
[zeroinstall.git] / zeroinstall / zerostore / __init__.py
blob5389441c7b7ad993a2e3b6c0d843d6865fc5bac6
1 """
2 Code for managing the implementation cache.
3 """
5 # Copyright (C) 2009, Thomas Leonard
6 # See the README file for details, or visit http://0install.net.
8 from zeroinstall import _
9 import os
10 from logging import debug, info, warn
12 from zeroinstall.support import basedir
13 from zeroinstall import SafeException, support
15 class BadDigest(SafeException):
16 """Thrown if a digest is invalid (either syntactically or cryptographically)."""
17 detail = None
19 class NotStored(SafeException):
20 """Throws if a requested implementation isn't in the cache."""
22 class NonwritableStore(SafeException):
23 """Attempt to add to a non-writable store directory."""
25 def _copytree2(src, dst):
26 import shutil
27 names = os.listdir(src)
28 assert os.path.isdir(dst)
29 errors = []
30 for name in names:
31 srcname = os.path.join(src, name)
32 dstname = os.path.join(dst, name)
33 if os.path.islink(srcname):
34 linkto = os.readlink(srcname)
35 os.symlink(linkto, dstname)
36 elif os.path.isdir(srcname):
37 os.mkdir(dstname)
38 mtime = int(os.lstat(srcname).st_mtime)
39 _copytree2(srcname, dstname)
40 os.utime(dstname, (mtime, mtime))
41 else:
42 shutil.copy2(srcname, dstname)
44 class Store:
45 """A directory for storing implementations."""
47 def __init__(self, dir, public = False):
48 """Create a new Store.
49 @param dir: directory to contain the implementations
50 @type dir: str
51 @param public: deprecated
52 @type public: bool"""
53 self.dir = dir
55 def __str__(self):
56 return _("Store '%s'") % self.dir
58 def lookup(self, digest):
59 try:
60 alg, value = digest.split('=', 1)
61 except ValueError:
62 raise BadDigest(_("Digest must be in the form ALG=VALUE, not '%s'") % digest)
63 try:
64 assert '/' not in value
65 int(value, 16) # Check valid format
66 except ValueError, ex:
67 raise BadDigest(_("Bad value for digest: %s") % str(ex))
68 dir = os.path.join(self.dir, digest)
69 if os.path.isdir(dir):
70 return dir
71 return None
73 def get_tmp_dir_for(self, required_digest):
74 """Create a temporary directory in the directory where we would store an implementation
75 with the given digest. This is used to setup a new implementation before being renamed if
76 it turns out OK.
77 @raise NonwritableStore: if we can't create it"""
78 try:
79 if not os.path.isdir(self.dir):
80 os.makedirs(self.dir)
81 from tempfile import mkdtemp
82 tmp = mkdtemp(dir = self.dir, prefix = 'tmp-')
83 os.chmod(tmp, 0755) # r-x for all; needed by 0store-helper
84 return tmp
85 except OSError, ex:
86 raise NonwritableStore(str(ex))
88 def add_archive_to_cache(self, required_digest, data, url, extract = None, type = None, start_offset = 0, try_helper = False):
89 import unpack
90 info(_("Caching new implementation (digest %s)"), required_digest)
92 if self.lookup(required_digest):
93 info(_("Not adding %s as it already exists!"), required_digest)
94 return
96 tmp = self.get_tmp_dir_for(required_digest)
97 try:
98 unpack.unpack_archive(url, data, tmp, extract, type = type, start_offset = start_offset)
99 except:
100 import shutil
101 shutil.rmtree(tmp)
102 raise
104 try:
105 self.check_manifest_and_rename(required_digest, tmp, extract, try_helper = try_helper)
106 except Exception, ex:
107 warn(_("Leaving extracted directory as %s"), tmp)
108 raise
110 def add_dir_to_cache(self, required_digest, path, try_helper = False):
111 """Copy the contents of path to the cache.
112 @param required_digest: the expected digest
113 @type required_digest: str
114 @param path: the root of the tree to copy
115 @type path: str
116 @param try_helper: attempt to use privileged helper before user cache (since 0.26)
117 @type try_helper: bool
118 @raise BadDigest: if the contents don't match the given digest."""
119 if self.lookup(required_digest):
120 info(_("Not adding %s as it already exists!"), required_digest)
121 return
123 tmp = self.get_tmp_dir_for(required_digest)
124 try:
125 _copytree2(path, tmp)
126 self.check_manifest_and_rename(required_digest, tmp, try_helper = try_helper)
127 except:
128 warn(_("Error importing directory."))
129 warn(_("Deleting %s"), tmp)
130 support.ro_rmtree(tmp)
131 raise
133 def _add_with_helper(self, required_digest, path):
134 """Use 0store-secure-add to copy 'path' to the system store.
135 @param required_digest: the digest for path
136 @type required_digest: str
137 @param path: root of implementation directory structure
138 @type path: str
139 @return: True iff the directory was copied into the system cache successfully
141 if required_digest.startswith('sha1='):
142 return False # Old digest alg not supported
143 helper = support.find_in_path('0store-secure-add-helper')
144 if not helper:
145 info(_("'0store-secure-add-helper' command not found. Not adding to system cache."))
146 return False
147 import subprocess
148 env = os.environ.copy()
149 env['ENV_NOT_CLEARED'] = 'Unclean' # (warn about insecure configurations)
150 env['HOME'] = 'Unclean' # (warn about insecure configurations)
151 dev_null = os.open('/dev/null', os.O_RDONLY)
152 try:
153 info(_("Trying to add to system cache using %s"), helper)
154 child = subprocess.Popen([helper, required_digest],
155 stdin = dev_null,
156 cwd = path,
157 env = env)
158 exit_code = child.wait()
159 finally:
160 os.close(dev_null)
162 if exit_code:
163 warn(_("0store-secure-add-helper failed."))
164 return False
166 info(_("Added succcessfully."))
167 return True
169 def check_manifest_and_rename(self, required_digest, tmp, extract = None, try_helper = False):
170 """Check that tmp[/extract] has the required_digest.
171 On success, rename the checked directory to the digest, and
172 make the whole tree read-only.
173 @param try_helper: attempt to use privileged helper to import to system cache first (since 0.26)
174 @type try_helper: bool
175 @raise BadDigest: if the input directory doesn't match the given digest"""
176 if extract:
177 extracted = os.path.join(tmp, extract)
178 if not os.path.isdir(extracted):
179 raise Exception(_('Directory %s not found in archive') % extract)
180 else:
181 extracted = tmp
183 import manifest
185 manifest.fixup_permissions(extracted)
187 alg, required_value = manifest.splitID(required_digest)
188 actual_digest = alg.getID(manifest.add_manifest_file(extracted, alg))
189 if actual_digest != required_digest:
190 raise BadDigest(_('Incorrect manifest -- archive is corrupted.\n'
191 'Required digest: %(required_digest)s\n'
192 'Actual digest: %(actual_digest)s\n') %
193 {'required_digest': required_digest, 'actual_digest': actual_digest})
195 if try_helper:
196 if self._add_with_helper(required_digest, extracted):
197 support.ro_rmtree(tmp)
198 return
199 info(_("Can't add to system store. Trying user store instead."))
201 final_name = os.path.join(self.dir, required_digest)
202 if os.path.isdir(final_name):
203 raise Exception(_("Item %s already stored.") % final_name) # XXX: not really an error
205 # If we just want a subdirectory then the rename will change
206 # extracted/.. and so we'll need write permission on 'extracted'
208 os.chmod(extracted, 0755)
209 os.rename(extracted, final_name)
210 os.chmod(final_name, 0555)
212 if extract:
213 os.rmdir(tmp)
215 def __repr__(self):
216 return "<store: %s>" % self.dir
218 class Stores(object):
219 """A list of L{Store}s. All stores are searched when looking for an implementation.
220 When storing, we use the first of the system caches (if writable), or the user's
221 cache otherwise."""
222 __slots__ = ['stores']
224 def __init__(self):
225 user_store = os.path.join(basedir.xdg_cache_home, '0install.net', 'implementations')
226 self.stores = [Store(user_store)]
228 impl_dirs = basedir.load_first_config('0install.net', 'injector',
229 'implementation-dirs')
230 debug(_("Location of 'implementation-dirs' config file being used: '%s'"), impl_dirs)
231 if impl_dirs:
232 dirs = file(impl_dirs)
233 else:
234 dirs = ['/var/cache/0install.net/implementations']
235 for directory in dirs:
236 directory = directory.strip()
237 if directory and not directory.startswith('#'):
238 debug(_("Added system store '%s'"), directory)
239 self.stores.append(Store(directory))
241 def lookup(self, digest):
242 return self.lookup_any([digest])
244 def lookup_any(self, digests):
245 """Search for digest in all stores."""
246 assert digests
247 for digest in digests:
248 assert digest
249 if '/' in digest or '=' not in digest:
250 raise BadDigest(_('Syntax error in digest (use ALG=VALUE, not %s)') % digest)
251 for store in self.stores:
252 path = store.lookup(digest)
253 if path:
254 return path
255 raise NotStored(_("Item with digests '%(digests)s' not found in stores. Searched:\n- %(stores)s") %
256 {'digests': digests, 'stores': '\n- '.join([s.dir for s in self.stores])})
258 def add_dir_to_cache(self, required_digest, dir):
259 """Add to the best writable cache.
260 @see: L{Store.add_dir_to_cache}"""
261 self._write_store(lambda store, **kwargs: store.add_dir_to_cache(required_digest, dir, **kwargs))
263 def add_archive_to_cache(self, required_digest, data, url, extract = None, type = None, start_offset = 0):
264 """Add to the best writable cache.
265 @see: L{Store.add_archive_to_cache}"""
266 self._write_store(lambda store, **kwargs: store.add_archive_to_cache(required_digest,
267 data, url, extract, type = type, start_offset = start_offset, **kwargs))
269 def _write_store(self, fn):
270 """Call fn(first_system_store). If it's read-only, try again with the user store."""
271 if len(self.stores) > 1:
272 try:
273 fn(self.get_first_system_store())
274 return
275 except NonwritableStore:
276 debug(_("%s not-writable. Trying helper instead."), self.get_first_system_store())
277 pass
278 fn(self.stores[0], try_helper = True)
280 def get_first_system_store(self):
281 """The first system store is the one we try writing to first.
282 @since: 0.30"""
283 try:
284 return self.stores[1]
285 except IndexError:
286 raise SafeException(_("No system stores have been configured"))