require 'fusefs' require 'thread' # # this class is a skeleton for a caching filesystem # a subclass should implement # * get_file_size(fullpath) [will be cached in @size_cache[fullpath] # * get_file_content(fullpath, offset) [the returned block will be cached, feel free to return any size] # * directory_listing(fullpath) [will be cached in @dirs_cache[fullpath] # # directory and size caches are invalidated at most every @cache_timeout seconds if this variable is defined # a control directory named after @control_dir contains writeable files, interaction with the user # are made through +read_control_file+ / +write_control_file+ # # Author:: Yoann Guillot (at ofjj.net) # Copyright:: Copyright (c) 2006 Yoann Guillot # Licence:: Distributes under the terms of the GPL version 2 # class FuseROFS # mounts the object under the specified mountpoint # drops root privileges # can fork to background # will add a debugging proxy object if +$DEBUG+ is set def mount_under(mountpoint, background = !$VERBOSE) raise 'mountpoint is not a directory' unless File.directory? mountpoint raise 'mountpoint is not writeable' unless File.writable? mountpoint raise 'mountpoint not empty' unless Dir.entries(mountpoint).sort == ['.', '..'] FuseFS.mount_under mountpoint, 'allow_other' init_cache if background if not Process.fork [$stdin, $stdout, $stderr].each { |fd| fd.close } Process.setsid else exit! end end fs = self fs = Dbg.new fs if $DEBUG FuseFS.set_root fs info 'running' if Process.euid == 0 # drop privileges if Process.fork # unmount need privileges Process.wait FuseFS.unmount else Process::Sys.setresgid 65534, 65534, 65534 Process::Sys.setresuid 65534, 65534, 65534 FuseFS.run exit! end else FuseFS.run end end def initialize @control_dir = '.fuserofs-ctrl/' @control_files = [] @control_files = %w[flush umount] @file_accessor_int = [] @file_accessor = [] @prefetch_mutex = Mutex.new end # :nodoc: FuseFS interface def raw_open(path, mode) not mode.include? 'w' end def raw_close(path) true end def can_write?(path) is_control_file(path) end # :nodoc: somehow 'echo 1 > bla' needs to be able to delete (O_TRUNC ?) alias :can_delete? :can_write? def raw_read(path, off, sz) if f = is_control_file(path) return read_control_file(f)[off, sz].to_s end @data_cache ||= {} if path != @data_cache[:path] or off < @data_cache[:off] or off >= @data_cache[:end] data = get_file_content_prefetch(path, off) @data_cache[:path] = path.dup @data_cache[:off] = off @data_cache[:end] = off + data.length @data_cache[:data] = data end @data_cache[:data][ off-@data_cache[:off], sz ].to_s end def write_to(path, content) write_control_file(is_control_file(path), content) end def contents(path) init_cache if defined? @cache_timeout and @last_init_cache < Time.now - @cache_timeout path << '/' if path[-1] != ?/ @dirs_cache[path] ||= @prefetch_mutex.synchronize { directory_listing(path) } @dirs_cache[path].map { |e| e[-1] == ?/ ? e.chop : e } end def directory?(path) dir = path[/.*\//] base = path[dir.length..-1] contents(dir) # ensure the dir cache is populated @dirs_cache[dir].include? "#{base}/" end def file?(path) dir = path[/.*\//] base = path[dir.length..-1] contents(dir) @dirs_cache[dir].include? base end def size(path) @size_cache[path] ||= if f = is_control_file(path) read_control_file(f).length else @prefetch_mutex.synchronize { get_file_size(path) } end end private def file_accessor_int(*a) a.map! { |s| s.to_s } (@control_files ||= []).concat a (@file_accessor_int ||= []).concat a end def file_accessor(*a) a.map! { |s| s.to_s } (@control_files ||= []).concat a (@file_accessor ||= []).concat a end # prints its argument in debug mode def debug(*s) puts(*s) if $DEBUG end # prints its argument in verbose mode def info(*s) puts(*s) if $VERBOSE end def get_file_content_prefetch(path, off) @prefetch_thread ||= Thread.new { loop do Thread.stop unless Thread.current[:status] == :pending @prefetch_mutex.synchronize { tc = Thread.current next unless tc[:status] == :pending tc[:status] = :running begin tc[:data] = get_file_content(tc[:path], tc[:off]) tc[:status] = :done rescue tc[:status] = :error end } end } @prefetch_mutex.synchronize { td = @prefetch_thread data = if td[:status] == :done and td[:path] == path and td[:off] == off debug 'prefetch hit' td[:data] else debug 'prefetch miss' get_file_content(path, off) end if data.length > 0 # queue prefetch td[:path] = path td[:off] = off + data.length td[:status] = :pending td.run end data } end # populates the top-level directory and the meta-directory of control files def init_cache @last_init_cache = Time.now @dirs_cache = {} @size_cache = {} @dirs_cache['/'] = @prefetch_mutex.synchronize { directory_listing('/') } @dirs_cache['/'] << @control_dir @dirs_cache['/'+@control_dir] = @control_files end # returns the control file part of the filename if the path starts with +@control_dir+ def is_control_file(path) f = path[@control_dir.length+1..-1] f if path[1, @control_dir.length] == @control_dir and @control_files.include? f end # returns the whole content of a control file # should include a terminating "\n" def read_control_file(f) if (@file_accessor_int + @file_accessor).include? f instance_variable_get("@#{f}") else 0 end.to_s + "\n" end # changes the content of a control file def write_control_file(f, content) case f when *@file_accessor_int instance_variable_set("@#{f}", content.to_i) info "set @#{f} to #{content.to_i}" when *@file_accessor instance_variable_set("@#{f}", content.chomp) info "set @#{f} to #{f}" when 'flush' if content.to_i != 0 init_cache info 'cache flushed' end when 'umount' if content.to_i != 0 FuseFS.exit info 'exiting' end end end # # Mandatory methods to override # # returns the size of the file def get_file_size(path) 0 end # returns a block of data from path starting at offset # If more data is returned than what is asked, the result is cached # for subsequent reads # Should return at least 200ko def get_file_content(path, offset) '' end # returns an array representing the content of the directory # entries ending with a "/" are directories # if this returns an array including an empty entry or filenames with # embedded slashes, will cause FuseFS to fail, and the directory will # appear empty def directory_listing(path) ['foobar'] end # debugging proxy class # shows all method call passed to its subject, and return values class Dbg def initialize(target) @target = target end def method_missing(m, *a, &b) puts "#{m} #{a.inspect}#{' {}' if block_given?}" ret = @target.send(m, *a, &b) if ret.respond_to? :length and ret.length > 50 puts " => #{ret[0..50].inspect}..." else puts " => #{ret.inspect}" end ret end private(*public_instance_methods) undef respond_to? def respond_to?(*a) # @target.respond_to?(*a) method_missing :respond_to?, *a end end end if $0 == __FILE__ abort "usage: #$0 " if ARGV.empty? FuseROFS.new.mount_under ARGV.shift end