class Gemstash::Resource
A resource within the storage engine. The resource may have 1 or more files associated with it along with a metadata Hash that is stored in a YAML file.
Constants
- VERSION
Attributes
Public Class Methods
This object should not be constructed directly, but instead via {Gemstash::Storage#resource}.
# File lib/gemstash/storage.rb, line 112 def initialize(folder, name) @base_path = folder @name = name # Avoid odd characters in paths, in case of issues with the file system safe_name = sanitize(@name) # Use a trie structure to avoid file system limits causing too many files in 1 folder # Downcase to avoid issues with case insensitive file systems trie_parents = safe_name[0...3].downcase.split("") # The digest is included in case the name differs only by case # Some file systems are case insensitive, so such collisions will be a problem digest = Digest::MD5.hexdigest(@name) child_folder = "#{safe_name}-#{digest}" @folder = File.join(@base_path, *trie_parents, child_folder) @properties = nil end
Public Instance Methods
Fetch the content for the given key
. This will load and cache the properties and the content of the key
. The key
corresponds to the content
key provided to {#save}.
@param key [Symbol] the key of the content to load @return [String] the content stored in the key
# File lib/gemstash/storage.rb, line 177 def content(key) @content ||= {} load(key) unless @content.include?(key) @content[key] end
Delete the content for the given key
. If the key
is the last one for this resource, the metadata properties will be deleted as well. The key
corresponds to the content
key provided to {#save}.
The resource will be reset afterwards, clearing any cached content or properties.
Does nothing if the key doesn’t {#exist?}.
@param key [Symbol] the key of the content to delete @return [Gemstash::Resource] self for chaining purposes
# File lib/gemstash/storage.rb, line 250 def delete(key) return self unless exist?(key) begin File.delete(content_filename(key)) rescue StandardError => e log_error "Failed to delete stored content at #{content_filename(key)}", e, level: :warn end begin File.delete(properties_filename) unless content? rescue StandardError => e log_error "Failed to delete stored properties at #{properties_filename}", e, level: :warn end self ensure reset end
When key
is nil, this will test if this resource exists with any content. If a key
is provided, this will test that the resource exists with at least the given key
file. The key
corresponds to the content
key provided to {#save}.
@param key [Symbol, nil] the key of the content to check existence @return [Boolean] true if the indicated content exists
# File lib/gemstash/storage.rb, line 135 def exist?(key = nil) if key File.exist?(properties_filename) && File.exist?(content_filename(key)) else File.exist?(properties_filename) && content? end end
Fetch the metadata properties for this resource. The properties will be cached for future calls.
@return [Hash] the metadata properties for this resource
# File lib/gemstash/storage.rb, line 187 def properties load_properties @properties || {} end
Check if the metadata properties includes the keys
. The keys
represent a nested path in the properties to check.
Examples:
resource = Gemstash::Storage.for("x").resource("y") resource.save({ file: "content" }, foo: "one", bar: { baz: "qux" }) resource.has_property?(:foo) # true resource.has_property?(:bar, :baz) # true resource.has_property?(:missing) # false resource.has_property?(:foo, :bar) # false
@param keys [Array<Object>] one or more keys pointing to a property @return [Boolean] whether the nested keys points to a valid property
# File lib/gemstash/storage.rb, line 228 def property?(*keys) keys.inject(node: properties, result: true) do |memo, key| if memo[:result] memo[:result] = memo[:node].is_a?(Hash) && memo[:node].include?(key) memo[:node] = memo[:node][key] if memo[:result] end memo end[:result] end
Save one or more files for this resource given by the content
hash. Metadata properties about the file(s) may be provided in the optional properties
parameter. The keys in the content hash correspond to the file name for this resource, while the values will be the content stored for that key.
Separate calls to save for the same resource will replace existing files, and add new ones. Properties on additional calls will be merged with existing properties. Nested hashes in properties will also be merged.
Examples:
Gemstash::Storage.for("foo").resource("bar").save(baz: "qux") Gemstash::Storage.for("foo").resource("bar").save(baz: "one", qux: "two") Gemstash::Storage.for("foo").resource("bar").save({ baz: "qux" }, meta: true)
@param content [Hash{Symbol => String}] files to save, *must not be nil* @param properties [Hash, nil] metadata properties related to this resource @return [Gemstash::Resource] self for chaining purposes
# File lib/gemstash/storage.rb, line 162 def save(content, properties = nil) content.each do |key, value| save_content(key, value) end update_properties(properties) self end
Update the metadata properties of this resource. The props
will be merged with any existing properties. Nested hashes in the properties will also be merged.
@param props [Hash] the properties to add @return [Gemstash::Resource] self for chaining purposes
# File lib/gemstash/storage.rb, line 198 def update_properties(props) load_properties(force: true) deep_merge = proc do |_, old_value, new_value| if old_value.is_a?(Hash) && new_value.is_a?(Hash) old_value.merge(new_value, &deep_merge) else new_value end end props = properties.merge(props || {}, &deep_merge) save_properties(properties.merge(props || {})) self end
Private Instance Methods
# File lib/gemstash/storage.rb, line 288 def check_resource_version version = @properties[:gemstash_resource_version] return if version <= Gemstash::Resource::VERSION reset raise Gemstash::Resource::VersionTooNew.new(name, folder, version) end
# File lib/gemstash/storage.rb, line 301 def content? return false unless Dir.exist?(@folder) entries = Dir.entries(@folder).reject {|file| file =~ /\A\.\.?\z/ || file == "properties.yaml" } !entries.empty? end
# File lib/gemstash/storage.rb, line 339 def content_filename(key) name = sanitize(key.to_s) raise "Invalid content key #{key.inspect}" if name.empty? File.join(@folder, name) end
# File lib/gemstash/storage.rb, line 272 def load(key) raise "Resource #{@name} has no #{key.inspect} content to load" unless exist?(key) load_properties # Ensures storage version is checked @content ||= {} @content[key] = read_file(content_filename(key)) end
# File lib/gemstash/storage.rb, line 280 def load_properties(force: false) return if @properties && !force return unless File.exist?(properties_filename) @properties = YAML.safe_load_file(properties_filename, permitted_classes: [Symbol]) || {} check_resource_version end
# File lib/gemstash/storage.rb, line 346 def properties_filename File.join(@folder, "properties.yaml") end
# File lib/gemstash/storage.rb, line 335 def read_file(filename) File.open(filename, "rb", &:read) end
# File lib/gemstash/storage.rb, line 296 def reset @content = nil @properties = nil end
# File lib/gemstash/storage.rb, line 308 def sanitize(name) name.gsub(/[^a-zA-Z0-9_]/, "_") end
# File lib/gemstash/storage.rb, line 312 def save_content(key, content) store(content_filename(key), content) @content ||= {} @content[key] = content end
# File lib/gemstash/storage.rb, line 330 def save_file(filename) content = yield gemstash_env.atomic_write(filename) {|f| f.write(content) } end
# File lib/gemstash/storage.rb, line 318 def save_properties(props) props ||= {} props = { gemstash_resource_version: Gemstash::Resource::VERSION }.merge(props) store(properties_filename, props.to_yaml) @properties = props end
# File lib/gemstash/storage.rb, line 325 def store(filename, content) FileUtils.mkpath(@folder) unless Dir.exist?(@folder) save_file(filename) { content } end