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
)
47 def __init__(self
, feed_editor
, local_archive
= None):
48 self
.feed_editor
= feed_editor
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)
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
):
72 path
= chooser
.get_filename()
73 widgets
.get_widget('subdirectory_frame').set_sensitive(False)
77 if mime_type
.get_active() == 0:
80 type = mime_type
.get_active_text()
82 archive_url
= widgets
.get_widget('archive_url')
83 url
= archive_url
.get_text()
85 url
= 'http://SITE/' + os
.path
.basename(path
)
86 archive_url
.set_text(url
)
90 if url
.endswith('.package'):
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')
102 # Must be readable to helper process running as 'zeroinst'...
103 old_umask
= os
.umask(0022)
105 unpack_dir
= os
.path
.join(self
.tmpdir
, 'unpacked')
108 dialog
.window
.set_cursor(watch
)
111 unpack
.unpack_archive(url
, file(path
), unpack_dir
,
112 type = type, start_offset
= start_offset
)
113 manifest
.fixup_permissions(unpack_dir
)
115 dialog
.window
.set_cursor(None)
119 chooser
.unselect_filename(path
)
122 iter = model
.append(None, ['Everything'])
123 items
= os
.listdir(unpack_dir
)
125 model
.append(iter, [f
])
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()
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
)
155 filename
= chooser
.get_filename()
157 if resp
!= g
.RESPONSE_OK
:
160 DownloadBox(url
, filename
, local_archive_button
, dialog
)
162 widgets
.get_widget('download').connect('clicked', download
)
165 if r
== g
.RESPONSE_OK
:
167 rox
.alert("Archive not downloaded yet!")
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()):
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
,
195 dialog
.connect('response', resp
)
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
):
204 ro_rmtree(self
.tmpdir
)
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
217 main
.stores
.lookup(id)
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]
229 for m
in re
.finditer(version_regexp
, leaf
):
230 match
= m
.group()[1:]
231 if version
is None or len(version
) < len(match
):
234 existing_versions
= self
.feed_editor
.list_versions()
236 if existing_versions
and version
:
237 parsed_version
= try_parse_version(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')
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])
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
)
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()
270 self
.feed_editor
.edit_properties(element
= impl_element
)
273 def __init__(self
, url
, path
, archive_button
, parent
):
274 widgets
= gtk
.glade
.XML(main
.gladefile
, 'download')
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()
286 dialog
.connect('response', resp
)
299 # (urllib2 is buggy; no fileno)
300 stream
= urllib
.urlopen(url
)
301 size
= float(stream
.info().get('Content-Length', None))
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)
314 progress
.set_fraction(got
/ size
)
318 # No finally in python 2.4
323 archive_button
.set_filename(path
)
326 tasks
.Task(download())