5 from dialog
import Dialog
, alert
6 from zeroinstall
.injector
.iface_cache
import iface_cache
7 from zeroinstall
.injector
import basedir
, namespaces
, model
8 from zeroinstall
.zerostore
import BadDigest
, manifest
9 from zeroinstall
import support
10 from treetips
import TreeTips
12 ROX_IFACE
= 'http://rox.sourceforge.net/2005/interfaces/ROX-Filer'
21 def popup_menu(bev
, obj
):
23 for i
in obj
.menu_items
:
25 item
= gtk
.SeparatorMenuItem()
28 item
= gtk
.MenuItem(name
)
29 item
.connect('activate', lambda item
, cb
=cb
: cb(obj
))
32 menu
.popup(None, None, None, bev
.button
, bev
.time
)
34 def size_if_exists(path
):
35 "Get the size for a file, or 0 if it doesn't exist."
36 if path
and os
.path
.isfile(path
):
37 return os
.path
.getsize(path
)
41 "Get the size for a directory tree. Get the size from the .manifest if possible."
42 man
= os
.path
.join(path
, '.manifest')
43 if os
.path
.exists(man
):
44 size
= os
.path
.getsize(man
)
45 for line
in file(man
):
47 size
+= long(line
.split(' ', 4)[3])
50 for root
, dirs
, files
in os
.walk(path
):
52 size
+= os
.path
.getsize(os
.path
.join(root
, name
))
57 return iface
.get_name() + ' - ' + iface
.summary
58 return iface
.get_name()
60 def get_selected_paths(tree_view
):
61 "GTK 2.0 doesn't have this built-in"
62 selection
= tree_view
.get_selection()
64 def add(model
, path
, iter):
66 selection
.selected_foreach(add
)
74 class CachedInterface(object):
75 def __init__(self
, uri
, size
):
80 if not self
.uri
.startswith('/'):
81 cached_iface
= basedir
.load_first_cache(namespaces
.config_site
,
82 'interfaces', model
.escape(self
.uri
))
84 #print "Delete", cached_iface
85 os
.unlink(cached_iface
)
86 user_overrides
= basedir
.load_first_config(namespaces
.config_site
,
87 namespaces
.config_prog
,
88 'user_overrides', model
.escape(self
.uri
))
90 #print "Delete", user_overrides
91 os
.unlink(user_overrides
)
93 def __cmp__(self
, other
):
94 return self
.uri
.__cmp
__(other
.uri
)
96 class ValidInterface(CachedInterface
):
97 def __init__(self
, iface
, size
):
98 CachedInterface
.__init
__(self
, iface
.uri
, size
)
102 def append_to(self
, model
, iter):
103 iter2
= model
.append(iter,
104 [self
.uri
, self
.size
, None, summary(self
.iface
), self
])
105 for cached_impl
in self
.in_cache
:
106 cached_impl
.append_to(model
, iter2
)
108 def get_may_delete(self
):
109 for c
in self
.in_cache
:
110 if not isinstance(c
, LocalImplementation
):
111 return False # Still some impls cached
114 may_delete
= property(get_may_delete
)
116 class InvalidInterface(CachedInterface
):
119 def __init__(self
, uri
, ex
, size
):
120 CachedInterface
.__init
__(self
, uri
, size
)
123 def append_to(self
, model
, iter):
124 model
.append(iter, [self
.uri
, self
.size
, None, self
.ex
, self
])
126 class LocalImplementation
:
129 def __init__(self
, impl
):
132 def append_to(self
, model
, iter):
133 model
.append(iter, [self
.impl
.id, 0, None, 'This is a local version, not held in the cache.', self
])
135 class CachedImplementation
:
138 def __init__(self
, cache_dir
, name
):
139 self
.impl_path
= os
.path
.join(cache_dir
, name
)
140 self
.size
= get_size(self
.impl_path
)
144 #print "Delete", self.impl_path
145 shutil
.rmtree(self
.impl_path
)
148 os
.spawnlp(os
.P_WAIT
, '0launch', '0launch', ROX_IFACE
, '-d', self
.impl_path
)
152 manifest
.verify(self
.impl_path
)
153 except BadDigest
, ex
:
154 box
= gtk
.MessageDialog(None, 0,
155 gtk
.MESSAGE_WARNING
, gtk
.BUTTONS_OK
, str(ex
))
157 swin
= gtk
.ScrolledWindow()
158 buffer = gtk
.TextBuffer()
159 mono
= buffer.create_tag('mono', family
= 'Monospace')
160 buffer.insert_with_tags(buffer.get_start_iter(), ex
.detail
, mono
)
161 text
= gtk
.TextView(buffer)
162 text
.set_editable(False)
163 text
.set_cursor_visible(False)
165 swin
.set_shadow_type(gtk
.SHADOW_IN
)
166 swin
.set_border_width(4)
167 box
.vbox
.pack_start(swin
)
169 box
.set_resizable(True)
171 box
= gtk
.MessageDialog(None, 0,
172 gtk
.MESSAGE_INFO
, gtk
.BUTTONS_OK
,
173 'Contents match digest; nothing has been changed.')
177 menu_items
= [('Open in ROX-Filer', open_rox
),
178 ('Verify integrity', verify
)]
180 class UnusedImplementation(CachedImplementation
):
181 def append_to(self
, model
, iter):
182 model
.append(iter, [self
.name
, self
.size
, None, self
.impl_path
, self
])
184 class KnownImplementation(CachedImplementation
):
185 def __init__(self
, cached_iface
, cache_dir
, impl
, impl_size
):
186 CachedImplementation
.__init
__(self
, cache_dir
, impl
.id)
187 self
.cached_iface
= cached_iface
189 self
.size
= impl_size
192 CachedImplementation
.delete(self
)
193 self
.cached_iface
.in_cache
.remove(self
)
195 def append_to(self
, model
, iter):
197 ['Version %s : %s' % (self
.impl
.get_version(), self
.impl
.id),
202 def __cmp__(self
, other
):
203 if hasattr(other
, 'impl'):
204 return self
.impl
.__cmp
__(other
.impl
)
207 class CacheExplorer(Dialog
):
209 Dialog
.__init
__(self
)
210 self
.set_title('Zero Install Cache')
211 self
.set_default_size(gtk
.gdk
.screen_width() / 2, gtk
.gdk
.screen_height() / 2)
214 self
.model
= gtk
.TreeStore(str, int, str, str, object)
215 self
.tree_view
= gtk
.TreeView(self
.model
)
218 swin
= gtk
.ScrolledWindow()
219 swin
.set_policy(gtk
.POLICY_AUTOMATIC
, gtk
.POLICY_ALWAYS
)
220 swin
.set_shadow_type(gtk
.SHADOW_IN
)
221 swin
.add(self
.tree_view
)
222 self
.vbox
.pack_start(swin
, True, True, 0)
223 self
.tree_view
.set_rules_hint(True)
226 column
= gtk
.TreeViewColumn('Item', gtk
.CellRendererText(), text
= ITEM
)
227 column
.set_resizable(True)
228 self
.tree_view
.append_column(column
)
230 cell
= gtk
.CellRendererText()
231 cell
.set_property('xalign', 1.0)
232 column
= gtk
.TreeViewColumn('Size', cell
, text
= PRETTY_SIZE
)
233 self
.tree_view
.append_column(column
)
235 def button_press(tree_view
, bev
):
238 pos
= tree_view
.get_path_at_pos(int(bev
.x
), int(bev
.y
))
241 path
, col
, x
, y
= pos
242 obj
= self
.model
[path
][ITEM_OBJECT
]
243 if obj
and hasattr(obj
, 'menu_items'):
245 self
.tree_view
.connect('button-press-event', button_press
)
248 def motion(tree_view
, ev
):
249 if ev
.window
is not tree_view
.get_bin_window():
251 pos
= tree_view
.get_path_at_pos(int(ev
.x
), int(ev
.y
))
254 row
= self
.model
[path
]
258 tips
.prime(tree_view
, tip
)
264 self
.tree_view
.connect('motion-notify-event', motion
)
265 self
.tree_view
.connect('leave-notify-event', lambda tv
, ev
: tips
.hide())
269 self
.add_button(gtk
.STOCK_HELP
, gtk
.RESPONSE_HELP
)
270 self
.add_button(gtk
.STOCK_CLOSE
, gtk
.RESPONSE_OK
)
271 self
.add_button(gtk
.STOCK_DELETE
, DELETE
)
272 self
.set_default_response(gtk
.RESPONSE_OK
)
274 selection
= self
.tree_view
.get_selection()
275 def selection_changed(selection
):
277 for x
in get_selected_paths(self
.tree_view
):
278 obj
= self
.model
[x
][ITEM_OBJECT
]
279 if obj
is None or not obj
.may_delete
:
280 self
.set_response_sensitive(DELETE
, False)
283 self
.set_response_sensitive(DELETE
, any_selected
)
284 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
285 selection
.connect('changed', selection_changed
)
286 selection_changed(selection
)
288 def response(dialog
, resp
):
289 if resp
== gtk
.RESPONSE_OK
:
291 elif resp
== gtk
.RESPONSE_HELP
:
295 self
.connect('response', response
)
301 paths
= get_selected_paths(self
.tree_view
)
304 item
= model
[path
][ITEM_OBJECT
]
309 errors
.append(str(ex
))
311 model
.remove(model
.get_iter(path
))
315 alert(self
, "Failed to delete:\n%s" % '\n'.join(errors
))
317 def populate_model(self
):
318 # Find cached implementations
320 unowned
= {} # Impl ID -> Store
321 duplicates
= [] # TODO
323 for s
in iface_cache
.stores
.stores
:
324 if os
.path
.isdir(s
.dir):
325 for id in os
.listdir(s
.dir):
327 duplicates
.append(id)
331 error_interfaces
= []
333 # Look through cached interfaces for implementation owners
334 all
= iface_cache
.list_all_interfaces()
339 if uri
.startswith('/'):
342 cached_iface
= basedir
.load_first_cache(namespaces
.config_site
,
343 'interfaces', model
.escape(uri
))
344 user_overrides
= basedir
.load_first_config(namespaces
.config_site
,
345 namespaces
.config_prog
,
346 'user_overrides', model
.escape(uri
))
348 iface_size
= size_if_exists(cached_iface
) + size_if_exists(user_overrides
)
349 iface
= iface_cache
.get_interface(uri
)
350 except Exception, ex
:
351 error_interfaces
.append((uri
, str(ex
), iface_size
))
353 cached_iface
= ValidInterface(iface
, iface_size
)
354 for impl
in iface
.implementations
.values():
355 if impl
.id.startswith('/') or impl
.id.startswith('.'):
356 cached_iface
.in_cache
.append(LocalImplementation(impl
))
357 if impl
.id in unowned
:
358 cached_dir
= unowned
[impl
.id].dir
359 impl_path
= os
.path
.join(cached_dir
, impl
.id)
360 impl_size
= get_size(impl_path
)
361 cached_iface
.in_cache
.append(KnownImplementation(cached_iface
, cached_dir
, impl
, impl_size
))
363 cached_iface
.in_cache
.sort()
364 ok_interfaces
.append(cached_iface
)
367 iter = self
.model
.append(None, [_("Invalid interfaces (unreadable)"),
369 _("These interfaces exist in the cache but cannot be "
370 "read. You should probably delete them."),
372 for uri
, ex
, size
in error_interfaces
:
373 item
= InvalidInterface(uri
, ex
, size
)
374 item
.append_to(self
.model
, iter)
377 local_dir
= os
.path
.join(basedir
.xdg_cache_home
, '0install.net', 'implementations')
379 if unowned
[id].dir == local_dir
:
380 impl
= UnusedImplementation(local_dir
, id)
381 unowned_sizes
.append((impl
.size
, impl
))
383 iter = self
.model
.append(None, [_("Unowned implementations and temporary files"),
385 _("These probably aren't needed any longer. You can "
386 "delete them."), None])
388 unowned_sizes
.reverse()
389 for size
, item
in unowned_sizes
:
390 item
.append_to(self
.model
, iter)
393 iter = self
.model
.append(None,
396 _("Interfaces in the cache"),
398 for item
in ok_interfaces
:
399 item
.append_to(self
.model
, iter)
402 def update_sizes(self
):
403 """Set PRETTY_SIZE to the total size, including all children."""
406 total
= m
[itr
][SELF_SIZE
]
407 child
= m
.iter_children(itr
)
409 total
+= update(child
)
410 child
= m
.iter_next(child
)
411 m
[itr
][PRETTY_SIZE
] = support
.pretty_size(total
)
413 itr
= m
.get_iter_root()
416 itr
= m
.iter_next(itr
)
418 cache_help
= help_box
.HelpBox("Cache Explorer Help",
420 When you run a program using Zero Install, it downloads the program's 'interface' file, \
421 which gives information about which versions of the program are available. This interface \
422 file is stored in the cache to save downloading it next time you run the program.
424 When you have chosen which version (implementation) of the program you want to \
425 run, Zero Install downloads that version and stores it in the cache too. Zero Install lets \
426 you have many different versions of each program on your computer at once. This is useful, \
427 since it lets you use an old version if needed, and different programs may need to use \
428 different versions of libraries in some cases.
430 The cache viewer shows you all the interfaces and implementations in your cache. \
431 This is useful to find versions you don't need anymore, so that you can delete them and \
432 free up some disk space."""),
434 ('Invalid interfaces', """
435 The cache viewer gets a list of all interfaces in your cache. However, some may not \
436 be valid; they are shown in the 'Invalid interfaces' section. It should be fine to \
437 delete these. An invalid interface may be caused by a local interface that no longer \
438 exists, by a failed attempt to download an interface (the name ends in '.new'), or \
439 by the interface file format changing since the interface was downloaded."""),
441 ('Unowned implementations and temporary files', """
442 The cache viewer searches through all the interfaces to find out which implementations \
443 they use. If no interface uses an implementation, it is shown in the 'Unowned implementations' \
446 Unowned implementations can result from old versions of a program no longer being listed \
447 in the interface file. Temporary files are created when unpacking an implementation after \
448 downloading it. If the archive is corrupted, the unpacked files may be left there. Unless \
449 you are currently unpacking new programs, it should be fine to delete everything in this \
453 All remaining interfaces are listed in this section. You may wish to delete old versions of \
454 certain programs. Deleting a program which you may later want to run will require it to be downloaded \
455 again. Deleting a version of a program which is currently running may cause it to crash, so be careful!