2 # vim:fileencoding=utf8
3 # Note both python and vim understand the above encoding declaration
5 # Note using env above will cause the system to register
6 # the name (in ps etc.) as "python" rather than fslint-gui
7 # (because "python" is passed to exec by env).
10 FSlint - A utility to find File System lint.
11 Copyright © 2000-2006 by Pádraig Brady <P@draigBrady.com>.
13 This program is free software; you can redistribute it and/or modify
14 it under the terms of the GNU General Public License as published by
15 the Free Software Foundation; either version 2 of the License, or
18 This program is distributed in the hope that it will be useful,
19 but WITHOUT ANY WARRANTY; without even the implied warranty of
20 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
21 See the GNU General Public License for more details,
22 which is available at www.gnu.org
30 import os, sys, time, stat
32 # Maintain compatibility with Python 2.2 and below,
33 # while getting rid of warnings in newer pyGtks.
39 time_commands=False #print sub commands timing on status line
41 liblocation=os.path.dirname(os.path.abspath(sys.argv[0]))
42 locale_base=liblocation+'/po/locale'
45 if sys.argv[0][0] != '/':
46 #If relative probably debugging so don't use system files if possible
47 if not os.path.exists(liblocation+'/fslint'):
48 liblocation = fslint.liblocation
49 locale_base = None #sys default
51 liblocation = fslint.liblocation
52 locale_base = None #sys default
59 #gtk2 only understands utf-8 so convert to unicode
60 #which will be automatically converted to utf-8 by pygtk
61 gettext.install("fslint", locale_base, unicode=1)
62 global N_ #translate string but not at point of definition
63 def N_(string): return string
64 td=gettext.bindtextdomain("fslint",locale_base)
65 gettext.textdomain("fslint")
66 #This call should be redundant
67 #(done correctly in gettext) for python 2.3
69 gtk.glade.bindtextdomain("fslint",td) #note sets codeset to utf-8
71 #if no glade.bindtextdomain then we'll do without
75 locale.setlocale(locale.LC_ALL,'')
77 #gtk will print warning for a duff locale
79 #Note python 2.3 has the nicer locale.getpreferredencoding()
80 self.preferred_encoding=locale.nl_langinfo(locale.CODESET)
82 class unicode_displayable(unicode):
83 """translate non displayable chars to an appropriate representation"""
84 translate_table=range(0x2400,0x2420) #control chars
85 def displayable(self,exclude):
87 curr_table=self.__class__.translate_table[:] #copy
90 curr_table[ord(char)]=ord(char)
92 pass #won't be converted anyway
94 curr_table=self.__class__.translate_table
95 return self.translate(curr_table)
96 #Note in python 2.2 this will be a new style class as
97 #we are subclassing the builtin (immutable) unicode type.
98 #Therefore you can make a factory function with the following.
99 #I.E. you can do uc=self.unicode_displayable(uc) below
100 #(the __class__ is redundant in python 2.2 also).
101 #def __new__(cls,string):
102 # translated_string=unicode(string).translate(cls.translate_table)
103 # return unicode.__new__(cls, translated_string)
105 #The following is used for display strings only
106 def utf8(self,orig,exclude=""):
107 """If orig is not a valid utf8 string,
108 then try to convert to utf8 using preferred encoding while
109 also replacing undisplayable or invalid characters.
110 Exclude contains control chars you don't want to translate."""
112 if type(orig) == types.UnicodeType:
116 uc=unicode(orig,"utf-8")
119 # Note I don't use "replace" here as that
120 # replaces multiple bytes with a FFFD in utf8 locales
121 # This is silly as then you know it's not utf8 so
122 # one should try each character.
123 uc=unicode(orig,self.preferred_encoding)
125 uc=unicode(orig,"ascii","replace")
126 # Note I don't like the "replacement char" representation
127 # on fedora core 3 at least (bitstream-vera doesn't have it,
128 # and the nimbus fallback looks like a comma).
129 # It's even worse on redhat 9 where there is no
130 # representation at all. Note FreeSans does have a nice
131 # question mark representation, and dejavu also since 1.12.
132 # Alternatively I could: uc=unicode(orig,"latin-1") as that
133 # has a representation for each byte, but it's not general.
134 uc=self.__class__.unicode_displayable(uc).displayable(exclude)
135 return uc.encode("utf-8")
137 I18N=i18n() #call first so everything is internationalized
141 print _("Usage: %s [OPTION] [PATHS]") % os.path.split(sys.argv[0])[1]
142 print _(" --version display version")
143 print _(" --help display help")
146 lOpts, lArgs = getopt.getopt(sys.argv[1:], "", ["help","version"])
149 lArgs = [os.getcwd()]
151 if ("--help","") in lOpts:
155 if ("--version","") in lOpts:
158 except getopt.error, msg:
164 def human_num(num, divisor=1, power=""):
167 return locale.format("%ld",int(num),1)
168 elif divisor == 1000:
169 powers=[" ","K","M","G","T","P"]
170 elif divisor == 1024:
171 powers=[" ","Ki","Mi","Gi","Ti","Pi"]
173 raise ValueError, "Invalid divisor"
174 if not power: power=powers[0]
175 while num >= 1000: #4 digits
177 power=powers[powers.index(power)+1]
178 human_num(num,divisor,power)
180 return "%6.1f%s" % (num,power)
182 return "%4ld %s" % (num,power)
186 Superclass for glade based applications. Just derive from this
187 and your subclass should create methods whose names correspond to
188 the signal handlers defined in the glade file. Any other attributes
189 in your class will be safely ignored.
191 This class will give you the ability to do:
192 subclass_instance.GtkWindow.method(...)
193 subclass_instance.widget_name...
195 def __init__(self, Filename, WindowName):
197 self.widgets = gtk.glade.XML(Filename, WindowName, gettext.textdomain())
198 self.GtkWindow = getattr(self, WindowName)
200 instance_attributes = {}
201 for attribute in dir(self.__class__):
202 instance_attributes[attribute] = getattr(self, attribute)
203 self.widgets.signal_autoconnect(instance_attributes)
205 def __getattr__(self, attribute): #Called when no attribute in __dict__
206 widget = self.widgets.get_widget(attribute)
208 raise AttributeError("Widget [" + attribute + "] not found")
209 self.__dict__[attribute] = widget #add reference to cache
212 # SubProcess Example:
214 # process = subProcess("your shell command")
215 # process.read() #timeout is optional
216 # handle(process.outdata, process.errdata)
218 import time, os, select, signal
220 """Class representing a child process. It's like popen2.Popen3
221 but there are three main differences.
222 1. This makes the new child process group leader (using setpgrp())
223 so that all children can be killed.
224 2. The output function (read) is optionally non blocking returning in
225 specified timeout if nothing is read, or as close to specified
226 timeout as possible if data is read.
227 3. The output from both stdout & stderr is read (into outdata and
228 errdata). Reading from multiple outputs while not deadlocking
229 is not trivial and is often done in a non robust manner."""
231 def __init__(self, cmd, bufsize=8192):
232 """The parameter 'cmd' is the shell command to execute in a
233 sub-process. If the 'bufsize' parameter is specified, it
234 specifies the size of the I/O buffers from the child process."""
237 self.outr, self.outw = os.pipe()
238 self.errr, self.errw = os.pipe()
242 os.close(self.outw) #parent doesn't write so close
244 # Note we could use self.stdout=fdopen(self.outr) here
245 # to get a higher level file object like popen2.Popen3 uses.
246 # This would have the advantages of auto handling the BUFSIZ
247 # and closing the files when deleted. However it would mean
248 # that it would block waiting for a full BUFSIZ unless we explicitly
249 # set the files non blocking, and there would be extra uneeded
250 # overhead like EOL conversion. So I think it's handier to use os.read()
251 self.outdata = self.errdata = ''
252 self._outeof = self._erreof = 0
254 def _child(self, cmd):
255 # Note sh below doesn't setup a seperate group (job control)
256 # for non interactive shells (hmm maybe -m option does?)
257 os.setpgrp() #seperate group so we can kill it
258 os.dup2(self.outw,1) #stdout to write side of pipe
259 os.dup2(self.errw,2) #stderr to write side of pipe
260 #stdout & stderr connected to pipe, so close all other files
261 map(os.close,[self.outr,self.outw,self.errr,self.errw])
263 cmd = ['/bin/sh', '-c', cmd]
264 os.execvp(cmd[0], cmd)
265 finally: #exit child on error
268 def read(self, timeout=None):
269 """return 0 when finished
270 else return 1 every timeout seconds
271 data will be in outdata and errdata"""
274 tocheck=[self.outr]*(not self._outeof)+ \
275 [self.errr]*(not self._erreof)
276 ready = select.select(tocheck,[],[],timeout)
277 if len(ready[0]) == 0: #no data timeout
280 if self.outr in ready[0]:
281 outchunk = os.read(self.outr,self.BUFSIZ)
284 self.outdata += outchunk
285 if self.errr in ready[0]:
286 errchunk = os.read(self.errr,self.BUFSIZ)
289 self.errdata += errchunk
290 if self._outeof and self._erreof:
293 if (time.time()-currtime) > timeout:
294 return 1 #may be more data but time to go
297 os.kill(-self.pid, signal.SIGTERM) #kill whole group
300 """Wait for and return the exit status of the child process."""
304 pid, sts = os.waitpid(self.pid, 0)
313 # Determine what type of distro we're on.
316 self.rpm = self.deb = False
317 if os.path.exists("/etc/redhat-release"):
319 elif os.path.exists("/etc/debian_version"):
322 self.rpm = (os.system("rpm --version >/dev/null 2>&1") == 0)
324 self.deb = (os.system("dpkg --version >/dev/null 2>&1") == 0)
325 dist_type=distroType()
327 def human_space_left(where):
328 (device, total, used_b, avail, used_p, mount)=\
329 os.popen("df -h %s | tail -1"%where).read().split()
330 return _("%s of %s is used leaving %sB available") % (used_p, mount, avail)
332 class dlgUserInteraction(GladeWrapper):
334 Note input buttons tuple text should not be translated
335 so that the stock buttons are used if possible. But the
336 translations should be available, so use N_ for input
337 buttons tuple text. Note the returned response is not
340 def init(self, app, message, buttons=('Ok',)):
343 stock_text=getattr(gtk,"STOCK_"+text.upper())
344 button = gtk.Button(stock=stock_text)
346 button = gtk.Button(label=_(text))
347 button.set_data("text", text)
348 button.connect("clicked", self.button_clicked)
349 self.GtkWindow.action_area.pack_start(button)
351 button.set_flags(gtk.CAN_DEFAULT)
352 button.grab_default() #last button is default
354 self.lblmsg.set_text(message)
356 if message.endswith(":"):
363 self.GtkWindow.set_transient_for(self.app.GtkWindow)#center on main win
364 self.GtkWindow.show()
365 if self.GtkWindow.modal:
366 gtk.main() #synchronous call from parent
373 def quit(self, *args):
374 self.GtkWindow.hide()
375 if self.GtkWindow.modal:
378 def button_clicked(self, button):
379 self.input = self.entry.get_text()
380 self.response = button.get_data("text")
383 class dlgPathSel(GladeWrapper):
387 self.pwd = self.app.pwd
389 def show(self, fileOps=0):
391 self.fileOps = fileOps
392 self.GtkWindow.set_filename(self.pwd+'/')
394 self.GtkWindow.show_fileop_buttons()
396 self.GtkWindow.hide_fileop_buttons()
397 #self.GtkWindow.set_parent(self.app.GtkWindow)#error (on gtk-1.2.10-11?)
398 self.GtkWindow.set_transient_for(self.app.GtkWindow)#center on main win
399 self.GtkWindow.show()
400 if self.GtkWindow.modal:
401 gtk.main() #synchronous call from parent
408 def quit(self, *args):
409 self.GtkWindow.hide()
410 if not self.canceled:
411 self.pwd = self.GtkWindow.history_pulldown.get_children()[0].get()
412 if self.GtkWindow.modal:
414 return True #Don't let window be destroyed
416 def on_okdirs_clicked(self, *args):
417 if self.fileOps: #creating new item
418 file = self.GtkWindow.get_filename()
419 if os.path.exists(file):
420 if os.path.isfile(file):
421 msgbox=dlgUserInteraction(liblocation+"/fslint.glade",
424 _("Do you want to overwrite?\n") + file,
425 (N_('Yes'), N_('No')))
427 if msgbox.response != "Yes":
430 msgbox=dlgUserInteraction(liblocation+"/fslint.glade",
432 msgbox.init(self, _("You can't overwrite ") + file)
438 class fslint(GladeWrapper):
440 class UserAbort(Exception):
443 def __init__(self, Filename, WindowName):
444 os.close(0) #don't hang if any sub cmd tries to read from stdin
445 self.pwd=os.path.realpath(os.curdir)
446 GladeWrapper.__init__(self, Filename, WindowName)
447 self.dlgPathSel = dlgPathSel(liblocation+"/fslint.glade", "PathSel")
448 self.dlgPathSel.init(self)
449 #Just need to keep this tuple in sync with tabs
450 (self.mode_up, self.mode_pkgs, self.mode_nl, self.mode_sn,
451 self.mode_tf, self.mode_bl, self.mode_id, self.mode_ed,
452 self.mode_ns, self.mode_rs) = range(10)
454 self.mode_up:self.clist_dups,
455 self.mode_pkgs:self.clist_pkgs,
456 self.mode_nl:self.clist_nl,
457 self.mode_sn:self.clist_sn,
458 self.mode_tf:self.clist_tf,
459 self.mode_bl:self.clist_bl,
460 self.mode_id:self.clist_id,
461 self.mode_ed:self.clist_ed,
462 self.mode_ns:self.clist_ns,
463 self.mode_rs:self.clist_rs
466 self.mode_up:_("Files with the same content"),
467 self.mode_pkgs:_("Installed packages ordered by disk usage"),
468 self.mode_nl:_("Problematic filenames"),
469 self.mode_sn:_("Possibly conflicting commands or filenames"),
470 self.mode_tf:_("Possibly old temporary files"),
471 self.mode_bl:_("Problematic symbolic links"),
472 self.mode_id:_("Files with missing user IDs"),
473 self.mode_ed:_("Empty directories"),
474 self.mode_ns:_("Executables still containing debugging info"),
475 self.mode_rs:_("Whitespace at the end of lines in a text file")
478 if os.path.exists(path):
479 self.addDirs(self.ok_dirs, os.path.abspath(path))
481 self.ShowErrors(_("Invalid path [") + path + "]")
482 for bad_dir in ['/lost+found','/dev','/proc','/sys','/tmp',
484 self.addDirs(self.bad_dirs, bad_dir)
486 self.status.set_text(self.mode_descs[self.mode])
487 self.bg_colour=self.vpanes.get_style().bg[gtk.STATE_NORMAL]
488 #make readonly GtkEntry and GtkTextView widgets obviously so,
489 #by making them the same colour as the surrounding panes
490 self.errors.modify_base(gtk.STATE_NORMAL,self.bg_colour)
491 self.status.modify_base(gtk.STATE_NORMAL,self.bg_colour)
492 self.pkg_info.modify_base(gtk.STATE_NORMAL,self.bg_colour)
493 #Other GUI stuff that ideally [lib]glade should support
494 self.clist_pkgs.set_column_visibility(2,0)
495 self.clist_ns.set_column_justification(2,gtk.JUSTIFY_RIGHT)
497 def get_fslint(self, command, delim='\n'):
498 self.fslintproc = subProcess(command)
500 while self.fslintproc.read(timeout=0.1):
501 self.status.set_text(_("searching")+"."*(count%10))
503 while gtk.events_pending(): gtk.main_iteration(False)
505 self.fslintproc.kill()
508 self.fslintproc.outdata=self.fslintproc.outdata.split(delim)[:-1]
509 ret = (self.fslintproc.outdata, self.fslintproc.errdata)
511 self.status.set_text(_("processing..."))
512 while gtk.events_pending(): gtk.main_iteration(False)
518 def ShowErrors(self, lines):
521 end=self.errors.get_buffer().get_end_iter()
522 self.errors.get_buffer().insert(end,I18N.utf8(lines,"\n"))
524 def ClearErrors(self):
525 self.errors.get_buffer().set_text("")
527 def buildFindParameters(self):
528 if self.mode == self.mode_sn:
529 if self.chk_sn_path.get_active():
531 elif self.mode == self.mode_ns:
532 if self.chk_ns_path.get_active():
534 elif self.mode == self.mode_pkgs:
537 if self.ok_dirs.rows == 0:
538 #raise _("No search paths specified") #py 2.2.2 can't raise unicode
539 return _("No search paths specified")
542 for row in range(self.ok_dirs.rows):
543 search_dirs = search_dirs + \
544 " '" + self.ok_dirs.get_row_data(row) + "'"
547 for row in range(self.bad_dirs.rows):
548 if exclude_dirs == "":
549 exclude_dirs = '\(' + ' -path "'
551 exclude_dirs += ' -o -path "'
552 exclude_dirs += self.bad_dirs.get_row_data(row) + '"'
553 if exclude_dirs != "":
554 exclude_dirs += ' \) -prune -o '
556 if not self.recurseDirs.get_active():
561 self.findParams = search_dirs + " -f " + recurseParam + exclude_dirs
562 self.findParams += self.extra_find_params.get_text()
566 def addDirs(self, dirlist, dirs):
567 """Adds items to passed clist."""
568 dirlist.append([I18N.utf8(dirs)])
569 dirlist.set_row_data(dirlist.rows-1, dirs)
571 def removeDirs(self, dirlist):
572 """Removes selected items from passed clist.
573 If no items selected then list is cleared."""
574 paths_to_remove = dirlist.selection
575 if len(paths_to_remove) == 0:
578 paths_to_remove.sort() #selection order irrelevant/problematic
579 paths_to_remove.reverse() #so deleting works correctly
580 for row in paths_to_remove:
583 def clist_alloc_colour(self, clist, colour):
584 """cache colour allocation for clist
585 as colour will probably be used multiple times"""
586 cmap = clist.get_colormap()
587 cmap_cache=clist.get_data("cmap_cache")
588 if cmap_cache == None:
590 if not cmap_cache.has_key(colour):
591 cmap_cache[colour]=cmap.alloc_color(colour)
592 clist.set_data("cmap_cache",cmap_cache)
593 return cmap_cache[colour]
595 def clist_append_path(self, clist, path, colour, *rest):
596 """append path to clist, handling utf8 and colour issues"""
597 colour = self.clist_alloc_colour(clist, colour)
598 utf8_path=I18N.utf8(path)
599 (dir,file)=os.path.split(utf8_path)
600 clist.append((file,dir)+rest)
601 row_data = clist.get_data("row_data")
602 row_data.append(path+'\n') #add '\n' so can write with writelines
604 clist.set_foreground(clist.rows-1,colour)
606 def clist_append_group_row(self, clist, cols):
607 """append header to clist"""
609 row_data = clist.get_data("row_data")
610 row_data.append('#'+"\t".join(cols).rstrip()+'\n')
611 clist.set_background(clist.rows-1,self.bg_colour)
612 clist.set_selectable(clist.rows-1,0)
614 def get_path_info(self, path):
615 """Returns path info appropriate for display, i.e.
616 (color, size, ondisk_size, username, groupname, mtime_ls_date)"""
618 stat_val = os.lstat(path)
620 date = time.ctime(stat_val[stat.ST_MTIME])
621 month_time = date[4:16]
623 timediff = time.time()-stat_val[stat.ST_MTIME]
624 if timediff > 15552000: #6months
625 date = month_time[0:6] + year
629 mode = stat_val[stat.ST_MODE]
630 if stat.S_ISREG(mode):
632 if mode & (stat.S_IXGRP|stat.S_IXUSR|stat.S_IXOTH):
634 elif stat.S_ISDIR(mode):
636 elif stat.S_ISLNK(mode):
638 if not os.path.exists(path):
643 size=stat_val.st_blocks*512
645 return (colour, stat_val[stat.ST_SIZE], size, stat_val[stat.ST_UID],
646 stat_val[stat.ST_GID], date, stat_val[stat.ST_MTIME])
648 def whatRequires(self, packages, level=0):
649 if not packages: return
650 if level==0: self.checked_pkgs={}
651 for package in packages:
652 #print "\t"*level+package
653 self.checked_pkgs[package]=''
655 cmd = r"rpm -e --test " + ' '.join(packages) + r" 2>&1 | "
656 cmd += r"sed -n 's/.*is needed by (installed) \(.*\)/\1/p' | "
657 cmd += r"LANG=C sort | uniq"
659 cmd = r"dpkg --purge --dry-run " + ' '.join(packages) + r" 2>&1 | "
660 cmd += r"sed -n 's/ \(.*\) depends on.*/\1/p' | "
661 cmd += r"LANG=C sort | uniq"
663 raise "unknown distro"
664 process = os.popen(cmd)
665 requires = process.read()
667 new_packages = [p for p in requires.split()
668 if not self.checked_pkgs.has_key(p)]
669 self.whatRequires(new_packages, level+1)
670 if level==0: return self.checked_pkgs.keys()
672 def findpkgs(self, clist_pkgs):
673 self.clist_pkgs_order=[False,False] #unordered, descending
674 self.clist_pkgs_user_input=False
675 self.pkg_info.get_buffer().set_text("")
677 #Package names unique on debian
678 cmd=r"dpkg-query -W --showformat='${Package}\t${Installed-Size}"
679 cmd+=r"\t${Status}\n' | LANG=C grep -F 'installed' | cut -f1,2"
681 #Must include version names to uniquefy on redhat
682 cmd=r"rpm -q -a --queryformat '%{N}-%{V}-%{R}.%{ARCH}\t%{SIZE}\n'"
684 return ("", _("Sorry, FSlint does not support this functionality \
685 on your system at present."))
686 po, pe = self.get_fslint(cmd + " | LANG=C sort -k2,2rn")
690 pkg_name, pkg_size= pkg_info.split()
691 pkg_size = int(pkg_size)
693 pkg_size = pkg_size*1024
695 clist_pkgs.append([pkg_name,human_num(pkg_size,1000).strip(),
697 clist_pkgs.set_row_data(row, pkg_name)
700 return (str(row) + _(" packages, ") +
701 _("consuming %sB. ") % human_num(pkg_tot,1000).strip() +
702 _("Note %s.") % human_space_left('/'), pe)
703 #Note pkgs generally installed on root partition so report space left.
705 def findrs(self, clist_rs):
706 po, pe = self.get_fslint("./findrs " + self.findParams)
709 colour,fsize,size,uid,gid,date,mtime = self.get_path_info(line)
710 self.clist_append_path(clist_rs, line, colour, str(fsize), date)
713 return (str(row) + _(" files"), pe)
715 def findns(self, clist_ns):
717 if not self.chk_ns_path.get_active():
718 cmd += self.findParams
719 po, pe = self.get_fslint(cmd)
723 colour,fsize,size,uid,gid,date,mtime = self.get_path_info(line)
724 unstripped.append((fsize, line, date))
729 for fsize, path, date in unstripped:
730 self.clist_append_path(clist_ns, path, '', human_num(fsize), date)
733 return (str(row) + _(" unstripped binaries"), pe)
735 def finded(self, clist_ed):
736 po, pe = self.get_fslint("./finded " + self.findParams)
739 colour,fsize,size,uid,gid,date,mtime = self.get_path_info(line)
740 self.clist_append_path(clist_ed, line, '', date)
743 return (str(row) + _(" empty directories"), pe)
745 def findid(self, clist_id):
746 po, pe = self.get_fslint("./findid " + self.findParams, '\0')
749 colour,fsize,size,uid,gid,date,mtime = self.get_path_info(record)
750 self.clist_append_path(clist_id, record, colour,
751 str(uid), str(gid), str(fsize), date)
754 return (str(row) + _(" files"), pe)
756 def findbl(self, clist_bl):
757 cmd = "./findbl " + self.findParams
758 if self.opt_bl_dangling.get_active():
760 elif self.opt_bl_suspect.get_active():
762 elif self.opt_bl_relative.get_active():
764 elif self.opt_bl_absolute.get_active():
766 elif self.opt_bl_redundant.get_active():
768 po, pe = self.get_fslint(cmd)
771 link, target = line.split(" -> ", 2)
772 self.clist_append_path(clist_bl, link, '', target)
775 return (str(row) + _(" links"), pe)
777 def findtf(self, clist_tf):
778 cmd = "./findtf " + self.findParams
779 cmd += " --age=" + str(int(self.spin_tf_core.get_value()))
780 if self.chk_tf_core.get_active():
782 po, pe = self.get_fslint(cmd)
786 colour,fsize,size,uid,gid,date,mtime = self.get_path_info(line)
787 self.clist_append_path(clist_tf, line, '', str(fsize), date)
791 return (human_num(byteWaste) + _(" bytes wasted in ") +
792 str(row) + _(" files"), pe)
794 def findsn(self, clist_sn):
796 if self.chk_sn_path.get_active():
797 option = self.opt_sn_path.get_children()[0].get()
798 if option == _("Aliases"):
800 elif option == _("Conflicting files"):
803 raise "glade GtkOptionMenu item not found"
805 cmd += self.findParams
806 option = self.opt_sn_paths.get_children()[0].get()
807 if option == _("Aliases"):
809 elif option == _("Same names"):
811 elif option == _("Same names(ignore case)"):
813 elif option == _("Case conflicts"):
816 raise "glade GtkOptionMenu item not found"
818 po, pe = self.get_fslint(cmd,'\0')
825 self.clist_append_group_row(clist_sn, ['','',''])
828 colour,fsize,size,uid,gid,date,mtime=self.get_path_info(record)
829 self.clist_append_path(clist_sn, record, colour, str(fsize))
830 clist_sn.set_row_data(clist_sn.rows-1,mtime)
834 findsn_groups += 1 #for stripped group head above
836 return (str(findsn_number) + _(" files (in ") + str(findsn_groups) +
839 def findnl(self, clist_nl):
840 if self.chk_findu8.get_active():
841 po, pe = self.get_fslint("./findu8 " + self.findParams, '\0')
843 sensitivity = ('1','2','3','p')[
844 int(self.hscale_findnl_level.get_adjustment().value)-1]
845 po, pe = self.get_fslint("./findnl " + self.findParams + " -" +
850 colour,fsize,size,uid,gid,date,mtime = self.get_path_info(record)
851 self.clist_append_path(clist_nl, record, colour)
854 return (str(row) + _(" files"), pe)
856 def findup(self, clist_dups):
857 po, pe = self.get_fslint("./findup " + self.findParams + " --gui")
859 numdups = size = fsize = 0
862 #inodes required to correctly report disk usage of
863 #duplicate files with seperate inode groups.
866 if line == '': #grouped == 1
868 alldups.append(((numdups-1)*size, numdups, fsize, dups))
873 colour,fsize,size,uid,gid,date,mtime = self.get_path_info(line)
874 dups.append((line,date,mtime))
875 inode = os.lstat(line)[stat.ST_INO]
876 if not inodes.has_key(inode):
878 numdups = numdups + 1
881 alldups.append(((numdups-1)*size, numdups, fsize, dups))
889 for groupWaste, groupSize, FilesSize, files in alldups:
890 byteWaste += groupWaste
891 numWaste += groupSize - 1
892 groupHeader = ["%d x %s" % (groupSize, human_num(FilesSize)),
893 "=> %s" % human_num(groupWaste),
895 self.clist_append_group_row(clist_dups, groupHeader)
898 self.clist_append_path(clist_dups,file[0],'',file[1])
899 clist_dups.set_row_data(row,file[2]) #mtime
902 return (human_num(byteWaste) + _(" bytes wasted in ") +
903 str(numWaste) + _(" files (in ") + str(len(alldups)) +
906 find_dispatch = (findup,findpkgs,findnl,findsn,findtf,
907 findbl,findid,finded,findns,findrs) #order NB
909 def enable_stop(self):
912 self.stop.grab_focus()
913 self.stop.grab_add() #Just allow app to stop
915 def disable_stop(self):
916 self.stop.grab_remove()
919 self.find.grab_focus() #as it already would have been in focus
921 def check_user(self, question):
923 #remove as status from previous operation could be confusing
924 self.status.delete_text(0,-1)
925 clist = self.clists[self.mode]
927 return False #Be consistent
928 #All actions buttons do nothing if no results
929 paths = clist.selection
931 self.ShowErrors(_("None selected"))
934 msgbox = dlgUserInteraction(liblocation+"/fslint.glade",
937 question += _(" this item?\n")
939 question += _(" these %d items?\n") % len(paths)
940 msgbox.init(self, question, (N_('Yes'), N_('No')))
942 if msgbox.response != "Yes":
950 def on_fslint_destroy(self, event):
952 self.fslintproc.kill()
955 pass #fslint wasn't searching
958 def on_addOkDir_clicked(self, event):
959 self.dlgPathSel.show()
960 if not self.dlgPathSel.canceled:
961 path = self.dlgPathSel.GtkWindow.get_filename()
962 self.addDirs(self.ok_dirs, path)
964 def on_addBadDir_clicked(self, event):
965 self.dlgPathSel.show()
966 if not self.dlgPathSel.canceled:
967 path = self.dlgPathSel.GtkWindow.get_filename()
968 self.addDirs(self.bad_dirs, path)
970 def on_removeOkDir_clicked(self, event):
971 self.removeDirs(self.ok_dirs)
973 def on_removeBadDir_clicked(self, event):
974 self.removeDirs(self.bad_dirs)
976 def on_chk_sn_path_toggled(self, event):
977 if self.chk_sn_path.get_active():
978 self.hbox_sn_paths.hide()
979 self.hbox_sn_path.show()
981 self.hbox_sn_path.hide()
982 self.hbox_sn_paths.show()
984 def on_chk_findu8_toggled(self, event):
985 if self.chk_findu8.get_active():
986 self.hscale_findnl_level.hide()
987 self.lbl_findnl_sensitivity.hide()
989 self.hscale_findnl_level.show()
990 self.lbl_findnl_sensitivity.show()
992 def on_fslint_functions_switch_page(self, widget, dummy, pagenum):
995 self.status.set_text(self.mode_descs[self.mode])
996 if self.mode == self.mode_up:
997 self.autoMerge.show()
999 self.autoMerge.hide()
1000 if self.mode == self.mode_ns or self.mode == self.mode_rs: #bl in future
1001 self.autoClean.show()
1003 self.autoClean.hide()
1005 def on_selection_menu_button_press_event(self, widget, event):
1006 if self.mode == self.mode_up or self.mode == self.mode_sn:
1007 self.groups_menu.show()
1009 self.groups_menu.hide()
1010 if event == None: #keyboard click
1011 self.selection_menu_menu.popup(None,None,None,3,0) #at mouse pointer
1012 elif type(widget) == gtk.Button or \
1013 (type(widget) == gtk.CList and event.button == 3):
1014 self.selection_menu_menu.popup(None,None,None,event.button,0)
1015 if type(widget) == gtk.Button:
1017 return True #Don't fire click event
1019 def on_find_clicked(self, event):
1024 clist = self.clists[self.mode]
1025 os.chdir(liblocation+"/fslint/")
1026 errors = self.buildFindParameters()
1028 self.ShowErrors(errors)
1030 self.status.delete_text(0,-1)
1033 #All GtkClist operations seem to be O(n),
1034 #so doing the following for example will be O((n/2)*(n+1))
1035 # for row in range(clist.rows):
1036 # path = clist.get_row_data(row)
1037 #Therefore we use a python list to store row data.
1038 clist.set_data("row_data",[])
1039 if self.mode == self.mode_pkgs:
1040 self.pkg_info.get_buffer().set_text("")
1043 while gtk.events_pending(): gtk.main_iteration(False)#update GUI
1046 status, errors=self.__class__.find_dispatch[self.mode](self, clist)
1049 status += ". Found in %.3fs" % (tend-tstart)
1050 except self.UserAbort:
1051 status=_("User aborted")
1053 etype, emsg, etb = sys.exc_info()
1054 errors=str(etype)+': '+str(emsg)+'\n'
1055 clist.columns_autosize()
1058 self.ShowErrors(errors)
1059 self.status.set_text(status)
1062 def on_stop_clicked(self, event):
1066 def on_stop_keypress(self, button, event):
1067 #ingore capslock and numlock
1068 state = event.state & ~(gtk.gdk.LOCK_MASK | gtk.gdk.MOD2_MASK)
1071 int(gtk.gdk.CONTROL_MASK):[ksyms.c,ksyms.C],
1072 0 :[ksyms.space,ksyms.Escape,ksyms.Return]
1075 if event.keyval in abort_keys[state]:
1076 self.on_stop_clicked(event)
1081 def on_saveAs_clicked(self, event):
1082 clist = self.clists[self.mode]
1085 self.dlgPathSel.show(1)
1086 if not self.dlgPathSel.canceled:
1088 fileSaveAs = self.dlgPathSel.GtkWindow.get_filename()
1090 fileSaveAs = open(fileSaveAs, 'w')
1092 etype, emsg, etb = sys.exc_info()
1093 self.ShowErrors(str(emsg)+'\n')
1095 rows_to_save = clist.selection
1096 if self.mode == self.mode_pkgs:
1097 for row in range(clist.rows):
1098 if len(rows_to_save):
1099 if row not in rows_to_save: continue
1101 for col in (0,2): #ignore "human number" col
1102 rowtext += clist.get_text(row,col) +'\t'
1103 fileSaveAs.write(rowtext[:-1]+'\n')
1105 row_data=clist.get_data("row_data")
1106 if len(rows_to_save):
1107 for row in range(clist.rows):
1108 if clist.get_selectable(row):
1109 if row not in rows_to_save: continue
1110 else: continue #don't save group headers
1111 fileSaveAs.write(row_data[row])
1113 fileSaveAs.writelines(row_data)
1115 def on_unselect_using_wildcard_activate(self, event):
1116 self.select_using_wildcard(False)
1117 def on_select_using_wildcard_activate(self, event):
1118 self.select_using_wildcard(True)
1119 def select_using_wildcard(self, select):
1120 msgbox=dlgUserInteraction(liblocation+"/fslint.glade",
1122 msgbox.init(self, _("wildcard:"), (N_('Cancel'), N_('Ok')))
1124 if msgbox.response != "Ok":
1126 wildcard=msgbox.input
1128 clist = self.clists[self.mode]
1131 select_func=select and clist.select_row or clist.unselect_row
1133 if self.mode == self.mode_pkgs:
1134 for row in range(clist.rows):
1135 pkgname = clist.get_text(row,0)
1136 if fnmatch.fnmatch(pkgname,wildcard):
1140 for path in clist.get_data("row_data"):
1141 if fnmatch.fnmatch(path,wildcard): #trailing \n ignored
1145 def on_select_all_but_first_in_each_group_activate(self, event):
1146 self.on_select_all_but_one_in_each_group_activate("first")
1147 def on_select_all_but_newest_in_each_group_activate(self, event):
1148 self.on_select_all_but_one_in_each_group_activate("newest")
1149 def on_select_all_but_oldest_in_each_group_activate(self, event):
1150 self.on_select_all_but_one_in_each_group_activate("oldest")
1152 def on_select_all_but_one_in_each_group_activate(self, which):
1154 def find_row_to_unselect(clist, row, which):
1156 if which == "first":
1157 if clist.get_selectable(row)==False:
1160 return row #for first row in clist_sn
1161 elif which == "newest":
1164 elif which == "oldest":
1165 unselect_mtime=2**32
1167 if clist.get_selectable(row)==False: #not the case for first sn row
1169 while clist.get_selectable(row) == True and row < clist.rows:
1170 mtime = clist.get_row_data(row)
1171 if comp(mtime,unselect_mtime):
1172 unselect_mtime = mtime
1177 clist = self.clists[self.mode]
1180 for row in range(clist.rows):
1181 if row==0 or clist.get_selectable(row)==False: #New group
1182 unselect_row = find_row_to_unselect(clist, row, which)
1183 clist.unselect_row(unselect_row, 0)
1186 def on_unselect_all_activate(self, event):
1187 clist = self.clists[self.mode]
1188 clist.unselect_all()
1190 def on_toggle_selection_activate(self, event):
1191 clist = self.clists[self.mode]
1193 selected = clist.selection
1194 if len(selected) == 0:
1196 elif len(selected) == clist.rows:
1197 clist.unselect_all()
1200 for row in selected:
1201 clist.unselect_row(row, 0)
1204 def on_selection_clicked(self, widget):
1205 self.on_selection_menu_button_press_event(self.selection, None)
1207 def on_delSelected_clicked(self, event):
1208 if self.mode == self.mode_pkgs:
1209 if os.geteuid() != 0:
1210 msgbox=dlgUserInteraction(liblocation+"/fslint.glade",
1214 _("Sorry, you must be root to delete system packages.")
1218 if not self.check_user(_("Are you sure you want to delete")):
1220 clist = self.clists[self.mode]
1221 row_data = clist.get_data("row_data")
1222 paths_to_remove = clist.selection
1223 paths_to_remove.sort() #selection order irrelevant/problematic
1224 paths_to_remove.reverse() #so deleting works correctly
1226 if self.mode == self.mode_pkgs:
1228 for row in paths_to_remove:
1229 package = clist.get_row_data(row)
1230 pkgs_selected.append(package)
1231 self.status.set_text(_("Calculating dependencies..."))
1232 while gtk.events_pending(): gtk.main_iteration(False)
1233 all_deps=self.whatRequires(pkgs_selected)
1234 if len(all_deps) > len(pkgs_selected):
1236 for package in all_deps:
1237 if package not in pkgs_selected:
1239 #Note clist.find_row_from_data() only compares pointers
1240 for row in range(clist.rows):
1241 if package == clist.get_row_data(row):
1242 clist.select_row(row,0)
1243 msgbox=dlgUserInteraction(liblocation+"/fslint.glade",
1247 _("%d extra packages need to be deleted.\n") % num_new_pkgs +
1248 _("Please review the updated selection.")
1251 #Note this is not ideal as it's difficult for users
1252 #to get info on selected packages (Ctrl click twice).
1253 #Should really have a seperate marked_for_deletion column.
1254 self.status.set_text("")
1257 self.status.set_text(_("Removing packages..."))
1258 while gtk.events_pending(): gtk.main_iteration(False)
1263 cmd+=' '.join(pkgs_selected) + " >/dev/null 2>&1"
1267 for row in paths_to_remove:
1268 if self.mode != self.mode_pkgs:
1270 path=row_data[row][:-1] #strip trailing '\n'
1271 if os.path.isdir(path):
1276 etype, emsg, etb = sys.exc_info()
1277 self.ShowErrors(str(emsg)+'\n')
1283 #Remove any redundant grouping rows
1285 rows_left = range(rowver)
1287 for row in rows_left:
1288 if clist.get_selectable(row) == False:
1289 if (row == clist.rows-1) or (clist.get_selectable(row+1) ==
1292 if self.mode != self.mode_pkgs:
1295 clist.columns_autosize()
1297 status = str(numDeleted) + _(" items deleted")
1298 if self.mode == self.mode_pkgs:
1299 status += ". " + human_space_left('/')
1300 self.status.set_text(status)
1302 def on_clist_pkgs_click_column(self, clist, col):
1303 self.clist_pkgs_order[col] = not self.clist_pkgs_order[col]
1304 if self.clist_pkgs_order[col]:
1305 clist.set_sort_type(gtk.SORT_ASCENDING)
1307 clist.set_sort_type(gtk.SORT_DESCENDING)
1309 #focus_row is not updated after ordering, so can't use
1310 #last_selected=clist.get_row_data(clist.focus_row)
1311 last_selected=self.clist_pkgs_last_selected
1315 clist.set_sort_column(0)
1317 clist.set_sort_column(2)
1319 #could instead just use our "row_data" and
1320 #repopulate the gtkclist with something like:
1321 #row_data = clist.get_data("row_data")
1322 #row_data.sort(key = lambda x:x[col],
1323 # reverse = not self.clist_pkgs_order[col])
1325 #Warning! As of pygtk2-2.4.0 at least
1326 #find_row_from_data only compares pointers, not strings!
1327 last_selected_row=clist.find_row_from_data(last_selected)
1328 clist.moveto(last_selected_row,0,0.5,0.0)
1329 #clist.set_focus_row(last_selected_row)
1331 clist.moveto(0,0,0.0,0.0)
1334 #Note the events and event ordering provided by the GtkClist
1335 #make it very hard to provide robust and performant handlers
1336 #for selected rows. These flags are an attempt to stop the
1337 #function below from being called too frequently.
1338 #Note most users will scoll the list with {Up,Down} rather
1339 #than Ctrl+{Up,Down}, but at worst this will result in slow scrolling.
1340 def on_clist_pkgs_button_press(self, list, event):
1341 if event.button == 3:
1342 self.on_selection_menu_button_press_event(list, event)
1344 self.clist_pkgs_user_input=True
1345 def on_clist_pkgs_key_press(self, *args):
1346 self.clist_pkgs_user_input=True
1348 def on_clist_pkgs_selection_changed(self, clist, *args):
1349 self.pkg_info.get_buffer().set_text("")
1350 if not self.clist_pkgs_user_input:
1351 self.clist_pkgs_last_selected=False
1353 self.clist_pkgs_user_input=False
1354 if len(clist.selection) == 0:
1356 pkg=clist.get_row_data(clist.selection[-1])
1357 self.clist_pkgs_last_selected=pkg #for ordering later
1359 cmd = "rpm -q --queryformat '%{DESCRIPTION}' "
1361 cmd = "dpkg-query -W --showformat='${Description}' "
1363 pkg_process = subProcess(cmd)
1365 lines=pkg_process.outdata
1366 self.pkg_info.get_buffer().set_text(I18N.utf8(lines,"\n"))
1369 def on_autoMerge_clicked(self, event):
1371 self.status.delete_text(0,-1)
1372 clist = self.clists[self.mode]
1376 question=_("Are you sure you want to merge ALL files?\n")
1378 paths_to_leave = clist.selection
1379 if len(paths_to_leave):
1380 question+=_("(Ignoring those selected)\n")
1382 msgbox = dlgUserInteraction(liblocation+"/fslint.glade",
1384 msgbox.init(self, question, (N_('Yes'), N_('No')))
1386 if msgbox.response != "Yes":
1390 row_data=clist.get_data("row_data")
1391 for row in range(clist.rows):
1392 if row in paths_to_leave:
1394 if clist.get_selectable(row) == False: #new group
1397 path = row_data[row][:-1] #strip '\n'
1405 try: #Don't do this for autodelete
1406 os.link(keepfile,dupfile)
1407 except OSError, value:
1408 if value.errno == 18: #EXDEV
1409 os.symlink(os.path.realpath(keepfile),dupfile)
1412 clist.set_background(row, self.bg_colour)
1414 self.ShowErrors(str(sys.exc_value)+'\n')
1416 def on_autoClean_clicked(self, event):
1417 if not self.check_user(_("Are you sure you want to clean")):
1419 clist = self.clists[self.mode]
1420 paths_to_clean = clist.selection
1424 row_data=clist.get_data("row_data")
1425 for row in paths_to_clean:
1427 path_to_clean = row_data[row][:-1] #strip '\n'
1428 startlen = os.stat(path_to_clean)[stat.ST_SIZE]
1429 if self.mode == self.mode_ns:
1430 stripProcess = subProcess("strip "+path_to_clean)
1432 errors = stripProcess.errdata
1435 raise '', errors[:-1]
1436 elif self.mode == self.mode_rs:
1437 file_to_clean = open(path_to_clean,'r')
1438 lines = open(path_to_clean,'r').readlines()
1439 file_to_clean.close()
1440 file_to_clean = open(path_to_clean,'w')
1441 lines = map(lambda s: s.rstrip()+'\n', lines)
1442 file_to_clean.writelines(lines)
1443 file_to_clean.close()
1444 clist.set_background(row, self.bg_colour)
1445 clist.unselect_row(row,0)
1446 totalSaved += startlen - os.stat(path_to_clean)[stat.ST_SIZE]
1449 etype, emsg, etb = sys.exc_info()
1450 self.ShowErrors(str(emsg)+'\n')
1451 status = str(numCleaned) + _(" items cleaned (") + \
1452 human_num(totalSaved) + _(" bytes saved)")
1453 self.status.set_text(status)
1455 FSlint = fslint(liblocation+"/fslint.glade", "fslint")