summaryrefslogtreecommitdiffstats
path: root/philesight
diff options
context:
space:
mode:
authorFlorian Pritz <f-p@gmx.at>2009-02-28 14:36:14 +0100
committerFlorian Pritz <f-p@gmx.at>2009-02-28 14:36:14 +0100
commit3232f9d90114778cb8d38cc1bc8477435dc65259 (patch)
tree0eb9ffafbe853c84b9ad6b808d86a00d44668392 /philesight
downloadaur-packages-3232f9d90114778cb8d38cc1bc8477435dc65259.tar.gz
aur-packages-3232f9d90114778cb8d38cc1bc8477435dc65259.tar.xz
initial commit
Diffstat (limited to 'philesight')
-rw-r--r--philesight/README100
-rwxr-xr-xphilesight/philesight62
-rwxr-xr-xphilesight/philesight.cgi127
-rw-r--r--philesight/philesight.rb458
4 files changed, 747 insertions, 0 deletions
diff --git a/philesight/README b/philesight/README
new file mode 100644
index 0000000..1a9905e
--- /dev/null
+++ b/philesight/README
@@ -0,0 +1,100 @@
+
+Summary
+=======
+
+Philesight is a tool to browse your filesystem and see where the diskspace is
+being used at a glance. Philesight is implemented as a simple command line
+program that generates PNG files; a wrapper CGI script is supplied to allow
+navigating through the filesystem.
+
+Philesight is actually a clone of the filelight program. Wheres filelight is
+ment as an interactive, user friendly application for the X-windows desktop,
+philesight is designed to run on a remote server without graphical user
+interface.
+
+
+Dependencies
+============
+
+Philesight is written in ruby1.8, and requires the berkely-db4 and cairo
+ruby-libraries.
+
+Changelog
+=========
+
+2008-05-29 Added option (use_gradients) to enable/disable gradients
+ in circular graph. Added option to set graph size.
+
+2008-04-16 Added optional table with file list to cgi, some bugfixes,
+ increased default graph size to 800. (Thanks to Guillaume
+ Sachot)
+
+2008-03-17 Fixed bug where no image was shown with CGI's running
+ on apache-mpm-worker.
+
+2007-03-12 Fixed crash when indexing file named 'rest'
+
+2006-12-09 Workaround for segmentaion fault with ruby 1.8.5
+
+Usage
+=====
+
+Philesight can be run in different modes: first, the filesystem is indexed
+and the results are stored in a database. When the database is generated, the
+tool can used to generate PNG files with the graphs. The database should be
+updated every once in a while of course.
+
+ * Index bulding:
+
+ ./philesight --db {db} --index {path}
+
+ {db} is the name of the database file that will be generated. It is a good
+ idea to throw away existing database files before indexing to avoid removed
+ files showing in your graph.
+
+ {path} is the top-level directory to start indexing. Usually, '/' is a
+ good choice.
+
+ This process might take some time, since it traverses the whole tree
+ from path-to-index downward and stores the data into the db. Make
+ sure to remove previous database files before indexing.
+
+
+ * PNG generating: In this mode, philesight generates a graph of the
+ filesystem from path and 4 levels downward. i
+
+ ./philesight --db {db} --path {path} --draw {png}
+
+ {db} is the filename of the index file that was generated earlier,
+ {path} is the directory which should be drawn, and {png} is the filename
+ of the to-be-generated PNG image file
+
+
+ * CGI: Philesight comes with a CGI module that can be run from within a
+ web server. Edit the configurable parameters in the top of this file
+ to point to the database. Make sure the datbase file is readable by
+ the webserver!
+
+ Available options:
+
+ * db: Path to database file.
+
+ * default_path: default path to show when CGI first loads.
+
+ * size: graph size. 800 pixels is often a good choice.
+
+ * show_list: render list of directories and their sizes blow graph.
+
+ * use_gradients: use gradient colors in graph (set to 'false' to generate
+ smaller PNG files)
+
+
+Bugs
+====
+
+ * Philesight is a ruby program, and thus is not particulary fast.
+ * Indexing takes longer than necassery.
+ * Proper error handling is mostly missing.
+ * Not very well tested.
+ * It might eat your disks.
+
diff --git a/philesight/philesight b/philesight/philesight
new file mode 100755
index 0000000..5ab0e09
--- /dev/null
+++ b/philesight/philesight
@@ -0,0 +1,62 @@
+#!/usr/bin/ruby
+# vi: ts=2 sw=2
+
+require 'getoptlong'
+require 'philesight'
+
+opts = GetoptLong.new(
+ [ "--index", "-i", GetoptLong::REQUIRED_ARGUMENT ],
+ [ "--draw", "-d", GetoptLong::REQUIRED_ARGUMENT ],
+ [ "--path", "-p", GetoptLong::REQUIRED_ARGUMENT ],
+ [ "--db", "-D", GetoptLong::REQUIRED_ARGUMENT ],
+ [ "--dump", "-u", GetoptLong::NO_ARGUMENT ],
+ [ "--help", "-h", GetoptLong::NO_ARGUMENT ]
+)
+
+def usage
+ puts
+ puts "usage: philesight <options>"
+ puts
+ puts "Options:"
+ puts " --db <db> Set path to database file"
+ puts " --path <path> Path to show in generated image"
+ puts " --index <path> Top-level directory to start indexing"
+ puts " --dump Dump database to readable format"
+ puts
+ puts "Examples:"
+ puts " Index to database: philesight --db <db> --index <path>"
+ puts " Generate PNG: philesight --db <db> --path <path> --draw <png>"
+ puts
+end
+
+
+t = Philesight.new
+path = ""
+
+opts.each do |opt, arg|
+
+ case opt
+ when "--draw"
+ t.draw(path, arg)
+
+ when "--index"
+ t.readdir(arg)
+
+ when "--path"
+ path = arg
+
+ when "--db"
+ t.db_open(arg)
+
+ when "--dump"
+ t.dump
+
+ else
+ usage
+
+ end
+end
+
+#
+# End
+#
diff --git a/philesight/philesight.cgi b/philesight/philesight.cgi
new file mode 100755
index 0000000..969269a
--- /dev/null
+++ b/philesight/philesight.cgi
@@ -0,0 +1,127 @@
+#!/usr/bin/ruby
+# vi: ts=4 sw=4
+
+require 'philesight'
+require 'cgi'
+
+# Config variables
+
+db = "./ps.db"
+default_path = "/"
+size = 800
+show_list = true
+use_gradients = true
+
+# Get parameters from environment and CGI. ISMAP image maps do not return a
+# proper CGI parameter, but only the coordinates appended after a question
+# mark. If this is found in the QUERY_STRING, assume the 'find' command
+
+cgi = CGI.new;
+qs = ENV["QUERY_STRING"]
+cmd = cgi.params['cmd'][0]
+path = cgi.params['path'][0] || default_path
+
+if(qs && qs =~ /\?(\d+,\d+)/ ) then
+ find_pos = $1
+ cmd = 'find'
+end
+
+ps = Philesight.new(4, size, use_gradients)
+ps.db_open(db)
+
+# Perform action depending on 'cmd' parameter
+
+case cmd
+
+ when "img"
+ puts "Content-type: image/png"
+ puts
+ $stdout.flush
+ ps.draw(path, "-")
+
+ when "find"
+ if(find_pos =~ /(\d+),(\d+)/) then
+ x, y = $1.to_i, $2.to_i
+ url = "?path=%s" % ps.find(path, x, y)
+ puts "Content-type: text/html"
+ puts
+ puts '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'
+ puts '<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr" >'
+ puts '<head>'
+ puts ' <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />'
+ puts ' <meta http-equiv="refresh" content="0; url=' + "#{url}" + '">'
+ puts '</head>'
+ puts '<body></body>'
+ puts '</html>'
+ end
+
+ else
+ random = ""
+ 1.upto(32) { random += (rand(26) + ?a).chr }
+ puts "Content-type: text/html"
+ puts
+ puts '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'
+ puts '<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr" >'
+ puts '<head>'
+ puts ' <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />'
+ puts " <title>Disk usage : #{path}</title>"
+ puts ' <style type="text/css">'
+ puts ' <!--'
+ puts ' body {color:black;text-align:center;background:#FAFAFA;}'
+ puts ' table {margin:auto;width:780px;}'
+ puts ' table,td {border:0;}'
+ puts ' td {padding:4px;text-align:left;}'
+ puts ' td.size {text-align:right;}'
+ puts ' thead td {font-weight:bold;border-bottom:1px solid black;background:#EEE;}'
+ puts ' tbody td {background:#F0F0F0;}'
+ puts ' tbody tr.parentdir td {background:#E5D0D0;}'
+ puts ' tbody tr.evenrow td {background:#E4E4E4;}'
+ puts ' '
+ puts ' -->'
+ puts ' </style>'
+ puts '</head>'
+ puts '<body>'
+ puts ' <p><a href="' + "?path=#{path}&amp;" + '">'
+ puts ' <img style="border:0" width="#{size}" height="#{size}" src="?cmd=img&amp;r=' + "#{random}&amp;path=#{path}" + '" ismap="ismap" alt="' + "#{path}" + '" />'
+ puts ' </a></p>'
+
+ if show_list then
+ # Array of files
+ content = ps.listcontent("#{path}")
+ if(content && content[0]) then
+ puts ' <table summary="File lists">'
+ puts ' <thead>'
+ puts ' <tr><td>Filename</td><td class="size">Size</td></tr>'
+ puts ' </thead>'
+ puts ' <tbody>'
+ puts ' <tr class="parentdir"><td>' + content[0][:path].to_s + '</td><td class="size">' + content[0][:humansize].to_s + '</td></tr>'
+
+ if(content[1].size > 0) then
+ linenum = 0
+
+ content[1] = content[1].sort_by { |f| - f[:size] }
+ content[1].each do |f|
+ if(linenum%2 == 0) then
+ print ' <tr class="evenrow">'
+ else
+ print ' <tr>'
+ end
+
+ puts '<td><a href="?path='+ CGI.escape(f[:path].to_s) +'">' + f[:path].to_s + '</a></td><td class="size">' + f[:humansize].to_s + '</td></tr>'
+
+ linenum += 1
+ end
+ end
+ puts ' </tbody>'
+ puts ' </table>'
+ end
+ end
+
+ puts '</body>'
+ puts '</html>'
+end
+
+#
+# End
+#
+
diff --git a/philesight/philesight.rb b/philesight/philesight.rb
new file mode 100644
index 0000000..b669927
--- /dev/null
+++ b/philesight/philesight.rb
@@ -0,0 +1,458 @@
+#!/usr/bin/ruby
+# vi: ts=2 sw=2
+
+require 'getoptlong'
+require 'cgi'
+require 'cairo'
+require 'bdb'
+
+class PNGWriter
+
+ def initialize(fname)
+ if fname != "-" then
+ @fd = File.open(fname, "w")
+ end
+ end
+
+ def write(data)
+ if @fd then
+ @fd.write(data)
+ else
+ print(data)
+ end
+ return data.length
+ end
+end
+
+
+class Philesight
+
+ def initialize(ringcount=4, size=800, use_gradient=true)
+ @max_files_per_dir = 50
+ @w = size
+ @h = size
+ @cx = @w / 2
+ @cy = @h / 2
+ @ringcount = ringcount
+ @ringwidth = ((size-50)/2) / (ringcount+1)
+ @use_gradient = use_gradient
+ @find_a = 0
+ @find_r = 0
+ end
+
+
+ #
+ # Open the database. Try read-write mode first, if this fails, re-open readonly
+ #
+
+ def db_open(fname)
+ begin
+ @db = BDB::Btree.open fname , nil, BDB::CREATE, 0644, "set_pagesize" => 1024, "set_cachesize" => [0, 32*1024,0]
+ rescue
+ @db = BDB::Btree.open fname , nil, BDB::RDONLY, 0644, "set_pagesize" => 1024, "set_cachesize" => [0, 32*1024,0]
+ end
+ end
+
+
+ #
+ # Dump database in human-readable form
+ #
+
+ def dump
+ @db.keys.each do |f|
+ puts "%s %s" % [ f, Marshal::load(@db[f]).inspect ]
+ end
+ end
+
+
+ #
+ # Concatenate a directory and a filename
+ #
+
+ def addpath(a, b)
+ return a + b if(a =~ /\/$/)
+ return a + "/" + b
+ end
+
+
+ #
+ # Read a directory and add to the database; this function is recursive
+ # for sub-directories
+ #
+
+ def readdir(dir)
+
+ size_file = {}
+ size_dir = {}
+ size_total = 0
+
+ # Traverse the directory and collect the size of all files and
+ # directories
+
+ begin
+ Dir.foreach(dir) do |f|
+
+ if(f != "." && f != "..") then
+
+ f_full = addpath(dir, f)
+ stat = File.lstat(f_full)
+
+ if(!stat.symlink?) then
+
+ if(stat.file?) then
+ size = File.size(f_full)
+ size_file[f] = size
+ size_total += size
+ end
+
+ if(stat.directory?) then
+ size = readdir(f_full)
+ if(size > 0) then
+ size_dir[f] = size
+ size_total += size
+ end
+ end
+ end
+ end
+
+ end
+ rescue SystemCallError => errmsg
+ puts errmsg
+ end
+
+ # If there are a lot of small files in this directory, group
+ # the smallest into one entry to avoid clutter
+
+ if(size_file.keys.length > @max_files_per_dir) then
+ list = size_file.keys.select { |f| size_file[f] < size_total / @max_files_per_dir }
+ rest = 0
+ list.each do |f|
+ rest += size_file[f]
+ size_file.delete(f)
+ end
+ size_file['rest'] = rest
+ end
+
+ # Store the files in the database
+
+ size_file.keys.each do |f|
+ f_full = addpath(dir, f)
+ @db[f_full] = Marshal::dump( [ size_file[f], [] ] )
+ end
+
+ # Store this directory with the list of children in the database
+
+ size = size_dir.merge(size_file)
+ children = size.keys.sort
+ @db[dir] = Marshal::dump( [ size_total, children ] )
+ return size_total
+ end
+
+
+ #
+ # Draw one section
+ #
+
+ def draw_section(cr, ang_from, ang_to, r_from, r_to, brightness)
+
+ ang_to, ang_from = ang_from, ang_to if (ang_to < ang_from)
+
+ if(brightness > 0) then
+ r, g, b = hsv2rgb((ang_from+ang_to)*0.5 / (Math::PI*2), 1.0-brightness, brightness/2+0.5)
+ else
+ r, g, b = 0.9, 0.9, 0.9
+ end
+
+ # Instead of using the r_from and r_to for the radial pattern, the stops
+ # are calculated. This is to work around a bug in cairo
+
+ r_total = ((@w - 50) / 2).to_f
+ pat = Cairo::RadialPattern.new(@cx, @cy, 0, @cx, @cy, r_total)
+ if @use_gradient then
+ pat.add_color_stop_rgb(r_from / r_total, r*0.8, g*0.8, b*0.8)
+ pat.add_color_stop_rgb(r_to / r_total, r*1.5, g*1.5, b*1.5)
+ else
+ pat.add_color_stop_rgb(0, r, g, b)
+ end
+
+ cr.new_path
+ cr.arc(@cx, @cy, r_from, ang_from, ang_to)
+ cr.arc_negative(@cx, @cy, r_to, ang_to, ang_from)
+ cr.close_path
+
+ cr.set_source(pat)
+ cr.fill_preserve
+ end
+
+
+ #
+ # Draw ring. This function is recursive for the outer rings
+ #
+
+ def draw_ring(cr, level, ang_min, ang_max, path)
+
+ ang_from = ang_min
+ ang_to = ang_min
+ ang_range = (ang_max - ang_min).to_f
+ r_from = level * @ringwidth
+ r_to = r_from + @ringwidth
+
+ unless(@db[path]) then
+ return "/"
+ end
+
+ total_path, child_path = Marshal::load( @db[path] )
+
+ # Draw a section proportional to the size of each file or subdir
+
+ child_path.each do |f|
+
+ f_full = addpath(path, f)
+ total_f, child_f = Marshal::load( @db[f_full] )
+
+ # Calculate start and end angles and draw section
+
+ ang_from = ang_to
+ ang_to += ang_range * total_f / total_path if(total_path > 0)
+ brightness = r_from.to_f / @cx
+ brightness = 0 if(f == 'rest')
+ draw_section(cr, ang_from, ang_to, r_from, r_to, brightness) if(cr)
+
+ # If we are looking for the path of an (x,y) pair, check if this section matches
+
+ if( (@find_a >= ang_from) && (@find_a <= ang_to) && (@find_r >= r_from) && (@find_r <= r_to) ) then
+ @find_path = f_full
+ end
+
+ # Draw outer rings
+
+ if(level < @ringcount) then
+ draw_ring(cr, level+1, ang_from, ang_to, f_full)
+ else
+ draw_section(cr, ang_from, ang_to, r_to, r_to+5, 0.5) if(cr && child_f.nitems > 0)
+ end
+
+ # Generate and save labels of filenames/sizes
+
+ if(cr && (ang_to - ang_from > 0.2)) then
+ size = filesize_readable(total_f)
+ x, y = pol2xy((ang_from+ang_to)/2, (r_from+r_to)/2)
+ label = {}
+ label[:x] = x
+ label[:y] = y
+ label[:text] = "%s\n%s" % [ f, size]
+ @labels << label
+ end
+ end
+ end
+
+
+ #
+ # Draw graph of the given path
+ #
+
+ def draw(path, fname)
+
+ # Create drawing context, white background
+
+ format = Cairo::FORMAT_ARGB32
+ surf = Cairo::ImageSurface.new(format, @w, @h)
+ cr = Cairo::Context.new(surf)
+ writer = PNGWriter.new(fname)
+
+ # Draw top level filename and size
+
+ unless(@db[path]) then
+ draw_text(cr, @cx, @cy, "Path '#{path}' not found in database", 12)
+ cr.target.write_to_png(writer)
+ return
+ end
+
+ total_path, child_path = Marshal::load( @db[path] )
+ draw_text(cr, @cx, 10, "%s (%s)" % [ path, filesize_readable(total_path) ], 14, true)
+ draw_text(cr, @cx, @cy, "cd ..", 14, true)
+
+ # Draw rings, recursively
+
+ @labels = []
+ draw_ring(cr, 1, 0, Math::PI*2, path)
+
+ # Draw circles on ring borders
+
+ cr.set_source_rgba(0, 0, 0, 0.7)
+ 0.upto(@ringcount+1) do | level |
+ cr.new_path
+ cr.set_line_width(0.3)
+ cr.arc(@cx, @cy, level * @ringwidth, 0, 2*Math::PI)
+ cr.stroke
+ end
+
+ # Draw labels on top of graph
+
+ @labels.each do |label|
+
+ cr.select_font_face("Sans", Cairo::FONT_SLANT_NORMAL, Cairo::FONT_WEIGHT_NORMAL)
+ cr.set_font_size(9)
+
+ # Draw text 4 times in a dark color, one time white on top
+
+ [[-1, 0, 0.2], [+1, 0, 0.2], [0, -1, 0.2], [0, +1, 0.2], [0, 0, 0.9]].each do |dx, dy, color|
+ cr.set_source_rgba(color, color, color, 1.0)
+ draw_text(cr, label[:x]+dx, label[:y]+dy, label[:text])
+ end
+
+ end
+
+ # Generate PNG file
+
+ cr.target.write_to_png(writer)
+ end
+
+
+ #
+ # List files/dir of the given path
+ #
+
+ def listcontent(path)
+
+ unless(@db[path]) then
+ return nil
+ end
+
+ dircontent = []
+
+ total_path, child_path = Marshal::load( @db[path] )
+
+ currentdir = {}
+ currentdir[:path] = path
+ currentdir[:humansize] = filesize_readable(total_path)
+
+ child_path.each do |f|
+ f_full = addpath(path, f)
+ total_f, child_f = Marshal::load( @db[f_full] )
+
+ fileinfo = {}
+ fileinfo[:path] = f_full
+ fileinfo[:size] = Integer(total_f)
+ fileinfo[:humansize] = filesize_readable(total_f)
+ dircontent << fileinfo
+ end
+
+ return [currentdir, dircontent]
+ end
+
+
+ #
+ # Find the path belonging to a (x,y) position in the graph
+ #
+
+ def find(path, x, y)
+ @find_a, @find_r = xy2pol(x, y)
+ @find_path = File.dirname(path)
+ draw_ring(nil, 1, 0, Math::PI*2, path)
+ @find_path
+ end
+
+
+ private
+
+
+ #
+ # Draw text on pos x,y
+ #
+
+ def draw_text(cr, x, y, text, size=11, bold=false)
+
+ lines = text.count("\n") + 1
+ y -= (lines-1) * (size+2) / 2.0
+
+ cr.select_font_face("Sans", Cairo::FONT_SLANT_NORMAL, bold ? Cairo::FONT_WEIGHT_BOLD : Cairo::FONT_WEIGHT_NORMAL)
+ cr.set_font_size(size)
+
+ text.split("\n").each do |line|
+ extents = cr.text_extents(line)
+ w = extents.width
+ h = extents.height
+ cr.move_to(x - w/2, y + h/2)
+ cr.show_text(line)
+ y += size+2
+ end
+ end
+
+
+ #
+ # convert color from (h,s,v) to (r,g,b) colorspace
+ #
+
+ def hsv2rgb(h, s, v)
+
+ h = h.to_f
+ s = s.to_f
+ v = v.to_f
+
+ h *= 6.0
+ i = h.floor
+ f = h - i
+ f = 1-f if ((i & 1) == 0)
+ m = v * (1 - s)
+ n = v * (1 - s * f)
+ i=0 if(i<0)
+ i=6 if(i>6)
+
+ case i
+ when 0, 6: r=v; g=n; b=m
+ when 1: r=n; g=v; b=m
+ when 2: r=m; g=v; b=n
+ when 3: r=m; g=n; b=v
+ when 4: r=n; g=m; b=v
+ when 5: r=v; g=m; b=n
+ end
+
+ [r, g, b]
+ end
+
+
+ #
+ # Convert polair (ang,radius) coordinate to cartesian (x,y)
+ #
+
+ def pol2xy(a, r)
+ x = Math.cos(a) * r + @cx
+ y = Math.sin(a) * r + @cy
+ [x, y]
+ end
+
+ #
+ # Convert cartesian (x,y) coordinate to polair (ang, radius)
+ #
+
+ def xy2pol(x, y)
+ x -= @cx;
+ y -= @cy;
+ a = Math.atan2(y, x)
+ a += 2*Math::PI if(a<0)
+ r = Math.sqrt(x*x + y*y)
+ [a, r]
+ end
+
+
+ #
+ # Convert a filesize in bytes to a human readable form
+ #
+
+ def filesize_readable(size)
+ if(size > 1024*1024*1024) then
+ return "%.1fG" % (size / (1024.0*1024*1024))
+ elsif(size > 1024*1024) then
+ return "%.1fM" % (size / (1024.0*1024))
+ elsif(size > 1024) then
+ return "%.1fK" % (size / (1024.0))
+ end
+ size
+ end
+
+end
+
+#
+# End
+#