5 from dialog
import Dialog
, alert
6 from zeroinstall
.injector
.iface_cache
import iface_cache
7 from zeroinstall
.injector
import namespaces
, model
8 from zeroinstall
.zerostore
import BadDigest
, manifest
9 from zeroinstall
import support
10 from zeroinstall
.support
import basedir
11 from treetips
import TreeTips
13 ROX_IFACE
= 'http://rox.sourceforge.net/2005/interfaces/ROX-Filer'
22 def popup_menu(bev
, obj
):
24 for i
in obj
.menu_items
:
26 item
= gtk
.SeparatorMenuItem()
29 item
= gtk
.MenuItem(name
)
30 item
.connect('activate', lambda item
, cb
=cb
: cb(obj
))
33 menu
.popup(None, None, None, bev
.button
, bev
.time
)
35 def size_if_exists(path
):
36 "Get the size for a file, or 0 if it doesn't exist."
37 if path
and os
.path
.isfile(path
):
38 return os
.path
.getsize(path
)
42 "Get the size for a directory tree. Get the size from the .manifest if possible."
43 man
= os
.path
.join(path
, '.manifest')
44 if os
.path
.exists(man
):
45 size
= os
.path
.getsize(man
)
46 for line
in file(man
):
48 size
+= long(line
.split(' ', 4)[3])
51 for root
, dirs
, files
in os
.walk(path
):
53 size
+= os
.path
.getsize(os
.path
.join(root
, name
))
58 return iface
.get_name() + ' - ' + iface
.summary
59 return iface
.get_name()
61 def get_selected_paths(tree_view
):
62 "GTK 2.0 doesn't have this built-in"
63 selection
= tree_view
.get_selection()
65 def add(model
, path
, iter):
67 selection
.selected_foreach(add
)
75 class CachedInterface(object):
76 def __init__(self
, uri
, size
):
81 if not self
.uri
.startswith('/'):
82 cached_iface
= basedir
.load_first_cache(namespaces
.config_site
,
83 'interfaces', model
.escape(self
.uri
))
85 #print "Delete", cached_iface
86 os
.unlink(cached_iface
)
87 user_overrides
= basedir
.load_first_config(namespaces
.config_site
,
88 namespaces
.config_prog
,
89 'user_overrides', model
.escape(self
.uri
))
91 #print "Delete", user_overrides
92 os
.unlink(user_overrides
)
94 def __cmp__(self
, other
):
95 return self
.uri
.__cmp
__(other
.uri
)
97 class ValidInterface(CachedInterface
):
98 def __init__(self
, iface
, size
):
99 CachedInterface
.__init
__(self
, iface
.uri
, size
)
103 def append_to(self
, model
, iter):
104 iter2
= model
.append(iter,
105 [self
.uri
, self
.size
, None, summary(self
.iface
), self
])
106 for cached_impl
in self
.in_cache
:
107 cached_impl
.append_to(model
, iter2
)
109 def get_may_delete(self
):
110 for c
in self
.in_cache
:
111 if not isinstance(c
, LocalImplementation
):
112 return False # Still some impls cached
115 may_delete
= property(get_may_delete
)
117 class InvalidInterface(CachedInterface
):
120 def __init__(self
, uri
, ex
, size
):
121 CachedInterface
.__init
__(self
, uri
, size
)
124 def append_to(self
, model
, iter):
125 model
.append(iter, [self
.uri
, self
.size
, None, self
.ex
, self
])
127 class LocalImplementation
:
130 def __init__(self
, impl
):
133 def append_to(self
, model
, iter):
134 model
.append(iter, [self
.impl
.id, 0, None, 'This is a local version, not held in the cache.', self
])
136 class CachedImplementation
:
139 def __init__(self
, cache_dir
, name
):
140 self
.impl_path
= os
.path
.join(cache_dir
, name
)
141 self
.size
= get_size(self
.impl_path
)
145 #print "Delete", self.impl_path
146 support
.ro_rmtree(self
.impl_path
)
149 os
.spawnlp(os
.P_WAIT
, '0launch', '0launch', ROX_IFACE
, '-d', self
.impl_path
)
153 manifest
.verify(self
.impl_path
)
154 except BadDigest
, ex
:
155 box
= gtk
.MessageDialog(None, 0,
156 gtk
.MESSAGE_WARNING
, gtk
.BUTTONS_OK
, str(ex
))
158 swin
= gtk
.ScrolledWindow()
159 buffer = gtk
.TextBuffer()
160 mono
= buffer.create_tag('mono', family
= 'Monospace')
161 buffer.insert_with_tags(buffer.get_start_iter(), ex
.detail
, mono
)
162 text
= gtk
.TextView(buffer)
163 text
.set_editable(False)
164 text
.set_cursor_visible(False)
166 swin
.set_shadow_type(gtk
.SHADOW_IN
)
167 swin
.set_border_width(4)
168 box
.vbox
.pack_start(swin
)
170 box
.set_resizable(True)
172 box
= gtk
.MessageDialog(None, 0,
173 gtk
.MESSAGE_INFO
, gtk
.BUTTONS_OK
,
174 'Contents match digest; nothing has been changed.')
178 menu_items
= [('Open in ROX-Filer', open_rox
),
179 ('Verify integrity', verify
)]
181 class UnusedImplementation(CachedImplementation
):
182 def append_to(self
, model
, iter):
183 model
.append(iter, [self
.name
, self
.size
, None, self
.impl_path
, self
])
185 class KnownImplementation(CachedImplementation
):
186 def __init__(self
, cached_iface
, cache_dir
, impl
, impl_size
):
187 CachedImplementation
.__init
__(self
, cache_dir
, impl
.id)
188 self
.cached_iface
= cached_iface
190 self
.size
= impl_size
193 CachedImplementation
.delete(self
)
194 self
.cached_iface
.in_cache
.remove(self
)
196 def append_to(self
, model
, iter):
198 ['Version %s : %s' % (self
.impl
.get_version(), self
.impl
.id),
203 def __cmp__(self
, other
):
204 if hasattr(other
, 'impl'):
205 return self
.impl
.__cmp
__(other
.impl
)
208 class CacheExplorer(Dialog
):
210 Dialog
.__init
__(self
)
211 self
.set_title('Zero Install Cache')
212 self
.set_default_size(gtk
.gdk
.screen_width() / 2, gtk
.gdk
.screen_height() / 2)
215 self
.model
= gtk
.TreeStore(str, int, str, str, object)
216 self
.tree_view
= gtk
.TreeView(self
.model
)
219 swin
= gtk
.ScrolledWindow()
220 swin
.set_policy(gtk
.POLICY_AUTOMATIC
, gtk
.POLICY_ALWAYS
)
221 swin
.set_shadow_type(gtk
.SHADOW_IN
)
222 swin
.add(self
.tree_view
)
223 self
.vbox
.pack_start(swin
, True, True, 0)
224 self
.tree_view
.set_rules_hint(True)
227 column
= gtk
.TreeViewColumn('Item', gtk
.CellRendererText(), text
= ITEM
)
228 column
.set_resizable(True)
229 self
.tree_view
.append_column(column
)
231 cell
= gtk
.CellRendererText()
232 cell
.set_property('xalign', 1.0)
233 column
= gtk
.TreeViewColumn('Size', cell
, text
= PRETTY_SIZE
)
234 self
.tree_view
.append_column(column
)
236 def button_press(tree_view
, bev
):
239 pos
= tree_view
.get_path_at_pos(int(bev
.x
), int(bev
.y
))
242 path
, col
, x
, y
= pos
243 obj
= self
.model
[path
][ITEM_OBJECT
]
244 if obj
and hasattr(obj
, 'menu_items'):
246 self
.tree_view
.connect('button-press-event', button_press
)
249 def motion(tree_view
, ev
):
250 if ev
.window
is not tree_view
.get_bin_window():
252 pos
= tree_view
.get_path_at_pos(int(ev
.x
), int(ev
.y
))
255 row
= self
.model
[path
]
259 tips
.prime(tree_view
, tip
)
265 self
.tree_view
.connect('motion-notify-event', motion
)
266 self
.tree_view
.connect('leave-notify-event', lambda tv
, ev
: tips
.hide())
270 self
.add_button(gtk
.STOCK_HELP
, gtk
.RESPONSE_HELP
)
271 self
.add_button(gtk
.STOCK_CLOSE
, gtk
.RESPONSE_OK
)
272 self
.add_button(gtk
.STOCK_DELETE
, DELETE
)
273 self
.set_default_response(gtk
.RESPONSE_OK
)
275 selection
= self
.tree_view
.get_selection()
276 def selection_changed(selection
):
278 for x
in get_selected_paths(self
.tree_view
):
279 obj
= self
.model
[x
][ITEM_OBJECT
]
280 if obj
is None or not obj
.may_delete
:
281 self
.set_response_sensitive(DELETE
, False)
284 self
.set_response_sensitive(DELETE
, any_selected
)
285 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
286 selection
.connect('changed', selection_changed
)
287 selection_changed(selection
)
289 def response(dialog
, resp
):
290 if resp
== gtk
.RESPONSE_OK
:
292 elif resp
== gtk
.RESPONSE_HELP
:
296 self
.connect('response', response
)
302 paths
= get_selected_paths(self
.tree_view
)
305 item
= model
[path
][ITEM_OBJECT
]
310 errors
.append(str(ex
))
312 model
.remove(model
.get_iter(path
))
316 alert(self
, "Failed to delete:\n%s" % '\n'.join(errors
))
318 def populate_model(self
):
319 # Find cached implementations
321 unowned
= {} # Impl ID -> Store
322 duplicates
= [] # TODO
324 for s
in iface_cache
.stores
.stores
:
325 if os
.path
.isdir(s
.dir):
326 for id in os
.listdir(s
.dir):
328 duplicates
.append(id)
332 error_interfaces
= []
334 # Look through cached interfaces for implementation owners
335 all
= iface_cache
.list_all_interfaces()
340 if uri
.startswith('/'):
343 cached_iface
= basedir
.load_first_cache(namespaces
.config_site
,
344 'interfaces', model
.escape(uri
))
345 user_overrides
= basedir
.load_first_config(namespaces
.config_site
,
346 namespaces
.config_prog
,
347 'user_overrides', model
.escape(uri
))
349 iface_size
= size_if_exists(cached_iface
) + size_if_exists(user_overrides
)
350 iface
= iface_cache
.get_interface(uri
)
351 except Exception, ex
:
352 error_interfaces
.append((uri
, str(ex
), iface_size
))
354 cached_iface
= ValidInterface(iface
, iface_size
)
355 for impl
in iface
.implementations
.values():
356 if impl
.id.startswith('/') or impl
.id.startswith('.'):
357 cached_iface
.in_cache
.append(LocalImplementation(impl
))
358 if impl
.id in unowned
:
359 cached_dir
= unowned
[impl
.id].dir
360 impl_path
= os
.path
.join(cached_dir
, impl
.id)
361 impl_size
= get_size(impl_path
)
362 cached_iface
.in_cache
.append(KnownImplementation(cached_iface
, cached_dir
, impl
, impl_size
))
364 cached_iface
.in_cache
.sort()
365 ok_interfaces
.append(cached_iface
)
368 iter = self
.model
.append(None, [_("Invalid interfaces (unreadable)"),
370 _("These interfaces exist in the cache but cannot be "
371 "read. You should probably delete them."),
373 for uri
, ex
, size
in error_interfaces
:
374 item
= InvalidInterface(uri
, ex
, size
)
375 item
.append_to(self
.model
, iter)
378 local_dir
= os
.path
.join(basedir
.xdg_cache_home
, '0install.net', 'implementations')
380 if unowned
[id].dir == local_dir
:
381 impl
= UnusedImplementation(local_dir
, id)
382 unowned_sizes
.append((impl
.size
, impl
))
384 iter = self
.model
.append(None, [_("Unowned implementations and temporary files"),
386 _("These probably aren't needed any longer. You can "
387 "delete them."), None])
389 unowned_sizes
.reverse()
390 for size
, item
in unowned_sizes
:
391 item
.append_to(self
.model
, iter)
394 iter = self
.model
.append(None,
397 _("Interfaces in the cache"),
399 for item
in ok_interfaces
:
400 item
.append_to(self
.model
, iter)
403 def update_sizes(self
):
404 """Set PRETTY_SIZE to the total size, including all children."""
407 total
= m
[itr
][SELF_SIZE
]
408 child
= m
.iter_children(itr
)
410 total
+= update(child
)
411 child
= m
.iter_next(child
)
412 m
[itr
][PRETTY_SIZE
] = support
.pretty_size(total
)
414 itr
= m
.get_iter_root()
417 itr
= m
.iter_next(itr
)
419 cache_help
= help_box
.HelpBox("Cache Explorer Help",
421 When you run a program using Zero Install, it downloads the program's 'interface' file, \
422 which gives information about which versions of the program are available. This interface \
423 file is stored in the cache to save downloading it next time you run the program.
425 When you have chosen which version (implementation) of the program you want to \
426 run, Zero Install downloads that version and stores it in the cache too. Zero Install lets \
427 you have many different versions of each program on your computer at once. This is useful, \
428 since it lets you use an old version if needed, and different programs may need to use \
429 different versions of libraries in some cases.
431 The cache viewer shows you all the interfaces and implementations in your cache. \
432 This is useful to find versions you don't need anymore, so that you can delete them and \
433 free up some disk space."""),
435 ('Invalid interfaces', """
436 The cache viewer gets a list of all interfaces in your cache. However, some may not \
437 be valid; they are shown in the 'Invalid interfaces' section. It should be fine to \
438 delete these. An invalid interface may be caused by a local interface that no longer \
439 exists, by a failed attempt to download an interface (the name ends in '.new'), or \
440 by the interface file format changing since the interface was downloaded."""),
442 ('Unowned implementations and temporary files', """
443 The cache viewer searches through all the interfaces to find out which implementations \
444 they use. If no interface uses an implementation, it is shown in the 'Unowned implementations' \
447 Unowned implementations can result from old versions of a program no longer being listed \
448 in the interface file. Temporary files are created when unpacking an implementation after \
449 downloading it. If the archive is corrupted, the unpacked files may be left there. Unless \
450 you are currently unpacking new programs, it should be fine to delete everything in this \
454 All remaining interfaces are listed in this section. You may wish to delete old versions of \
455 certain programs. Deleting a program which you may later want to run will require it to be downloaded \
456 again. Deleting a version of a program which is currently running may cause it to crash, so be careful!