summaryrefslogtreecommitdiffstats
path: root/philesight/philesight.rb
diff options
context:
space:
mode:
Diffstat (limited to 'philesight/philesight.rb')
-rw-r--r--philesight/philesight.rb458
1 files changed, 458 insertions, 0 deletions
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
+#