JSONfs - the JSON file system
#!/usr/bin/ruby | |
# JSONfs - the JSON file system | |
# (c) 2014 nilsding | |
# License: GPLv2 | |
# | |
# Requires RFuse, install it via `gem install rfuse` | |
# | |
# Based on the RFuse sample file system: | |
# https://github.com/lwoggardner/rfuse/blob/master/sample/test-ruby.rb | |
# (c) 2014 lwoggardner | |
# | |
# Usage: | |
# JSON=./a_json_file.json ruby jsonfs.rb mountpoint | |
require "rfuse" | |
require "json" | |
require "yaml" | |
if ENV['JSON'].nil? | |
raise "please set the JSON environment variable to the path of a valid " \ | |
"JSON file" | |
end | |
$json = {} | |
File.open ENV['JSON'], 'r' do |f| | |
$json = JSON.parse f.read | |
end | |
class JSONDir < Hash | |
attr_accessor :name, :mode, :actime, :modtime, :uid, :gid | |
def initialize(name, mode) | |
@uid = 0 | |
@gid = 0 | |
@actime = Time.now | |
@modtime = Time.now | |
@xattr = Hash.new | |
@name = name | |
@mode = mode | |
end | |
def stat | |
RFuse::Stat.directory(mode, uid: uid, gid: gid, atime: actime, | |
mtime: modtime, size: size) | |
end | |
def listxattr | |
@xattr.keys | |
end | |
def setxattr(name, value, flag) | |
@xattr[name] = value # TODO: don't ignore flag | |
end | |
def getxattr(name) | |
@xattr[name] | |
end | |
def removexattr(name) | |
@xattr.delete(name) | |
end | |
def size | |
48 #for testing only | |
end | |
def isdir | |
true | |
end | |
def insert_obj(obj, path) | |
d = self.search File.dirname path | |
if d.isdir | |
d[obj.name] = obj | |
else | |
raise Errno::ENOTDIR.new d.name | |
end | |
d | |
end | |
def remove_obj path | |
d = self.search File.dirname path | |
d.delete File.basename path | |
end | |
def search path | |
p = path.split('/').delete_if { |x| x == '' } | |
if p.length==0 | |
self | |
else | |
self.follow p | |
end | |
end | |
def follow(path_array) | |
if path_array.length == 0 | |
self | |
else | |
d = self[path_array.shift] | |
if d | |
d.follow(path_array) | |
else | |
raise Errno::ENOENT.new | |
end | |
end | |
end | |
def to_s | |
"Dir: " + @name + "(" + @mode.to_s + ")" | |
end | |
end | |
class JSONFile | |
attr_accessor :name, :mode, :actime, :modtime, :uid, :gid, :content | |
def initialize(name, mode, uid, gid) | |
@actime=0 | |
@modtime=0 | |
@xattr=Hash.new | |
@content="" | |
@uid=uid | |
@gid=gid | |
@name=name | |
@mode=mode | |
end | |
def stat | |
RFuse::Stat.file(mode, uid: uid, gid: gid, atime: actime, | |
mtime: modtime, size: size) | |
end | |
def listxattr | |
@xattr.keys | |
end | |
def setxattr(name,value,flag) | |
@xattr[name]=value #TODO:don't ignore flag | |
end | |
def getxattr(name) | |
@xattr[name] | |
end | |
def removexattr(name) | |
@xattr.delete(name) | |
end | |
def size | |
content.size | |
end | |
def isdir | |
false | |
end | |
def follow(path_array) | |
if path_array.length != 0 | |
raise Errno::ENOTDIR.new | |
else | |
self | |
end | |
end | |
def to_s | |
"File: " + @name + "(" + @mode.to_s + ")" | |
end | |
end | |
class JSONfs | |
def initialize(root) | |
@root = root | |
@blocks = 0 | |
end | |
def read_json_obj(o, path = '/', is_hash = true) | |
# puts "read_json_obj(#{o.class.to_s}, #{path.inspect}, #{is_hash})" | |
mkdir nil, path, 0777 unless path == '/' | |
@blocks += 1 | |
if is_hash | |
print "." | |
o.each do |k, v| | |
decide path, v, k | |
end | |
else | |
print "," | |
o.each_with_index do |elem, i| | |
decide path, elem, sprintf("%04d", i) | |
end | |
end | |
end | |
def decide(path, v, k) | |
# puts "decide(#{path.inspect}, #{v.class.to_s}, #{k.inspect})" | |
@blocks += 1 | |
file = File.expand_path(k.to_s, path) | |
if v.is_a? Array | |
read_json_obj v, file, false | |
elsif v.is_a? Hash | |
read_json_obj v, file | |
elsif v.is_a? Numeric | |
@root.insert_obj( | |
JSONFile.new(File.basename("#{file}"), 0777, 0, 0),"#{file}" | |
) | |
write nil, "#{file}", v.to_s, 0, nil | |
elsif v.is_a? String | |
@root.insert_obj( | |
JSONFile.new(File.basename("#{file}.txt"), 0777, 0, 0), "#{file}.txt" | |
) | |
write nil, "#{file}.txt", v.to_s, 0, nil | |
elsif v.is_a? NilClass | |
# TODO | |
elsif v.is_a? TrueClass | |
# TODO | |
elsif v.is_a? FalseClass | |
# TODO | |
end | |
end | |
def readdir(ctx, path, filler, offset, ffi) | |
d = @root.search path | |
if d.isdir | |
d.each do |name, obj| | |
filler.push name, obj.stat, 0 | |
end | |
else | |
raise Errno::ENOTDIR.new path | |
end | |
end | |
def getattr(ctx, path) | |
d = @root.search path | |
d.stat | |
end | |
def mkdir(ctx, path, mode) | |
raise Errno::EACCES.new unless @reading | |
@root.insert_obj JSONDir.new(File.basename(path), mode), path | |
end | |
def mknod(ctx, path, mode, major, minor) | |
raise Errno::EACCES.new unless @reading | |
@root.insert_obj( | |
JSONFile.new(File.basename(path), mode, ctx.uid, ctx.gid), path | |
) | |
end | |
def open(ctx,path,ffi) | |
end | |
#def release(ctx, path, fi) | |
#end | |
#def flush(ctx, path, fi) | |
#end | |
def chmod(ctx, path, mode) | |
raise Errno::EACCES.new unless @reading | |
d = @root.search path | |
d.mode = mode | |
end | |
def chown(ctx, path, uid, gid) | |
raise Errno::EACCES.new unless @reading | |
d = @root.search path | |
d.uid = uid | |
d.gid = gid | |
end | |
def truncate(ctx, path, offset) | |
d = @root.search path | |
d.content = d.content[0..offset] | |
end | |
def utime(ctx, path, actime, modtime) | |
d = @root.search path | |
d.actime = actime | |
d.modtime = modtime | |
end | |
def unlink(ctx, path) | |
raise Errno::EACCES.new unless @reading | |
@root.remove_obj path | |
end | |
def rmdir(ctx, path) | |
raise Errno::EACCES.new unless @reading | |
@root.remove_obj path | |
end | |
#def symlink(ctx, path, as) | |
#end | |
def rename(ctx, path, as) | |
raise Errno::EACCES.new unless @reading | |
d = @root.search path | |
@root.remove_obj path | |
@root.insert_obj d, path | |
end | |
#def link(ctx, path, as) | |
#end | |
def read(ctx, path, size, offset, fi) | |
d = @root.search path | |
if d.isdir | |
raise Errno::EISDIR.new path | |
nil | |
else | |
d.content[offset..offset + size - 1] | |
end | |
end | |
def write(ctx, path, buf, offset, fi) | |
raise Errno::ENOSPC.new unless @reading | |
d = @root.search path | |
if d.isdir | |
raise Errno::EISDIR.new path | |
else | |
d.content[offset..offset + buf.length - 1] = buf | |
end | |
buf.length | |
end | |
def setxattr(ctx, path, name, value, size, flags) | |
raise Errno::EACCES.new unless @reading | |
d = @root.search path | |
d.setxattr path, name, value, size, flags | |
end | |
def getxattr(ctx, path, name) | |
d = @root.search path | |
if d | |
value = d.getxattr name | |
unless value | |
value = "" | |
#raise Errno::ENOENT.new # TODO raise the correct error : | |
#NOATTR which is not implemented in Linux/glibc | |
end | |
else | |
raise Errno::ENOENT.new | |
end | |
value | |
end | |
def listxattr(ctx, path) | |
d = @root.search path | |
value = d.listxattr | |
value | |
end | |
def removexattr(ctx, path, name) | |
raise Errno::EACCES.new unless @reading | |
d = @root.search path | |
d.removexattr name | |
end | |
#def opendir(ctx, path, ffi) | |
#end | |
#def releasedir(ctx, path, ffi) | |
#end | |
#def fsyncdir(ctx, path, meta, ffi) | |
#end | |
def statfs(ctx, path) | |
s = RFuse::StatVfs.new() | |
s.f_bsize = 1024 | |
s.f_frsize = 1024 | |
s.f_blocks = @blocks | |
s.f_bfree = 0 | |
s.f_bavail = 0 | |
s.f_files = 10000 | |
s.f_ffree = 9900 | |
s.f_favail = 9900 | |
s.f_fsid = 23423 | |
s.f_flag = 0 | |
s.f_namemax = 10000 | |
s | |
end | |
def ioctl(ctx, path, cmd, arg, ffi, flags, data) | |
# TODO: test this | |
puts "*** IOCTL: command: #{cmd}" | |
end | |
def poll(ctx, path, ffi, ph, reventsp) | |
puts "*** POLL: #{path}" | |
# This is how we notify the caller if something happens: | |
ph.notifyPoll(); | |
# when the GC harvests the object it calls fuse_pollhandle_destroy | |
# by itself. | |
end | |
def init(ctx, rfuseconninfo) | |
puts "JSONfs started" | |
puts "init called" | |
puts "proto_major:#{rfuseconninfo.proto_major}" | |
print "reading JSON" | |
@reading = true | |
read_json_obj $json | |
@reading = nil | |
$json = nil | |
puts " done" | |
end | |
end | |
if ARGV.length == 0 | |
puts "Usage: #{$0} mountpoint [mount_options...]" | |
puts | |
puts " -h for supported options" | |
exit 1 | |
end | |
fs = JSONfs.new JSONDir.new("", 0777) | |
fo = RFuse::FuseDelegator.new fs, *ARGV | |
if fo.mounted? | |
trap "TERM" do | |
puts "Caught SIGTERM" | |
fo.exit | |
end | |
trap "INT" do | |
puts "Caught SIGINT" | |
fo.exit | |
end | |
begin | |
fo.loop | |
rescue | |
puts "Error: #{$!}" | |
ensure | |
fo.unmount if fo.mounted? | |
puts "Unmounted #{ARGV[0]}" | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment