#!/usr/bin/env ruby
#
# acvfs.rb: Archive FileSystem Based on FUSE/Ruby
# (C)2006 by HIROSE, Yuuji [yuuji@yatex.org]
#
# Usage: acvfs.rb mountpoint
#
# This file system provides zip/tar.gz/tar.bz2 auto inspection.
#
# Requirements:
# http://fuse.sourceforge.net/ - Filesystem in Userspace
# http://rubyforge.org/projects/fusefs - Ruby binding of FUSE-FS
#
# Todo:
#  Support other archivers than arc, arj, rar, ... and so on.
#  But it might be easy.  Try it by yourself.
#
require 'fusefs'

class ArchiveDir
  def initialize
    @cachedacv = Hash.new
  end
  def zip?(path)
    /\.zip/i =~ path
  end
  def targz?(path)
    /\.(tar\.gz|tgz)/i =~ path
  end
  def tarbz2?(path)
    /\.(tar\.bz2|tbz)/i =~ path
  end
  def acv?(path)  # path is archived file or within it, or not
    zip?(path) || targz?(path) || tarbz2(path)
  end
  def acvfile?(path) # path IS archived file, or not
    /\.(zip|tar\.(gz|bz2))$/i =~ path
  end
  def listcmd(arc)
    # Return the array of [ListingCmd, Regexp, [$size, $name], EndRattern]
    if zip?(arc)
      ["| unzip -v \"#{arc}\" | tail +4",
        /(\d+) +(\d+)% +(\d+)-(\d+)-(\d+) +(\d\d):(\d\d) .* (\S+)$/,
        [1, 8],
        /^---/]
    elsif targz?(arc)||tarbz2?(arc)
      c = targz?(arc) ? "z" : "j"
      ["| tar v#{c}tf \"#{arc}\"",
        /(\d+) +(\d{4})-(\d+)-(\d+) +(\d\d):(\d\d):(\d\d) +(\S+)$/,
        [1, 8],
        /^$/]
    end
  end

  # cache all filenames, sizes and whether they are directory
  def acvinfo(acv)
    %r,(.*\.(zip|tar\.(gz|bz2)))(/(.*))?, =~ acv
    dir = $1
    prefix = $5
    if !@cachedacv[acv]
      @cachedacv[acv] = Hash.new
      listinfo = listcmd(dir)
      open(listinfo[0], "r") do |zls|
        while line = zls.gets
          case line
          when listinfo[3]
            break
          when listinfo[1]
            md = Regexp.last_match
            dirp = nil          # is_directory flag
            name = md[listinfo[2][1]]
            if prefix
              next unless name.index(prefix) == 0
              next if name == prefix
              name.sub!(prefix+"/", "") # strip prefix directory
            end
            next if name == ""     # skip prefix dir itself
            next if %r,/., =~ name # skip files in subdirectory
            if %r,/$, =~ name
              name.chop!
              dirp = true
            end
            @cachedacv[acv][name] = Hash.new
            @cachedacv[acv][name]['dir'] = dirp
            @cachedacv[acv][name]['size'] = md[listinfo[2][0]].to_i
            # p ["set", acv, name, dir, md[listinfo[2][0]].to_i, dirp]
          end
        end
      end
    end
    @cachedacv[acv]
  end
  # Return the list of files in directory: path
  def contents(path)
    mntpt=ARGV[0].gsub(%r|/+|, "/")
    if test(?d, path)
      Dir.entries(path).select{|f|
        n = (path+"/"+f).gsub(%r|/+|, "/")
        next if ARGV[0] == n
        test(?d, n) || test(?f, n) && acvfile?(f)
      }
    elsif acv?(path)
      acvinfo(path).keys
    end
  end

  def file?(path)
    test(?f, path) ||
      if acv?(path)
        acv = File.dirname(path)
        file = File.basename(path)
        acvinfo(acv).has_key?(file) && !acvinfo(acv)[file]['dir']
      end

  end
  def executable?(path)
    test(?d, path) ? test(x, path) : false # shoud be safer
  end
  def directory?(path)
    test(?d, path) || acvfile?(path) ||
      if acv?(path)
        acv = File.dirname(path)
        file = File.basename(path)
        acvinfo(acv)[file]['dir']
      end
  end
  def can_write?(path)
    false
  end

  def read_file(path)
    %r,(.*\.(tar\.(gz|bz2)|zip))(/(.*))?, =~ path
    acv, file = $1, $5
    if acv && file
      cmd = if zip?(acv)
              "unzip -cqq \"#{acv}\" \"#{file}\""
            elsif targz?(acv)
              "tar zxOf \"#{acv}\" \"#{file}\""
            elsif tarbz2?(acv)
              "tar jxOf \"#{acv}\" \"#{file}\""
            end
      IO.popen(cmd, "r").read
    else
      "Not an archived file\n"
    end
  end

  def size(path)
    acv = File.dirname(path)
    file = File.basename(path)
    if acv?(acv)
      acvinfo(acv)[file]['size']
    else
      test(?s, path)
    end
  end
end

adir = ArchiveDir.new
FuseFS.set_root( adir )

# Mount under a directory given on the command line.
FuseFS.mount_under ARGV[0]
FuseFS.run
