require "file_utils" require "json" class FS::Hash(K, V) class PartitionData(V) property name : String property key_proc : Proc(V, String) def initialize(@name, @key_proc) end end @partitions = [] of PartitionData(V) def initialize(@directory_name : String) Dir.mkdir_p @directory_name end ## # name is the name that will be used on the file system. def new_partition(name : String, &block : Proc(V, String)) @partitions.push PartitionData(V).new name, block Dir.mkdir_p "#{@directory_name}/.by_#{name}" end def get_partition(name : String, key : K) r_value = Array(V).new partition_directory = "#{@directory_name}/.by_#{name}/#{key}" Dir.each_child partition_directory do |child| pp child r_value << V.from_json ::File.read "#{partition_directory}/#{child}" end r_value end def []?(key : K) : V? begin read file_path key rescue # FIXME: Only rescue JSON and “no such file” errors. return nil end end def [](key : K) : V read file_path key end def []=(key : K, value : V) # FIXME: Update partitions pointing to previous value (if any) # avoid corruption in case of crash during file writing file_path(key).tap do |path| ::File.write "#{path}.new", value.to_json ::FileUtils.mv "#{path}.new", path end @partitions.each do |index| index_key = index.key_proc.call value symlink = file_path(key.to_s, index.name, index_key) Dir.mkdir_p ::File.dirname symlink # FIXME: why? # Hint: to update old symlinks (new data directory) ::File.delete symlink if ::File.exists? symlink ::File.symlink symlink_path(key), symlink end end def delete(key : K) value = self[key]? begin ::File.delete file_path key rescue # FIXME: Only intercept “no such file" errors end unless value.nil? @partitions.each do |index| index_key = index.key_proc.call value symlink = file_path(key, index.name, index_key) puts "old index #{key.to_s} => #{index_key}" puts "symlink is #{symlink}" ::File.delete symlink end end value end ## # CAUTION: Very slow. Try not to use. # Can be useful for making dumps or to restore a database, however. def each Dir.each_child @directory_name do |child| next if child.match /^\./ full_path = "#{@directory_name}/#{child}" begin # FIXME: Only intercept JSON parsing errors. field = read full_path rescue next end # FIXME: Will only work for String. :( key = child.gsub /\.json$/, "" yield key, field end end ## # CAUTION: Very slow. Try not to use. def to_h hash = ::Hash(K, V).new each do |key, value| hash[key] = value end hash end private def file_path(key : K) "#{@directory_name}/#{key.to_s}.json" end private def file_path(key : String, index_name : String, index_key : String) "#{@directory_name}/.by_#{index_name}/#{index_key}/#{key}.json" end private def symlink_path(key : K) "../../#{key.to_s}.json" end private def read(file_path : String) V.from_json ::File.read file_path end end