Created
November 17, 2014 18:40
-
-
Save nilsding/b42038aef85c3adc0d66 to your computer and use it in GitHub Desktop.
JSONfs - the JSON file system
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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