class Sgfa::StateFs

Maintains {Jacket} state using filesystem storage.

This stores the current {History} number for a {Jacket} as well as the current revision numbers for each {Entry}. It also maintains for each tag, a date/time ordered list of all {Entry} items where the current revision contains that tag. Finally, it keeps a list of all tags which are present in the {Jacket}.

The current revision of each entry is stored in a file named “_state” with each line consisting of a 9-digit current revision (zero padded) followed by newline. The current {History} number is stored on line zero, with each {Entry} on the corresponding line.

The list of tags is in a file named “_list” and consists of each tag followed by a newline.

Each tag is kept in a seperate file, named with the tag name. Each line consists of a UTC date/time in “YYYY-MM-DD HH:MM:SS” format, followed by a space, and the 9-digit (zero padded) {Entry} number, with a newline. A “_all” tag tracks all {Entry}s.

Constants

EntrySize

Size (bytes) of each entry revision

TagAll

Tag which has all {Entry} items in a {Jacket}

TagList

File name to store list of all tags

TagReg

Regular expression to get date/time string and entry number from a tag file

TagSize

Size (bytes) of each tag date/time and entry listing

TagState

File name used to store the current {History} and {Entry} revision numbers

Public Class Methods

create(path) click to toggle source

Create a new state

@param path [String] Path to the directory containing the state @raise [Error::Conflict] if path already exists

# File lib/sgfa/state_fs.rb, line 75
def self.create(path)
  begin
    Dir.mkdir(path)
  rescue Errno::EEXIST
    raise Error::Conflict, 'State path already exists'
  end
  FileUtils.touch(File.join(path, TagList))
  File.open(File.join(path, TagState), 'wb'){|fi| fi.write "000000000\n"}
end
new(path=nil) click to toggle source

Initialize new state, optionally opening

@param path [String] Path to the directory containing the state @raise (see open)

# File lib/sgfa/state_fs.rb, line 91
def initialize(path=nil)
  open(path) if path
end

Public Instance Methods

close() click to toggle source

Close the state

@raise [Error::Sanity] if state is not open @return [StateFs] self

# File lib/sgfa/state_fs.rb, line 119
def close
  raise Error::Sanity, 'State not open' if !@path
  @file.close
  @path = nil
  return self
end
get(enum) click to toggle source

Get current revision number for an entry

@param enum [Integer] Entry number @return [Integer] Current revision number @raise [Error::Sanity] if state not open @raise [ArgumentError] if enum is negative @raise [Error::NonExistent] if entry number does not exist

# File lib/sgfa/state_fs.rb, line 175
def get(enum)
  raise Error::Sanity, 'State not open' if !@path
  raise ArgumentError, 'Invalid entry number' if enum < 0
  
  @file.seek(enum*EntrySize, IO::SEEK_SET)
  res = @file.read(EntrySize)
  raise Error::NonExistent, 'Entry does not exist' if !res || res[0] == "\x00"
  return res.to_i
end
list() click to toggle source

Read list of tags

@return [Array] List of strings containing tag names @raise [Error::Sanity] if state not open @raise [Error::Corrupt] if tag list is missing

# File lib/sgfa/state_fs.rb, line 192
def list
  tagh, max = _list
  return tagh.keys
end
open(path) click to toggle source

Open the state

@param path [String] Path to the directory containing the state @raise [Error::NonExistent] if path does not exist @return [StateFs] self

# File lib/sgfa/state_fs.rb, line 102
def open(path)
  fn = File.join(path, TagState)
  begin
    @file = File.open(fn, 'r+b')
  rescue Errno::ENOENT
    raise Error::NonExistent, 'State path does not exist'
  end
  @path = path.dup
  return self
end
reset() click to toggle source

Reset to empty state

@raise [Error::Sanity] if state is not open @return (see open)

# File lib/sgfa/state_fs.rb, line 132
def reset
  raise Error::Sanity, 'State not open' if !@path

  path = @path
  self.close

  FileUtils.remove_dir(path)
  Dir.mkdir(path)
  FileUtils.touch(File.join(path, TagList))
  File.open(File.join(path, TagState), 'wb'){|fi| fi.write "000000000\n"}
  
  return open(path)
end
set(enum, rnum) click to toggle source

Set current revision number for an entry

@param enum [Integer] Entry number @param rnum [Integer] Current revision number @raise [Error::Sanity] if state is not open @raise [ArgumentError] if enum or rnum is negative @return [StateFs] self

# File lib/sgfa/state_fs.rb, line 155
def set(enum, rnum)
  raise Error::Sanity, 'State not open' if !@path
  if enum < 0 || rnum < 0
    raise ArgumentError, 'Invalid entry/revision numbers'
  end

  @file.seek(enum*EntrySize, IO::SEEK_SET)
  @file.write("%09d\n" % rnum)
  return self
end
tag(name, offs, max) click to toggle source

Read entry numbers from a tag

@param name [String] Tag name @param offs [Integer] Offset to begin reading @param max [Integer] Maximum number of entries to return @raise [Error::Sanity] if state not open @raise [Error::NonExistent] if tag does not exist @raise [Error::Corrupt] if file format is bad @return [Array] Total number of entries in the tag, and possibly empty

array of entry numbers.
# File lib/sgfa/state_fs.rb, line 237
def tag(name, offs, max)
  raise Error::Sanity, 'State not open' if !@path

  tagh, tagm = _list
  raise Error::NonExistent, 'Tag does not exist' if !tagh[name]
  fn = File.join(@path, tagh[name].to_s)
  begin
    fi = File.open(fn, 'rb')
  rescue Errno::ENOENT
    raise Error::Corrupt, 'Tag file missing'
  end

  ents = []
  size = nil
  begin
    size = fi.size / TagSize
    return [size, ents] if( offs > size || max == 0 )
    num = (offs+max > size) ? (size - offs) : max
    fi.seek(((size-offs-num)*TagSize), IO::SEEK_SET)
    num.times do
      ln = fi.read(TagSize)
      ma = TagReg.match(ln)
      raise Error::Corrupt, 'Bad tag format' unless ma
      ents.push( ma[2].to_i )
    end
  ensure
    fi.close
  end

  return [size, ents]
end
update(cng) click to toggle source

Update tags based on new {History}

@param cng [Hash] Changes in the format returned by {History#next} @raise [Error::Sanity] if state not open @raise [Error::Corrupt] if file format is bad @raise [Error::Corrupt] if list of tags is missing @raise [Error::Corrupt] if existing tag is missing @return [StateFs] self

# File lib/sgfa/state_fs.rb, line 279
def update(cng)

  # read list of tags
  changed = false
  thash, max = _list

  cng.each do |tag, hc|
    cnt = 0

    # sorted changes
    se = hc.to_a.select{|en, ti| ti }.sort{|aa, bb| aa[1] <=> bb[1] }

    # files
    if thash[tag]
      fn = File.join(@path, thash[tag].to_s)
      begin
        oldf = File.open(fn, 'rb')
      rescue Errno::ENOENT
        raise Error::Corrupt, 'Existing tag is missing'
      end
    else
      fn = File.join(@path, (max + 1).to_s)
      oldf = nil
    end
    newf = Tempfile.new('state', @path, :encoding => 'ASCII-8BIT')

    # merge new into old file
    while oldf && ln = oldf.read(TagSize)
      unless ma = TagReg.match(ln)
        newf.close!
        raise Error::Corrupt, 'Bad tag format'
      end

      tme = ma[1]
      enum = ma[2].to_i

      next if hc.has_key?(enum)

      while se.size > 0 && tme > se[0][1]
        ne, nt = se.shift
        newf.write("%s %09d\n" % [nt, ne])
        cnt += 1
      end

      newf.write(ln)
      cnt += 1
    end
    oldf.close() if oldf

    # write out any remaining new
    while ary = se.shift
      ne, nt = ary
      newf.write("%s %09d\n" % [nt, ne])
      cnt += 1
    end

    # adjust files
    if cnt == 0
      if oldf
        File.unlink(fn)
        thash.delete(tag)
        changed = true
      end
    else
      FileUtils.ln(newf.path, fn, :force => true)
      if !oldf
        max += 1
        thash[tag] = max
        changed = true
      end
    end
    newf.close!
  end

  # write list of tags
  if changed
    fnl = File.join(@path, TagList)
    File.open(fnl, 'w', :encoding => 'utf-8') do |fi|
      thash.each{|tag, num| fi.puts '%09d %s' % [num, tag] }
    end
  end

  return self
end

Private Instance Methods

_list() click to toggle source

Read raw tag list return [Array] [max_tagn, tag_hash]

# File lib/sgfa/state_fs.rb, line 201
def _list
  raise Error::Sanity, 'State not open' if !@path

  ftn = File.join(@path, TagList)
  begin
    txt = File.read(ftn)
  rescue Errno::ENOENT
    raise Error::Corrupt, 'Unable to read tag list'
  end

  tagh = {}
  max = 0
  txt.lines.each do |ln|
    ma = /^(\d{9}) (.*)$/.match(ln)
    raise Error::Corrupt, 'Tag list format incorrect' if !ma
    num = ma[1].to_i
    tagh[ma[2]] = num
    max = num if num > max
  end

  return tagh, max
end