1 # Copyright (C) 2008, Thomas Leonard
2 # See the README file for details, or visit http://0install.net.
8 from dialog
import Dialog
, alert
9 from zeroinstall
.injector
.iface_cache
import iface_cache
10 from zeroinstall
.injector
import namespaces
, model
11 from zeroinstall
.zerostore
import BadDigest
, manifest
12 from zeroinstall
import support
13 from zeroinstall
.support
import basedir
14 from zeroinstall
.gtkui
.treetips
import TreeTips
16 ROX_IFACE
= 'http://rox.sourceforge.net/2005/interfaces/ROX-Filer'
25 def popup_menu(bev
, obj
):
27 for i
in obj
.menu_items
:
29 item
= gtk
.SeparatorMenuItem()
32 item
= gtk
.MenuItem(name
)
33 item
.connect('activate', lambda item
, cb
=cb
: cb(obj
))
36 menu
.popup(None, None, None, bev
.button
, bev
.time
)
38 def size_if_exists(path
):
39 "Get the size for a file, or 0 if it doesn't exist."
40 if path
and os
.path
.isfile(path
):
41 return os
.path
.getsize(path
)
45 "Get the size for a directory tree. Get the size from the .manifest if possible."
46 man
= os
.path
.join(path
, '.manifest')
47 if os
.path
.exists(man
):
48 size
= os
.path
.getsize(man
)
49 for line
in file(man
):
51 size
+= long(line
.split(' ', 4)[3])
54 for root
, dirs
, files
in os
.walk(path
):
56 size
+= os
.path
.getsize(os
.path
.join(root
, name
))
61 return iface
.get_name() + ' - ' + iface
.summary
62 return iface
.get_name()
64 def get_selected_paths(tree_view
):
65 "GTK 2.0 doesn't have this built-in"
66 selection
= tree_view
.get_selection()
68 def add(model
, path
, iter):
70 selection
.selected_foreach(add
)
78 class CachedInterface(object):
79 def __init__(self
, uri
, size
):
84 if not self
.uri
.startswith('/'):
85 cached_iface
= basedir
.load_first_cache(namespaces
.config_site
,
86 'interfaces', model
.escape(self
.uri
))
88 #print "Delete", cached_iface
89 os
.unlink(cached_iface
)
90 user_overrides
= basedir
.load_first_config(namespaces
.config_site
,
91 namespaces
.config_prog
,
92 'user_overrides', model
.escape(self
.uri
))
94 #print "Delete", user_overrides
95 os
.unlink(user_overrides
)
97 def __cmp__(self
, other
):
98 return self
.uri
.__cmp
__(other
.uri
)
100 class ValidInterface(CachedInterface
):
101 def __init__(self
, iface
, size
):
102 CachedInterface
.__init
__(self
, iface
.uri
, size
)
106 def append_to(self
, model
, iter):
107 iter2
= model
.append(iter,
108 [self
.uri
, self
.size
, None, summary(self
.iface
), self
])
109 for cached_impl
in self
.in_cache
:
110 cached_impl
.append_to(model
, iter2
)
112 def get_may_delete(self
):
113 for c
in self
.in_cache
:
114 if not isinstance(c
, LocalImplementation
):
115 return False # Still some impls cached
118 may_delete
= property(get_may_delete
)
120 class InvalidInterface(CachedInterface
):
123 def __init__(self
, uri
, ex
, size
):
124 CachedInterface
.__init
__(self
, uri
, size
)
127 def append_to(self
, model
, iter):
128 model
.append(iter, [self
.uri
, self
.size
, None, self
.ex
, self
])
130 class LocalImplementation
:
133 def __init__(self
, impl
):
136 def append_to(self
, model
, iter):
137 model
.append(iter, [self
.impl
.id, 0, None, 'This is a local version, not held in the cache.', self
])
139 class CachedImplementation
:
142 def __init__(self
, cache_dir
, name
):
143 self
.impl_path
= os
.path
.join(cache_dir
, name
)
144 self
.size
= get_size(self
.impl_path
)
148 #print "Delete", self.impl_path
149 support
.ro_rmtree(self
.impl_path
)
152 os
.spawnlp(os
.P_WAIT
, '0launch', '0launch', ROX_IFACE
, '-d', self
.impl_path
)
156 manifest
.verify(self
.impl_path
)
157 except BadDigest
, ex
:
158 box
= gtk
.MessageDialog(None, 0,
159 gtk
.MESSAGE_WARNING
, gtk
.BUTTONS_OK
, str(ex
))
161 swin
= gtk
.ScrolledWindow()
162 buffer = gtk
.TextBuffer()
163 mono
= buffer.create_tag('mono', family
= 'Monospace')
164 buffer.insert_with_tags(buffer.get_start_iter(), ex
.detail
, mono
)
165 text
= gtk
.TextView(buffer)
166 text
.set_editable(False)
167 text
.set_cursor_visible(False)
169 swin
.set_shadow_type(gtk
.SHADOW_IN
)
170 swin
.set_border_width(4)
171 box
.vbox
.pack_start(swin
)
173 box
.set_resizable(True)
175 box
= gtk
.MessageDialog(None, 0,
176 gtk
.MESSAGE_INFO
, gtk
.BUTTONS_OK
,
177 'Contents match digest; nothing has been changed.')
181 menu_items
= [('Open in ROX-Filer', open_rox
),
182 ('Verify integrity', verify
)]
184 class UnusedImplementation(CachedImplementation
):
185 def append_to(self
, model
, iter):
186 model
.append(iter, [self
.name
, self
.size
, None, self
.impl_path
, self
])
188 class KnownImplementation(CachedImplementation
):
189 def __init__(self
, cached_iface
, cache_dir
, impl
, impl_size
):
190 CachedImplementation
.__init
__(self
, cache_dir
, impl
.id)
191 self
.cached_iface
= cached_iface
193 self
.size
= impl_size
196 CachedImplementation
.delete(self
)
197 self
.cached_iface
.in_cache
.remove(self
)
199 def append_to(self
, model
, iter):
201 ['Version %s : %s' % (self
.impl
.get_version(), self
.impl
.id),
206 def __cmp__(self
, other
):
207 if hasattr(other
, 'impl'):
208 return self
.impl
.__cmp
__(other
.impl
)
211 class CacheExplorer(Dialog
):
213 Dialog
.__init
__(self
)
214 self
.set_title('Zero Install Cache')
215 self
.set_default_size(gtk
.gdk
.screen_width() / 2, gtk
.gdk
.screen_height() / 2)
218 self
.model
= gtk
.TreeStore(str, int, str, str, object)
219 self
.tree_view
= gtk
.TreeView(self
.model
)
222 swin
= gtk
.ScrolledWindow()
223 swin
.set_policy(gtk
.POLICY_AUTOMATIC
, gtk
.POLICY_ALWAYS
)
224 swin
.set_shadow_type(gtk
.SHADOW_IN
)
225 swin
.add(self
.tree_view
)
226 self
.vbox
.pack_start(swin
, True, True, 0)
227 self
.tree_view
.set_rules_hint(True)
230 column
= gtk
.TreeViewColumn('Item', gtk
.CellRendererText(), text
= ITEM
)
231 column
.set_resizable(True)
232 self
.tree_view
.append_column(column
)
234 cell
= gtk
.CellRendererText()
235 cell
.set_property('xalign', 1.0)
236 column
= gtk
.TreeViewColumn('Size', cell
, text
= PRETTY_SIZE
)
237 self
.tree_view
.append_column(column
)
239 def button_press(tree_view
, bev
):
242 pos
= tree_view
.get_path_at_pos(int(bev
.x
), int(bev
.y
))
245 path
, col
, x
, y
= pos
246 obj
= self
.model
[path
][ITEM_OBJECT
]
247 if obj
and hasattr(obj
, 'menu_items'):
249 self
.tree_view
.connect('button-press-event', button_press
)
252 def motion(tree_view
, ev
):
253 if ev
.window
is not tree_view
.get_bin_window():
255 pos
= tree_view
.get_path_at_pos(int(ev
.x
), int(ev
.y
))
258 row
= self
.model
[path
]
262 tips
.prime(tree_view
, tip
)
268 self
.tree_view
.connect('motion-notify-event', motion
)
269 self
.tree_view
.connect('leave-notify-event', lambda tv
, ev
: tips
.hide())
273 self
.add_button(gtk
.STOCK_HELP
, gtk
.RESPONSE_HELP
)
274 self
.add_button(gtk
.STOCK_CLOSE
, gtk
.RESPONSE_OK
)
275 self
.add_button(gtk
.STOCK_DELETE
, DELETE
)
276 self
.set_default_response(gtk
.RESPONSE_OK
)
278 selection
= self
.tree_view
.get_selection()
279 def selection_changed(selection
):
281 for x
in get_selected_paths(self
.tree_view
):
282 obj
= self
.model
[x
][ITEM_OBJECT
]
283 if obj
is None or not obj
.may_delete
:
284 self
.set_response_sensitive(DELETE
, False)
287 self
.set_response_sensitive(DELETE
, any_selected
)
288 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
289 selection
.connect('changed', selection_changed
)
290 selection_changed(selection
)
292 def response(dialog
, resp
):
293 if resp
== gtk
.RESPONSE_OK
:
295 elif resp
== gtk
.RESPONSE_HELP
:
299 self
.connect('response', response
)
305 paths
= get_selected_paths(self
.tree_view
)
308 item
= model
[path
][ITEM_OBJECT
]
313 errors
.append(str(ex
))
315 model
.remove(model
.get_iter(path
))
319 alert(self
, "Failed to delete:\n%s" % '\n'.join(errors
))
321 def populate_model(self
):
322 # Find cached implementations
324 unowned
= {} # Impl ID -> Store
325 duplicates
= [] # TODO
327 for s
in iface_cache
.stores
.stores
:
328 if os
.path
.isdir(s
.dir):
329 for id in os
.listdir(s
.dir):
331 duplicates
.append(id)
335 error_interfaces
= []
337 # Look through cached interfaces for implementation owners
338 all
= iface_cache
.list_all_interfaces()
343 if uri
.startswith('/'):
346 cached_iface
= basedir
.load_first_cache(namespaces
.config_site
,
347 'interfaces', model
.escape(uri
))
348 user_overrides
= basedir
.load_first_config(namespaces
.config_site
,
349 namespaces
.config_prog
,
350 'user_overrides', model
.escape(uri
))
352 iface_size
= size_if_exists(cached_iface
) + size_if_exists(user_overrides
)
353 iface
= iface_cache
.get_interface(uri
)
354 except Exception, ex
:
355 error_interfaces
.append((uri
, str(ex
), iface_size
))
357 cached_iface
= ValidInterface(iface
, iface_size
)
358 for impl
in iface
.implementations
.values():
359 if impl
.id.startswith('/') or impl
.id.startswith('.'):
360 cached_iface
.in_cache
.append(LocalImplementation(impl
))
361 if impl
.id in unowned
:
362 cached_dir
= unowned
[impl
.id].dir
363 impl_path
= os
.path
.join(cached_dir
, impl
.id)
364 impl_size
= get_size(impl_path
)
365 cached_iface
.in_cache
.append(KnownImplementation(cached_iface
, cached_dir
, impl
, impl_size
))
367 cached_iface
.in_cache
.sort()
368 ok_interfaces
.append(cached_iface
)
371 iter = self
.model
.append(None, [_("Invalid interfaces (unreadable)"),
373 _("These interfaces exist in the cache but cannot be "
374 "read. You should probably delete them."),
376 for uri
, ex
, size
in error_interfaces
:
377 item
= InvalidInterface(uri
, ex
, size
)
378 item
.append_to(self
.model
, iter)
381 local_dir
= os
.path
.join(basedir
.xdg_cache_home
, '0install.net', 'implementations')
383 if unowned
[id].dir == local_dir
:
384 impl
= UnusedImplementation(local_dir
, id)
385 unowned_sizes
.append((impl
.size
, impl
))
387 iter = self
.model
.append(None, [_("Unowned implementations and temporary files"),
389 _("These probably aren't needed any longer. You can "
390 "delete them."), None])
392 unowned_sizes
.reverse()
393 for size
, item
in unowned_sizes
:
394 item
.append_to(self
.model
, iter)
397 iter = self
.model
.append(None,
400 _("Interfaces in the cache"),
402 for item
in ok_interfaces
:
403 item
.append_to(self
.model
, iter)
406 def update_sizes(self
):
407 """Set PRETTY_SIZE to the total size, including all children."""
410 total
= m
[itr
][SELF_SIZE
]
411 child
= m
.iter_children(itr
)
413 total
+= update(child
)
414 child
= m
.iter_next(child
)
415 m
[itr
][PRETTY_SIZE
] = support
.pretty_size(total
)
417 itr
= m
.get_iter_root()
420 itr
= m
.iter_next(itr
)
422 cache_help
= help_box
.HelpBox("Cache Explorer Help",
424 When you run a program using Zero Install, it downloads the program's 'interface' file, \
425 which gives information about which versions of the program are available. This interface \
426 file is stored in the cache to save downloading it next time you run the program.
428 When you have chosen which version (implementation) of the program you want to \
429 run, Zero Install downloads that version and stores it in the cache too. Zero Install lets \
430 you have many different versions of each program on your computer at once. This is useful, \
431 since it lets you use an old version if needed, and different programs may need to use \
432 different versions of libraries in some cases.
434 The cache viewer shows you all the interfaces and implementations in your cache. \
435 This is useful to find versions you don't need anymore, so that you can delete them and \
436 free up some disk space."""),
438 ('Invalid interfaces', """
439 The cache viewer gets a list of all interfaces in your cache. However, some may not \
440 be valid; they are shown in the 'Invalid interfaces' section. It should be fine to \
441 delete these. An invalid interface may be caused by a local interface that no longer \
442 exists, by a failed attempt to download an interface (the name ends in '.new'), or \
443 by the interface file format changing since the interface was downloaded."""),
445 ('Unowned implementations and temporary files', """
446 The cache viewer searches through all the interfaces to find out which implementations \
447 they use. If no interface uses an implementation, it is shown in the 'Unowned implementations' \
450 Unowned implementations can result from old versions of a program no longer being listed \
451 in the interface file. Temporary files are created when unpacking an implementation after \
452 downloading it. If the archive is corrupted, the unpacked files may be left there. Unless \
453 you are currently unpacking new programs, it should be fine to delete everything in this \
457 All remaining interfaces are listed in this section. You may wish to delete old versions of \
458 certain programs. Deleting a program which you may later want to run will require it to be downloaded \
459 again. Deleting a version of a program which is currently running may cause it to crash, so be careful!