bio-miga/miga

View on GitHub
lib/miga/metadata.rb

Summary

Maintainability
A
25 mins
Test Coverage
A
95%
# @package MiGA
# @license Artistic-2.0

##
# Metadata associated to objects like MiGA::Project, MiGA::Dataset, and
# MiGA::Result.
class MiGA::Metadata < MiGA::MiGA
  # Class-level

  ##
  # Does the metadata described in +path+ already exist?
  def self.exist?(path) File.exist? path end

  ##
  # Load the metadata described in +path+ and return MiGA::Metadata if it
  # exists, or nil otherwise.
  def self.load(path)
    return nil unless Metadata.exist? path

    MiGA::Metadata.new(path)
  end

  # Instance-level

  ##
  # Path to the JSON file describing the metadata
  attr_reader :path

  ##
  # Hash (Integer) of the last saved data Hash (object)
  attr_reader :saved_hash

  ##
  # Initiate a MiGA::Metadata object with description in +path+.
  # It will create it if it doesn't exist.
  def initialize(path, defaults = {})
    @data = nil
    @path = File.absolute_path(path)
    @saved_hash = nil
    unless File.exist? path
      @data = {}
      defaults.each { |k, v| self[k] = v }
      create
    end
  end

  ##
  # Parsed data as a Hash
  def data
    self.load if @data.nil?
    @data
  end

  ##
  # Reset :created field and save the current data
  def create
    self[:created] = Time.now.to_s
    save
  end

  ##
  # Save the metadata into #path
  def save
    return if self[:never_save]
    return if !saved_hash.nil? && saved_hash == data.hash

    MiGA::MiGA.DEBUG "Metadata.save #{path}"
    path_tmp = "#{path}.tmp"
    self[:updated] = Time.now.to_s
    @saved_hash = data.hash
    json = to_json
    wait_for_lock
    FileUtils.touch(lock_file)
    File.open(path_tmp, 'w') { |ofh| ofh.puts json }

    unless File.exist?(path_tmp) && File.exist?(lock_file)
      raise "Lock-racing detected for #{path}"
    end

    File.rename(path_tmp, path)
    File.unlink(lock_file)
  end

  ##
  # Force +save+ even if nothing has changed since the last save
  # or load. However, it doesn't save if +:never_save+ is true.
  def save!
    @saved_hash = nil
    save
  end

  ##
  # (Re-)load metadata stored in #path
  def load
    wait_for_lock
    tmp = MiGA::Json.parse(path, additions: true)
    @data = {}
    tmp.each { |k, v| self[k] = v }
    @saved_hash = data.hash
  end

  ##
  # Delete file at #path
  def remove!
    MiGA.DEBUG "Metadata.remove! #{path}"
    File.unlink(path) if File.exist?(path)
    nil
  end

  ##
  # Lock file for the metadata
  def lock_file
    "#{path}.lock"
  end

  ##
  # Return the value of +k+ in #data
  def [](k)
    if k.to_s =~ /^([^:]+):(.+)$/
      data[$1.to_sym]&.fetch($2)
    else
      data[k.to_sym]
    end
  end

  ##
  # Set the value of +k+ to +v+
  def []=(k, v)
    self.load if @data.nil?
    k = k.to_sym
    return @data.delete(k) if v.nil?

    case k
    when :name
      # Protect the special field :name
      v = v.miga_name
    when :type
      # Symbolize the special field :type
      v = v.to_sym if k == :type
    end

    @data[k] = v
  end

  ##
  # Iterate +blk+ for each data with 2 arguments: key and value
  def each(&blk)
    data.each { |k, v| blk.call(k, v) }
  end

  ##
  # Time of last update
  def updated
    Time.parse(self[:updated]) unless self[:updated].nil?
  end

  ##
  # Time of creation
  def created
    Time.parse(self[:created]) unless self[:created].nil?
  end

  ##
  # Show contents in JSON format as a String
  def to_json
    MiGA::Json.generate(data)
  end

  private

  ##
  # Wait for the lock to go away
  def wait_for_lock
    sleeper = 0.0
    slept = 0.0
    while File.exist?(lock_file)
      MiGA::MiGA.DEBUG "Waiting for lock: #{lock_file}"
      sleeper += 0.1 if sleeper <= 10.0
      sleep(sleeper)
      slept += sleeper
      raise "Lock detected for over 10 minutes: #{lock_file}" if slept > 600
    end
  end
end