Start development series 0.11-post
[0publish-gui.git] / archive.py
blobfab9e1bd06c2c589dcf8f1c73c0618d8058f4c79
1 from xml.dom import Node, minidom
2 import re
4 import rox, os, sys, urlparse, tempfile, shutil, time, urllib
5 from rox import g, tasks
6 import gtk.glade
8 from zeroinstall.support import ro_rmtree
9 from zeroinstall.injector import model
10 from zeroinstall.zerostore import unpack, manifest, NotStored
12 from logging import warn
13 import signing
14 from xmltools import *
15 import main
17 dotted_ints = '[0-9]+(.[0-9]+)*'
18 version_regexp = '[^a-zA-Z0-9](%s)(-(pre|rc|post|)%s)*' % (dotted_ints, dotted_ints)
20 def get_combo_value(combo):
21 i = combo.get_active()
22 m = combo.get_model()
23 return m[i][0]
25 watch = gtk.gdk.Cursor(gtk.gdk.WATCH)
27 def autopackage_get_details(package):
28 size = None
29 type = 'application/x-bzip-compressed-tar'
30 for line in file(package):
31 if line.startswith('export dataSize=') or line.startswith('export data_size='):
32 size = os.path.getsize(package) - int(line.split('"', 2)[1])
33 elif line.startswith('compression=') and 'lzma' in line:
34 type = 'application/x-lzma-compressed-tar'
35 if line.startswith('## END OF STUB'): break
36 if size is None:
37 raise Exception("Can't find payload in autopackage (missing 'dataSize')")
38 return size, type
40 def try_parse_version(version_str):
41 try:
42 return model.parse_version(version_str)
43 except model.SafeException, ex:
44 warn("Bad version number '%s'", ex)
45 return None
47 class AddArchiveBox:
48 def __init__(self, feed_editor, local_archive = None):
49 self.feed_editor = feed_editor
50 self.tmpdir = None
51 self.mime_type = self.start_offset = None
53 widgets = gtk.glade.XML(main.gladefile, 'add_archive')
55 tree = widgets.get_widget('extract_list')
56 model = g.TreeStore(str)
57 tree.set_model(model)
58 selection = tree.get_selection()
59 selection.set_mode(g.SELECTION_BROWSE)
61 cell = g.CellRendererText()
62 col = g.TreeViewColumn('Extract', cell)
63 col.add_attribute(cell, 'text', 0)
64 tree.append_column(col)
66 dialog = widgets.get_widget('add_archive')
68 mime_type = widgets.get_widget('mime_type')
69 mime_type.set_active(0)
71 def local_archive_changed(chooser):
72 model.clear()
73 path = chooser.get_filename()
74 widgets.get_widget('subdirectory_frame').set_sensitive(False)
75 self.destroy_tmp()
76 if not path: return
78 if mime_type.get_active() == 0:
79 type = None
80 else:
81 type = mime_type.get_active_text()
83 archive_url = widgets.get_widget('archive_url')
84 url = archive_url.get_text()
85 if not url:
86 url = 'http://SITE/' + os.path.basename(path)
87 archive_url.set_text(url)
89 start_offset = 0
90 if not type:
91 if url.endswith('.package'):
92 type = 'Autopackage'
93 else:
94 type = unpack.type_from_url(url)
96 if type == 'Autopackage':
97 # Autopackage isn't a real type. Examine the .package file
98 # and find out what it really is.
99 start_offset, type = autopackage_get_details(path)
101 self.tmpdir = tempfile.mkdtemp('-0publish-gui')
102 try:
103 # Must be readable to helper process running as 'zeroinst'...
104 old_umask = os.umask(0022)
105 try:
106 unpack_dir = os.path.join(self.tmpdir, 'unpacked')
107 os.mkdir(unpack_dir)
109 dialog.window.set_cursor(watch)
110 gtk.gdk.flush()
111 try:
112 unpack.unpack_archive(url, file(path), unpack_dir,
113 type = type, start_offset = start_offset)
114 manifest.fixup_permissions(unpack_dir)
115 finally:
116 dialog.window.set_cursor(None)
117 finally:
118 os.umask(old_umask)
119 except:
120 chooser.unselect_filename(path)
121 self.destroy_tmp()
122 raise
123 iter = model.append(None, ['Everything'])
124 items = os.listdir(unpack_dir)
125 for f in items:
126 model.append(iter, [f])
127 tree.expand_all()
128 # Choose a sensible default
129 iter = model.get_iter_root()
130 if len(items) == 1 and \
131 os.path.isdir(os.path.join(unpack_dir, items[0])) and \
132 items[0] not in ('usr', 'opt', 'bin', 'etc', 'sbin', 'doc', 'var'):
133 iter = model.iter_children(iter)
134 selection.select_iter(iter)
136 self.mime_type = type
137 self.start_offset = start_offset
138 widgets.get_widget('subdirectory_frame').set_sensitive(True)
140 local_archive_button = widgets.get_widget('local_archive')
141 local_archive_button.connect('selection-changed', local_archive_changed)
142 widgets.get_widget('subdirectory_frame').set_sensitive(False)
144 def download(button):
145 url = widgets.get_widget('archive_url').get_text()
146 if not url:
147 raise Exception("Enter a URL to download from!")
149 chooser = g.FileChooserDialog('Save archive as...', dialog, g.FILE_CHOOSER_ACTION_SAVE)
150 chooser.set_current_name(os.path.basename(url))
151 chooser.add_button(g.STOCK_CANCEL, g.RESPONSE_CANCEL)
152 chooser.add_button(g.STOCK_SAVE, g.RESPONSE_OK)
153 chooser.set_default_response(g.RESPONSE_OK)
154 resp = chooser.run()
155 filename = chooser.get_filename()
156 chooser.destroy()
157 if resp != g.RESPONSE_OK:
158 return
160 DownloadBox(url, filename, local_archive_button, dialog)
162 widgets.get_widget('download').connect('clicked', download)
164 def resp(dialog, r):
165 if r == g.RESPONSE_OK:
166 if not self.tmpdir:
167 rox.alert("Archive not downloaded yet!")
168 return
169 unpack_dir = os.path.join(self.tmpdir, 'unpacked')
171 url = widgets.get_widget('archive_url').get_text()
172 if urlparse.urlparse(url)[1] == '':
173 raise Exception('Missing host name in URL "%s"' % url)
174 if urlparse.urlparse(url)[2] == '':
175 raise Exception('Missing resource part in URL "%s"' % url)
176 local_archive = widgets.get_widget('local_archive').get_filename()
177 if not local_archive:
178 raise Exception('Please select a local file')
179 if selection.iter_is_selected(model.get_iter_root()):
180 root = unpack_dir
181 extract = None
182 else:
183 _, iter = selection.get_selected()
184 extract = model[iter][0]
185 root = os.path.join(unpack_dir, extract)
187 size = os.path.getsize(local_archive)
188 if self.start_offset:
189 size -= self.start_offset
190 self.create_archive_element(url, self.mime_type, root, extract, size,
191 self.start_offset)
192 self.destroy_tmp()
193 dialog.destroy()
195 dialog.connect('response', resp)
197 if local_archive:
198 local_archive_button.set_filename(local_archive)
199 initial_url = 'http://SITE/' + os.path.basename(local_archive)
200 widgets.get_widget('archive_url').set_text(initial_url)
202 def destroy_tmp(self):
203 if self.tmpdir:
204 ro_rmtree(self.tmpdir)
205 self.tmpdir = None
207 def create_archive_element(self, url, mime_type, root, extract, size, start_offset):
208 alg = manifest.get_algorithm('sha1new')
209 digest = alg.new_digest()
210 for line in alg.generate_manifest(root):
211 digest.update(line + '\n')
212 id = alg.getID(digest)
214 # Add it to the cache if missing
215 # Helps with setting 'main' attribute later
216 try:
217 main.stores.lookup(id)
218 except NotStored:
219 main.stores.add_dir_to_cache(id, root)
221 # Do we already have an implementation with this digest?
222 impl_element = self.feed_editor.find_implementation(id)
224 if impl_element is None:
225 # No. Create a new implementation. Guess the details...
227 leaf = url.split('/')[-1]
228 version = None
229 for m in re.finditer(version_regexp, leaf):
230 match = m.group()[1:]
231 if version is None or len(version) < len(match):
232 version = match
234 existing_versions = self.feed_editor.list_versions()
235 older_versions = []
236 if existing_versions and version:
237 parsed_version = try_parse_version(version)
238 if parsed_version:
239 older_versions = [(v, elem) for v, elem in existing_versions if v < parsed_version]
241 impl_element = self.feed_editor.doc.createElementNS(XMLNS_INTERFACE, 'implementation')
243 if older_versions:
244 # Try to add it just after the previous version's element in the XML document
245 insert_after(impl_element, max(older_versions)[1])
246 elif existing_versions:
247 # Else add it before the first
248 insert_before(impl_element, min(existing_versions)[1])
249 else:
250 # Put it in the root
251 insert_element(impl_element, self.feed_editor.doc.documentElement)
253 impl_element.setAttribute('id', id)
254 impl_element.setAttribute('released', time.strftime('%Y-%m-%d'))
255 if version: impl_element.setAttribute('version', version)
256 created_impl = True
257 else:
258 created_impl = False
260 archive_element = create_element(impl_element, 'archive')
261 archive_element.setAttribute('size', str(size))
262 archive_element.setAttribute('href', url)
263 if extract: archive_element.setAttribute('extract', extract)
264 if mime_type: archive_element.setAttribute('type', mime_type)
265 if start_offset: archive_element.setAttribute('start-offset', str(start_offset))
267 self.feed_editor.update_version_model()
269 if created_impl:
270 self.feed_editor.edit_properties(element = impl_element)
272 class DownloadBox:
273 def __init__(self, url, path, archive_button, parent):
274 widgets = gtk.glade.XML(main.gladefile, 'download')
275 gtk.gdk.flush()
277 output = file(path, 'w')
279 dialog = widgets.get_widget('download')
280 dialog.set_transient_for(parent)
281 progress = widgets.get_widget('progress')
283 cancelled = tasks.Blocker()
284 def resp(box, resp):
285 cancelled.trigger()
286 dialog.connect('response', resp)
288 def download():
289 stream = None
291 def cleanup():
292 dialog.destroy()
293 if output:
294 output.close()
295 if stream:
296 stream.close()
298 try:
299 # (urllib2 is buggy; no fileno)
300 stream = urllib.urlopen(url)
301 size = float(stream.info().get('Content-Length', None))
302 got = 0
304 while True:
305 yield signing.InputBlocker(stream), cancelled
306 if cancelled.happened:
307 raise Exception("Download cancelled at user's request")
308 data = os.read(stream.fileno(), 1024)
309 if not data: break
310 output.write(data)
311 got += len(data)
313 if size:
314 progress.set_fraction(got / size)
315 else:
316 progress.pulse()
317 except:
318 # No finally in python 2.4
319 cleanup()
320 raise
321 else:
322 cleanup()
323 archive_button.set_filename(path)
325 dialog.show()
326 tasks.Task(download())