Start development series 0.25-post
[0publish.git] / archive.py
blob682d22594b6ea31ee851f8257fe896be92708f92
1 from xml.dom import minidom
2 from zeroinstall import SafeException
3 from zeroinstall.zerostore import manifest
4 try:
5 from zeroinstall.zerostore import unpack
6 except ImportError:
7 # Older versions don't have it
8 import unpack
9 from zeroinstall.injector import namespaces
10 import os, shutil, tempfile
12 import digest
13 import xmltools
15 def ro_rmtree(root):
16 """Like shutil.rmtree, except that we also delete with read-only items.
17 @param root: the root of the subtree to remove
18 @type root: str
19 @since: 0.28"""
20 for main, dirs, files in os.walk(root):
21 os.chmod(main, 0700)
22 shutil.rmtree(root)
24 def manifest_for_dir(dir, alg):
25 if alg == 'sha1':
26 # (for older versions of the injector)
27 import sha
28 class SHA1:
29 def new_digest(self): return sha.new()
30 def generate_manifest(self, dir): return manifest.generate_manifest(dir)
31 def getID(self, digest): return 'sha1=' + digest.hexdigest()
32 algorithm = SHA1()
33 else:
34 algorithm = manifest.get_algorithm(alg)
36 digest = algorithm.new_digest()
37 for line in algorithm.generate_manifest(dir):
38 digest.update(line + '\n')
39 return algorithm.getID(digest)
41 def autopackage_get_start_offset(package):
42 for line in file(package):
43 if line.startswith('export dataSize=') or line.startswith('export data_size='):
44 return os.path.getsize(package) - int(line.split('"', 2)[1])
45 raise Exception("Can't find payload in autopackage (missing 'dataSize')")
47 def add_archive(data, url, local_file, extract, algs):
48 if local_file is None:
49 local_file = os.path.abspath(os.path.basename(url))
50 if not os.path.exists(local_file):
51 raise SafeException("Use --archive-file option to specify a local copy of the archive "
52 "(default file '%s' does not exist)" % local_file)
54 doc = minidom.parseString(data)
56 assert algs
58 if local_file.endswith('.package'):
59 start_offset = autopackage_get_start_offset(local_file)
60 type = 'application/x-bzip-compressed-tar'
61 else:
62 start_offset = 0
63 type = None
65 all_impls = doc.documentElement.getElementsByTagNameNS(namespaces.XMLNS_IFACE, 'implementation')
66 tmpdir = tempfile.mkdtemp('-0publish')
67 try:
68 if start_offset or type:
69 unpack.unpack_archive(url, file(local_file), tmpdir, extract, start_offset = start_offset, type = type)
70 else:
71 unpack.unpack_archive(url, file(local_file), tmpdir, extract)
72 if extract:
73 extracted = os.path.join(tmpdir, extract)
74 else:
75 extracted = tmpdir
77 archive_id = manifest_for_dir(extracted, algs[0])
78 extra_digests = set([manifest_for_dir(extracted, a) for a in algs[1:]])
80 if '=' not in archive_id:
81 # New-style digests can't go in the ID
82 extra_digests.add(archive_id)
83 finally:
84 ro_rmtree(tmpdir)
86 local_ifaces = []
87 for impl in all_impls:
88 this_id = impl.getAttribute('id')
89 if this_id == archive_id:
90 break
91 if this_id.startswith('/') or this_id.startswith('.'):
92 local_ifaces.append(impl)
93 else:
94 if len(local_ifaces) == 0:
95 raise Exception('Nothing with id "%s", and no local implementations' % archive_id)
96 if len(local_ifaces) > 1:
97 raise Exception('Nothing with id "%s", and multiple local implementations!' % archive_id)
98 impl = local_ifaces[0]
99 impl.setAttribute('id', archive_id)
101 assert impl.getAttribute('id') == archive_id
103 archive = xmltools.create_element(impl, 'archive')
104 archive.setAttribute('href', url)
105 archive.setAttribute('size', str(os.stat(local_file).st_size - start_offset))
106 if extract is not None:
107 archive.setAttribute('extract', extract)
108 if start_offset:
109 archive.setAttribute('start-offset', str(start_offset))
110 if type:
111 archive.setAttribute('type', type)
113 # Remove digests we already
114 for x in xmltools.children(impl, 'manifest-digest'):
115 digest_element = x
116 break
117 else:
118 digest_element = doc.createElementNS(namespaces.XMLNS_IFACE, 'manifest-digest')
119 xmltools.insert_before(digest_element, archive)
120 for x in extra_digests - set([digest.digests(impl)]):
121 for old_alg in ['sha1=', 'sha1new=', 'sha256=']:
122 if x.startswith(old_alg):
123 name, value = x.split('=')
124 break
125 else:
126 assert '_' in x, "Invalid digest %s" % x
127 name, value = x.split('_')
128 digest_element.setAttribute(name, value)
130 return doc.toxml('utf-8')