4 from tempfile
import mkdtemp
7 from logging
import debug
, info
, warn
10 from zeroinstall
.injector
import basedir
12 class BadDigest(Exception): pass
13 class NotStored(Exception): pass
15 _recent_gnu_tar
= None
17 global _recent_gnu_tar
18 if _recent_gnu_tar
is None:
19 _recent_gnu_tar
= False
20 version
= os
.popen('tar --version 2>&1').next()
21 if '(GNU tar)' in version
:
23 version
= version
.split(')', 1)[1].strip()
25 version
= map(int, version
.split('.'))
26 _recent_gnu_tar
= version
> [1, 14, 0]
28 warn("Failed to extract GNU tar version number")
29 return _recent_gnu_tar
31 def copytree2(src
, dst
):
32 names
= os
.listdir(src
)
33 assert os
.path
.isdir(dst
)
36 srcname
= os
.path
.join(src
, name
)
37 dstname
= os
.path
.join(dst
, name
)
38 if os
.path
.islink(srcname
):
39 linkto
= os
.readlink(srcname
)
40 os
.symlink(linkto
, dstname
)
41 elif os
.path
.isdir(srcname
):
43 mtime
= os
.lstat(srcname
).st_mtime
44 copytree2(srcname
, dstname
)
45 os
.utime(dstname
, (mtime
, mtime
))
47 shutil
.copy2(srcname
, dstname
)
50 def __init__(self
, dir):
53 def lookup(self
, digest
):
54 alg
, value
= digest
.split('=', 1)
56 assert '/' not in value
57 int(value
, 16) # Check valid format
58 dir = os
.path
.join(self
.dir, digest
)
59 if os
.path
.isdir(dir):
63 def add_archive_to_cache(self
, required_digest
, data
, url
, extract
= None):
64 if url
.endswith('.tar.bz2'):
65 self
.add_tbz_to_cache(required_digest
, data
, extract
)
67 if not (url
.endswith('.tar.gz') or url
.endswith('.tgz')):
68 warn('Unknown extension on "%s"; assuming tar.gz format' % url
)
69 self
.add_tgz_to_cache(required_digest
, data
, extract
)
71 def add_tbz_to_cache(self
, required_digest
, data
, extract
= None):
72 self
.add_tar_to_cache(required_digest
, data
, extract
, '--bzip2')
74 def add_tgz_to_cache(self
, required_digest
, data
, extract
= None):
75 self
.add_tar_to_cache(required_digest
, data
, extract
, '-z')
77 def add_tar_to_cache(self
, required_digest
, data
, extract
, decompress
):
78 """Data is a .tgz compressed archive. Extract it somewhere, check that
79 the digest is correct, and add it to the store.
80 extract is the name of a directory within the archive to extract, rather
81 than extracting the whole archive. This is most useful to remove an extra
82 top-level directory."""
83 assert required_digest
.startswith('sha1=')
84 info("Caching new implementation (digest %s)", required_digest
)
86 if self
.lookup(required_digest
):
87 info("Not adding %s as it already exists!", required_digest
)
91 args
= ['tar', decompress
, '-x', '--no-same-owner', '--no-same-permissions']
93 args
= ['tar', decompress
, '-xf', '-']
96 # Limit the characters we accept, to avoid sending dodgy
98 if not re
.match('^[a-zA-Z0-9][-_a-zA-Z0-9.]*$', extract
):
99 raise Exception('Illegal character in extract attribute')
102 tmp
= self
.extract(data
, args
)
104 self
.check_manifest_and_rename(required_digest
, tmp
, extract
)
105 except Exception, ex
:
106 warn("Leaving extracted directory as %s", tmp
)
109 def add_dir_to_cache(self
, required_digest
, path
):
110 if self
.lookup(required_digest
):
111 info("Not adding %s as it already exists!", required_digest
)
114 if not os
.path
.isdir(self
.dir):
115 os
.makedirs(self
.dir)
116 tmp
= mkdtemp(dir = self
.dir, prefix
= 'tmp-')
119 self
.check_manifest_and_rename(required_digest
, tmp
)
121 warn("Error importing directory.")
122 warn("Deleting %s", tmp
)
126 def check_manifest_and_rename(self
, required_digest
, tmp
, extract
= None):
128 extracted
= os
.path
.join(tmp
, extract
)
129 if not os
.path
.isdir(extracted
):
130 raise Exception('Directory %s not found in archive' % extract
)
134 sha1
= 'sha1=' + manifest
.add_manifest_file(extracted
, sha
.new()).hexdigest()
135 if sha1
!= required_digest
:
136 raise BadDigest('Incorrect manifest -- archive is corrupted.\n'
137 'Required digest: %s\n'
138 'Actual digest: %s\n' %
139 (required_digest
, sha1
))
141 final_name
= os
.path
.join(self
.dir, required_digest
)
142 if os
.path
.isdir(final_name
):
143 raise Exception("Item %s already stored." % final_name
)
145 os
.rename(os
.path
.join(tmp
, extract
), final_name
)
148 os
.rename(tmp
, final_name
)
150 def extract(self
, stream
, command
):
151 tmp
= mkdtemp(dir = self
.dir, prefix
= 'tmp-')
159 os
.dup2(stream
.fileno(), 0)
160 os
.execvp(command
[0], command
)
162 traceback
.print_exc()
165 id, status
= os
.waitpid(child
, 0)
168 raise Exception('Failed to extract archive; exit code %d' % status
)
174 class Stores(object):
175 __slots__
= ['stores']
178 user_store
= os
.path
.join(basedir
.xdg_cache_home
, '0install.net', 'implementations')
179 self
.stores
= [Store(user_store
)]
181 impl_dirs
= basedir
.load_first_config('0install.net', 'injector',
182 'implementation-dirs')
183 debug("Location of 'implementation-dirs' config file being used: '%s'", impl_dirs
)
185 for directory
in file(impl_dirs
):
186 directory
= directory
.strip()
187 if os
.path
.isdir(directory
):
188 self
.stores
.append(Store(directory
))
189 debug("Added system store '%s'", directory
)
191 info("Ignoring non-directory store '%s'", directory
)
193 def lookup(self
, digest
):
194 """Search for digest in all stores."""
196 if '/' in digest
or '=' not in digest
:
197 raise BadDigest('Syntax error in digest (use ALG=VALUE)')
198 for store
in self
.stores
:
199 path
= store
.lookup(digest
)
202 raise NotStored("Item with digest '%s' not found in stores. Searched:\n- %s" %
203 (digest
, '\n- '.join([s
.dir for s
in self
.stores
])))
205 def add_dir_to_cache(self
, required_digest
, dir):
206 self
.stores
[0].add_dir_to_cache(required_digest
, dir)
208 def add_archive_to_cache(self
, required_digest
, data
, url
, extract
= None):
209 self
.stores
[0].add_archive_to_cache(required_digest
, data
, url
, extract
)