Fixed version regex
[0publish-gui.git] / archive.py
blob9ac3f7fc7de162b84d2b427c43bf91fc42245c64
1 import re
3 import rox, os, urlparse, tempfile, time, urllib
4 from rox import g, tasks
5 import gtk.glade
7 from zeroinstall.support import ro_rmtree
8 from zeroinstall.injector import model
9 from zeroinstall.zerostore import unpack, manifest, NotStored
11 from logging import warn
12 import signing
13 from xmltools import *
14 import main
16 dotted_ints = '[0-9]+(\\.[0-9]+)*'
17 version_regexp = '[^a-zA-Z0-9](%s)(-(pre|rc|post|)%s)*' % (dotted_ints, dotted_ints)
19 def get_combo_value(combo):
20 i = combo.get_active()
21 m = combo.get_model()
22 return m[i][0]
24 watch = gtk.gdk.Cursor(gtk.gdk.WATCH)
26 def autopackage_get_details(package):
27 size = None
28 type = 'application/x-bzip-compressed-tar'
29 for line in file(package):
30 if line.startswith('export dataSize=') or line.startswith('export data_size='):
31 size = os.path.getsize(package) - int(line.split('"', 2)[1])
32 elif line.startswith('compression=') and 'lzma' in line:
33 type = 'application/x-lzma-compressed-tar'
34 if line.startswith('## END OF STUB'): break
35 if size is None:
36 raise Exception("Can't find payload in autopackage (missing 'dataSize')")
37 return size, type
39 def try_parse_version(version_str):
40 try:
41 return model.parse_version(version_str)
42 except model.SafeException, ex:
43 warn("Bad version number '%s'", ex)
44 return None
46 class AddArchiveBox:
47 def __init__(self, feed_editor, local_archive = None):
48 self.feed_editor = feed_editor
49 self.tmpdir = None
50 self.mime_type = self.start_offset = None
52 widgets = gtk.glade.XML(main.gladefile, 'add_archive')
54 tree = widgets.get_widget('extract_list')
55 model = g.TreeStore(str)
56 tree.set_model(model)
57 selection = tree.get_selection()
58 selection.set_mode(g.SELECTION_BROWSE)
60 cell = g.CellRendererText()
61 col = g.TreeViewColumn('Extract', cell)
62 col.add_attribute(cell, 'text', 0)
63 tree.append_column(col)
65 dialog = widgets.get_widget('add_archive')
67 mime_type = widgets.get_widget('mime_type')
68 mime_type.set_active(0)
70 def local_archive_changed(chooser):
71 model.clear()
72 path = chooser.get_filename()
73 widgets.get_widget('subdirectory_frame').set_sensitive(False)
74 self.destroy_tmp()
75 if not path: return
77 if mime_type.get_active() == 0:
78 type = None
79 else:
80 type = mime_type.get_active_text()
82 archive_url = widgets.get_widget('archive_url')
83 url = archive_url.get_text()
84 if not url:
85 url = 'http://SITE/' + os.path.basename(path)
86 archive_url.set_text(url)
88 start_offset = 0
89 if not type:
90 if url.endswith('.package'):
91 type = 'Autopackage'
92 else:
93 type = unpack.type_from_url(url)
95 if type == 'Autopackage':
96 # Autopackage isn't a real type. Examine the .package file
97 # and find out what it really is.
98 start_offset, type = autopackage_get_details(path)
100 self.tmpdir = tempfile.mkdtemp('-0publish-gui')
101 try:
102 # Must be readable to helper process running as 'zeroinst'...
103 old_umask = os.umask(0022)
104 try:
105 unpack_dir = os.path.join(self.tmpdir, 'unpacked')
106 os.mkdir(unpack_dir)
108 dialog.window.set_cursor(watch)
109 gtk.gdk.flush()
110 try:
111 unpack.unpack_archive(url, file(path), unpack_dir,
112 type = type, start_offset = start_offset)
113 manifest.fixup_permissions(unpack_dir)
114 finally:
115 dialog.window.set_cursor(None)
116 finally:
117 os.umask(old_umask)
118 except:
119 chooser.unselect_filename(path)
120 self.destroy_tmp()
121 raise
122 iter = model.append(None, ['Everything'])
123 items = os.listdir(unpack_dir)
124 for f in items:
125 model.append(iter, [f])
126 tree.expand_all()
127 # Choose a sensible default
128 iter = model.get_iter_root()
129 if len(items) == 1 and \
130 os.path.isdir(os.path.join(unpack_dir, items[0])) and \
131 items[0] not in ('usr', 'opt', 'bin', 'etc', 'sbin', 'doc', 'var'):
132 iter = model.iter_children(iter)
133 selection.select_iter(iter)
135 self.mime_type = type
136 self.start_offset = start_offset
137 widgets.get_widget('subdirectory_frame').set_sensitive(True)
139 local_archive_button = widgets.get_widget('local_archive')
140 local_archive_button.connect('selection-changed', local_archive_changed)
141 widgets.get_widget('subdirectory_frame').set_sensitive(False)
143 def download(button):
144 url = widgets.get_widget('archive_url').get_text()
145 if not url:
146 raise Exception("Enter a URL to download from!")
148 chooser = g.FileChooserDialog('Save archive as...', dialog, g.FILE_CHOOSER_ACTION_SAVE)
149 chooser.set_current_folder(os.getcwd()) # Needed with GTK 2.24 to avoid "Recently Used" thing
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 tasks.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())