3 import rox
, os
, urlparse
, tempfile
, time
, urllib
4 from rox
import g
, tasks
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
13 from xmltools
import *
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()
24 watch
= gtk
.gdk
.Cursor(gtk
.gdk
.WATCH
)
26 def autopackage_get_details(package
):
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
36 raise Exception("Can't find payload in autopackage (missing 'dataSize')")
39 def try_parse_version(version_str
):
41 return model
.parse_version(version_str
)
42 except model
.SafeException
, ex
:
43 warn("Bad version number '%s'", ex
)
46 def sanity_check_tar_file(path
, start_offset
):
48 with
open(path
, 'rb') as stream
:
49 stream
.seek(start_offset
)
50 child
= subprocess
.Popen(['tar', 'tf', path
], stdout
= subprocess
.PIPE
)
51 stdout
, unused
= child
.communicate(None)
53 first
= stdout
.split('\n', 1)[0]
54 if first
.startswith('./'):
55 rox
.alert('WARNING: leading "./" in tar archive member names (e.g. "%s"); extracting a subdirectory may not work. Consider recreating the archive without the prefix.' % first
)
58 def __init__(self
, feed_editor
, local_archive
= None):
59 self
.feed_editor
= feed_editor
61 self
.mime_type
= self
.start_offset
= None
63 widgets
= gtk
.glade
.XML(main
.gladefile
, 'add_archive')
65 tree
= widgets
.get_widget('extract_list')
66 model
= g
.TreeStore(str)
68 selection
= tree
.get_selection()
69 selection
.set_mode(g
.SELECTION_BROWSE
)
71 cell
= g
.CellRendererText()
72 col
= g
.TreeViewColumn('Extract', cell
)
73 col
.add_attribute(cell
, 'text', 0)
74 tree
.append_column(col
)
76 dialog
= widgets
.get_widget('add_archive')
78 mime_type
= widgets
.get_widget('mime_type')
79 mime_type
.set_active(0)
81 def local_archive_changed(chooser
):
83 path
= chooser
.get_filename()
84 widgets
.get_widget('subdirectory_frame').set_sensitive(False)
88 if mime_type
.get_active() == 0:
91 type = mime_type
.get_active_text()
93 archive_url
= widgets
.get_widget('archive_url')
94 url
= archive_url
.get_text()
96 url
= 'http://SITE/' + os
.path
.basename(path
)
97 archive_url
.set_text(url
)
101 if url
.endswith('.package'):
104 type = unpack
.type_from_url(url
)
106 if type == 'Autopackage':
107 # Autopackage isn't a real type. Examine the .package file
108 # and find out what it really is.
109 start_offset
, type = autopackage_get_details(path
)
111 if type.endswith('-tar'):
112 sanity_check_tar_file(path
, start_offset
= start_offset
)
114 self
.tmpdir
= tempfile
.mkdtemp('-0publish-gui')
116 # Must be readable to helper process running as 'zeroinst'...
117 old_umask
= os
.umask(0022)
119 unpack_dir
= os
.path
.join(self
.tmpdir
, 'unpacked')
122 dialog
.window
.set_cursor(watch
)
125 unpack
.unpack_archive(url
, file(path
), unpack_dir
,
126 type = type, start_offset
= start_offset
)
127 manifest
.fixup_permissions(unpack_dir
)
129 dialog
.window
.set_cursor(None)
133 chooser
.unselect_filename(path
)
136 iter = model
.append(None, ['Everything'])
137 items
= os
.listdir(unpack_dir
)
139 model
.append(iter, [f
])
141 # Choose a sensible default
142 iter = model
.get_iter_root()
143 if len(items
) == 1 and \
144 os
.path
.isdir(os
.path
.join(unpack_dir
, items
[0])) and \
145 items
[0] not in ('usr', 'opt', 'bin', 'etc', 'sbin', 'doc', 'var'):
146 iter = model
.iter_children(iter)
147 selection
.select_iter(iter)
149 self
.mime_type
= type
150 self
.start_offset
= start_offset
151 widgets
.get_widget('subdirectory_frame').set_sensitive(True)
153 local_archive_button
= widgets
.get_widget('local_archive')
154 local_archive_button
.connect('selection-changed', local_archive_changed
)
155 widgets
.get_widget('subdirectory_frame').set_sensitive(False)
157 def download(button
):
158 url
= widgets
.get_widget('archive_url').get_text()
160 raise Exception("Enter a URL to download from!")
162 chooser
= g
.FileChooserDialog('Save archive as...', dialog
, g
.FILE_CHOOSER_ACTION_SAVE
)
163 chooser
.set_current_folder(os
.getcwd()) # Needed with GTK 2.24 to avoid "Recently Used" thing
164 chooser
.set_current_name(os
.path
.basename(url
))
165 chooser
.add_button(g
.STOCK_CANCEL
, g
.RESPONSE_CANCEL
)
166 chooser
.add_button(g
.STOCK_SAVE
, g
.RESPONSE_OK
)
167 chooser
.set_default_response(g
.RESPONSE_OK
)
169 filename
= chooser
.get_filename()
171 if resp
!= g
.RESPONSE_OK
:
174 DownloadBox(url
, filename
, local_archive_button
, dialog
)
176 widgets
.get_widget('download').connect('clicked', download
)
179 if r
== g
.RESPONSE_OK
:
181 rox
.alert("Archive not downloaded yet!")
183 unpack_dir
= os
.path
.join(self
.tmpdir
, 'unpacked')
185 url
= widgets
.get_widget('archive_url').get_text()
186 if urlparse
.urlparse(url
)[1] == '':
187 raise Exception('Missing host name in URL "%s"' % url
)
188 if urlparse
.urlparse(url
)[2] == '':
189 raise Exception('Missing resource part in URL "%s"' % url
)
190 local_archive
= widgets
.get_widget('local_archive').get_filename()
191 if not local_archive
:
192 raise Exception('Please select a local file')
193 if selection
.iter_is_selected(model
.get_iter_root()):
197 _
, iter = selection
.get_selected()
198 extract
= model
[iter][0]
199 root
= os
.path
.join(unpack_dir
, extract
)
201 size
= os
.path
.getsize(local_archive
)
202 if self
.start_offset
:
203 size
-= self
.start_offset
204 self
.create_archive_element(url
, self
.mime_type
, root
, extract
, size
,
209 dialog
.connect('response', resp
)
212 local_archive_button
.set_filename(local_archive
)
213 initial_url
= 'http://SITE/' + os
.path
.basename(local_archive
)
214 widgets
.get_widget('archive_url').set_text(initial_url
)
216 def destroy_tmp(self
):
218 ro_rmtree(self
.tmpdir
)
221 def create_archive_element(self
, url
, mime_type
, root
, extract
, size
, start_offset
):
222 alg
= manifest
.get_algorithm('sha1new')
223 digest
= alg
.new_digest()
224 for line
in alg
.generate_manifest(root
):
225 digest
.update(line
+ '\n')
226 id = alg
.getID(digest
)
228 # Add it to the cache if missing
229 # Helps with setting 'main' attribute later
231 main
.stores
.lookup(id)
233 main
.stores
.add_dir_to_cache(id, root
)
235 # Do we already have an implementation with this digest?
236 impl_element
= self
.feed_editor
.find_implementation(id)
238 if impl_element
is None:
239 # No. Create a new implementation. Guess the details...
241 leaf
= url
.split('/')[-1]
243 for m
in re
.finditer(version_regexp
, leaf
):
244 match
= m
.group()[1:]
245 if version
is None or len(version
) < len(match
):
248 existing_versions
= self
.feed_editor
.list_versions()
250 if existing_versions
and version
:
251 parsed_version
= try_parse_version(version
)
253 older_versions
= [(v
, elem
) for v
, elem
in existing_versions
if v
< parsed_version
]
255 impl_element
= self
.feed_editor
.doc
.createElementNS(XMLNS_INTERFACE
, 'implementation')
258 # Try to add it just after the previous version's element in the XML document
259 insert_after(impl_element
, max(older_versions
)[1])
260 elif existing_versions
:
261 # Else add it before the first
262 insert_before(impl_element
, min(existing_versions
)[1])
265 insert_element(impl_element
, self
.feed_editor
.doc
.documentElement
)
267 impl_element
.setAttribute('id', id)
268 impl_element
.setAttribute('released', time
.strftime('%Y-%m-%d'))
269 impl_element
.setAttribute('version', version
or '0.1')
274 archive_element
= create_element(impl_element
, 'archive')
275 archive_element
.setAttribute('size', str(size
))
276 archive_element
.setAttribute('href', url
)
277 if extract
: archive_element
.setAttribute('extract', extract
)
278 if mime_type
: archive_element
.setAttribute('type', mime_type
)
279 if start_offset
: archive_element
.setAttribute('start-offset', str(start_offset
))
281 self
.feed_editor
.update_version_model()
284 self
.feed_editor
.edit_properties(element
= impl_element
)
287 def __init__(self
, url
, path
, archive_button
, parent
):
288 widgets
= gtk
.glade
.XML(main
.gladefile
, 'download')
291 output
= file(path
, 'w')
293 dialog
= widgets
.get_widget('download')
294 dialog
.set_transient_for(parent
)
295 progress
= widgets
.get_widget('progress')
297 cancelled
= tasks
.Blocker()
300 dialog
.connect('response', resp
)
313 # (urllib2 is buggy; no fileno)
314 stream
= urllib
.urlopen(url
)
315 size
= float(stream
.info().get('Content-Length', None))
319 yield tasks
.InputBlocker(stream
), cancelled
320 if cancelled
.happened
:
321 raise Exception("Download cancelled at user's request")
322 data
= os
.read(stream
.fileno(), 1024)
328 progress
.set_fraction(got
/ size
)
332 # No finally in python 2.4
337 archive_button
.set_filename(path
)
340 tasks
.Task(download())