class FDB::DirectoryLayer
def check_version(tr, write_access)
def check_version(tr, write_access) version = tr[@root_node['version']] initialize_directory(tr) if !version && write_access return if !version version = version.to_s.unpack('III<') dir_ver = "#{version[0]}.#{version[1]}.#{version[2]}" layer_ver = "#{@@VERSION[0]}.#{@@VERSION[1]}.#{@@VERSION[2]}" if version[0] != @@VERSION[0] raise "Cannot load directory with version #{dir_ver} using directory layer #{layer_ver}" elsif version[1] != @@VERSION[1] && write_access raise "Directory with version #{dir_ver} is read-only when opened using directory layer #{layer_ver}" end end
def contents_of_node(node, path, layer='')
def contents_of_node(node, path, layer='') prefix = @node_subspace.unpack(node.key)[0] if layer == 'partition' DirectoryPartition.new(@path + path, prefix, self) else DirectorySubspace.new(@path + path, prefix, self, layer) end end
def convert_path_element(name)
def convert_path_element(name) if !name.kind_of? String raise TypeError, 'Invalid path: must be a unicode string or an array of unicode strings' end name.dup.force_encoding('UTF-8') end
def create(db_or_tr, path, options={})
def create(db_or_tr, path, options={}) create_or_open_internal(db_or_tr, path, true, false, options) end
def create_directory(tr, path, options)
def create_directory(tr, path, options) check_version(tr, true) prefix = options[:prefix] if prefix.nil? prefix = @content_subspace.key + @allocator.allocate(tr) if !tr.get_range_start_with(prefix, { :limit => 1 }).to_a.empty? raise "The database has keys stored at the prefix chosen by the automatic prefix allocator: #{prefix.dump}." end if !is_prefix_free?(tr.snapshot, prefix) raise 'The directory layer has manually allocated prefixes that conflict with the automatic prefix allocator.' end elsif !is_prefix_free?(tr, prefix) raise ArgumentError, 'The given prefix is already in use.' end parent_node = if path[0...-1].length > 0 node_with_prefix(create_or_open(tr, path[0...-1]).key) else @root_node end raise 'The parent directory does not exist.' unless parent_node node = node_with_prefix(prefix) tr[parent_node[@@SUBDIRS][path[-1]]] = prefix tr[node['layer']] = options[:layer] contents_of_node(node, path, options[:layer]) end
def create_or_open(db_or_tr, path, options={})
def create_or_open(db_or_tr, path, options={}) create_or_open_internal(db_or_tr, path, true, true, options) end
def create_or_open_internal(db_or_tr, path, allow_create, allow_open, options={})
def create_or_open_internal(db_or_tr, path, allow_create, allow_open, options={}) defaults = { :layer => '', :prefix => nil } options = defaults.merge(options) if !options[:prefix].nil? and allow_open and allow_create raise ArgumentError, 'Cannot specify a prefix when calling create_or_open.' end if !options[:prefix].nil? and !@allow_manual_prefixes if @path.length == 0 raise ArgumentError, 'Cannot specify a prefix unless manual prefixes are enabled.' else raise ArgumentError, 'Cannot specify a prefix in a partition.' end end db_or_tr.transact do |tr| check_version(tr, false) path = to_unicode_path(path) raise ArgumentError, 'The root directory cannot be opened.' if path.length == 0 existing_node = find(tr, path).prefetch_metadata(tr) if existing_node.exists? if existing_node.is_in_partition? subpath = existing_node.get_partition_subpath existing_node.get_contents(self).directory_layer.create_or_open_internal(tr, subpath, allow_create, allow_open, options) else raise ArgumentError, 'The directory already exists.' unless allow_open open_directory(path, options, existing_node) end else raise ArgumentError, 'The directory does not exist.' unless allow_create create_directory(tr, path, options) end end end
def exists?(db_or_tr, path=[])
def exists?(db_or_tr, path=[]) db_or_tr.transact do |tr| check_version(tr, false) path = to_unicode_path(path) node = find(tr, path).prefetch_metadata(tr) next false if !node.exists? if node.is_in_partition? next node.get_contents(self).exists?(tr, node.get_partition_subpath) end true end end
def find(tr, path)
def find(tr, path) node = Internal::Node.new(@root_node, [], path) path.each_with_index do |name, index| node = Internal::Node.new(node_with_prefix(tr[node.subspace[@@SUBDIRS][name]]), path[0..index], path) return node unless node.exists? and node.layer(tr) != 'partition' end node end
def initialize(options={})
def initialize(options={}) defaults = { :node_subspace => Subspace.new([], "\xfe"), :content_subspace =>Subspace.new, :allow_manual_prefixes => false } options = defaults.merge(options) @content_subspace = options[:content_subspace] @node_subspace = options[:node_subspace] @allow_manual_prefixes = options[:allow_manual_prefixes] @root_node = @node_subspace[@node_subspace.key] @allocator = HighContentionAllocator.new(@root_node['hca']) @path = [] @layer = '' end
def initialize_directory(tr)
def initialize_directory(tr) tr[@root_node['version']] = @@VERSION.pack('III<') end
def is_prefix_free?(tr, prefix)
def is_prefix_free?(tr, prefix) prefix && prefix.length > 0 && !node_containing_key(tr, prefix) && tr.get_range(@node_subspace.pack([prefix]), @node_subspace.pack([FDB.strinc(prefix)]), { :limit => 1 }).to_a.empty? end
def layer
def layer return @layer.dup end
def list(db_or_tr, path=[])
def list(db_or_tr, path=[]) db_or_tr.transact do |tr| check_version(tr, false) path = to_unicode_path(path) node = find(tr, path).prefetch_metadata(tr) raise ArgumentError, 'The directory does not exist.' unless node.exists? if node.is_in_partition?(nil, true) next node.get_contents(self).list(tr, node.get_partition_subpath) end subdir_names_and_nodes(tr, node.subspace).map { |name, node| name } end end
def move(db_or_tr, old_path, new_path)
def move(db_or_tr, old_path, new_path) db_or_tr.transact do |tr| check_version(tr, true) old_path = to_unicode_path(old_path) new_path = to_unicode_path(new_path) if old_path == new_path[0...old_path.length] raise ArgumentError, 'The desination directory cannot be a subdirectory of the source directory.' end old_node = find(tr, old_path).prefetch_metadata(tr) new_node = find(tr, new_path).prefetch_metadata(tr) raise ArgumentError, 'The source directory does not exist.' unless old_node.exists? if old_node.is_in_partition? || new_node.is_in_partition? if !old_node.is_in_partition? || !new_node.is_in_partition? || old_node.path != new_node.path then raise ArgumentError, 'Cannot move between partitions' end next new_node .get_contents(self) .move(tr, old_node.get_partition_subpath, new_node.get_partition_subpath) end if new_node.exists? raise ArgumentError, 'The destination directory already exists. Remove it first.' end parent_node = find(tr, new_path[0...-1]) if !parent_node.exists? raise ArgumentError, 'The parent directory of the destination directory does not exist. Create it first.' end tr[parent_node.subspace[@@SUBDIRS][new_path[-1]]] = @node_subspace.unpack(old_node.subspace.key)[0] remove_from_parent(tr, old_path) contents_of_node(old_node.subspace, new_path, old_node.layer) end end
def move_to(db_or_tr, new_absolute_path)
def move_to(db_or_tr, new_absolute_path) raise 'The root directory cannot be moved.' end
def node_containing_key(tr, key)
def node_containing_key(tr, key) return @root_node if key.start_with?(@node_subspace.key) tr.get_range(@node_subspace.range[0], @node_subspace.pack([key]) + "\x00", { :reverse => true, :limit => 1}) .map { |kv| prev_prefix = @node_subspace.unpack(kv.key)[0] node_with_prefix(prev_prefix) if key.start_with?(prev_prefix) }[0] end
def node_with_prefix(prefix)
def node_with_prefix(prefix) @node_subspace[prefix] if !prefix.nil? end
def open(db_or_tr, path, options={})
def open(db_or_tr, path, options={}) create_or_open_internal(db_or_tr, path, false, true, options) end
def open_directory(path, options, existing_node)
def open_directory(path, options, existing_node) if options[:layer] and !options[:layer].empty? and options[:layer] != existing_node.layer raise 'The directory was created with an incompatible layer.' end existing_node.get_contents(self) end
def path
def path return @path.dup end
def remove(db_or_tr, path=[])
def remove(db_or_tr, path=[]) remove_internal(db_or_tr, path, true) end
def remove_from_parent(tr, path)
def remove_from_parent(tr, path) parent = find(tr, path[0...-1]) tr.clear(parent.subspace[@@SUBDIRS][path[-1]]) end
def remove_if_exists(db_or_tr, path=[])
def remove_if_exists(db_or_tr, path=[]) remove_internal(db_or_tr, path, false) end
def remove_internal(db_or_tr, path, fail_on_nonexistent)
def remove_internal(db_or_tr, path, fail_on_nonexistent) db_or_tr.transact do |tr| check_version(tr, true) path = to_unicode_path(path) if path.empty? raise ArgumentError, 'The root directory cannot be removed.' end node = find(tr, path).prefetch_metadata(tr) if !node.exists? raise ArgumentError, 'The directory does not exist.' if fail_on_nonexistent next false end if node.is_in_partition? next node.get_contents(self).directory_layer .remove_internal(tr, node.get_partition_subpath, fail_on_nonexistent) end remove_recursive(tr, node.subspace) remove_from_parent(tr, path) true end end
def remove_recursive(tr, node)
def remove_recursive(tr, node) subdir_names_and_nodes(tr, node).each do |name, subnode| remove_recursive(tr, subnode) end tr.clear_range_start_with(@node_subspace.unpack(node.key)[0]) tr.clear_range(node.range[0], node.range[1]) end
def subdir_names_and_nodes(tr, node)
def subdir_names_and_nodes(tr, node) subdir = node[@@SUBDIRS] tr.get_range(subdir.range[0], subdir.range[1]).map { |kv| [subdir.unpack(kv.key)[0], node_with_prefix(kv.value)] } end
def to_unicode_path(path)
def to_unicode_path(path) if path.respond_to? 'each_with_index' path.each_with_index { |name, index| path[index] = convert_path_element(name) } else [convert_path_element(path)] end end