Release 0.18
[0publish.git] / archive.py
blobfbdfbf7dc218e2c36f72bbd62a945c8cc7a9634d
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:]])
79 finally:
80 ro_rmtree(tmpdir)
82 local_ifaces = []
83 for impl in all_impls:
84 this_id = impl.getAttribute('id')
85 if this_id == archive_id:
86 break
87 if this_id.startswith('/') or this_id.startswith('.'):
88 local_ifaces.append(impl)
89 else:
90 if len(local_ifaces) == 0:
91 raise Exception('Nothing with id "%s", and no local implementations' % archive_id)
92 if len(local_ifaces) > 1:
93 raise Exception('Nothing with id "%s", and multiple local implementations!' % archive_id)
94 impl = local_ifaces[0]
95 impl.setAttribute('id', archive_id)
97 assert impl.getAttribute('id') == archive_id
99 archive = xmltools.create_element(impl, 'archive')
100 archive.setAttribute('href', url)
101 archive.setAttribute('size', str(os.stat(local_file).st_size - start_offset))
102 if extract is not None:
103 archive.setAttribute('extract', extract)
104 if start_offset:
105 archive.setAttribute('start-offset', str(start_offset))
106 if type:
107 archive.setAttribute('type', type)
109 # Remove digests we already
110 for x in xmltools.children(impl, 'manifest-digest'):
111 digest_element = x
112 break
113 else:
114 digest_element = doc.createElementNS(namespaces.XMLNS_IFACE, 'manifest-digest')
115 xmltools.insert_before(digest_element, archive)
116 for x in extra_digests - set([digest.digests(impl)]):
117 name, value = x.split('=')
118 digest_element.setAttribute(name, value)
120 return doc.toxml()