class Sgfa::Binder

The basic administrative unit in the Sgfa system. A binder provides access control for a collection of {Jacket}s. In addition it allows values to be set for the Binder itself, to use in managing a collection of Binders.

Constants

LimJacketInv

Invalid chars in jacket name

LimJacketMax

Maximum characters in jacket name

LimPermInv

Invalid chars in permission

LimPermMax

Max chars in permission

LimSettingInv

Invalid chars in value setting

LimSettingMax

Max chars in value setting

LimTitleInv

Invalid chars in jacket title

LimTitleMax

Max characters in jacket title

LimValueInv

Invalid chars in value name

LimValueMax

Max chars in value name

Public Class Methods

limits_create(info) click to toggle source

Limits check, create values

# File lib/sgfa/binder.rb, line 117
def self.limits_create(info)
  if !info.is_a?(Hash)
    raise Error::Limits, 'Binder create info is not a hash'
  end

  if !info[:jackets].is_a?(Array)
    raise Error::Limits, 'Binder create info :jackets is not an array'
  end
  info[:jackets].each do |jin|
    if !jin.is_a?(Hash)
      raise Error::Limits, 'Binder create info jacket not a hash'
    end      
    Binder.limits_jacket(jin[:name])
    Binder.limits_title(jin[:title])
    Binder.limits_perms(jin[:perms])
  end

  if !info[:users].is_a?(Array)
    raise Error::Limits, 'Binder create info :users is not an array'
  end
  info[:users].each do |uin|
    if !uin.is_a?(Hash)
      raise Error::Limits, 'Binder create info user not a hash'
    end      
    History.limits_user(uin[:name])
    Binder.limits_perms(uin[:perms])
  end
  
  if !info[:values].is_a?(Array)
    raise Error::Limits, 'Binder create info :values is not an array'
  end
  info[:values].each do |ary|
    if !ary.is_a?(Array)
      raise Error::Limits, 'Binder create info :value items are not arrays'
    end
    vn, vs = ary
    Binder.limits_value(vn)
    Binder.limits_setting(vs)
  end

  Jacket.limits_id(info[:id_text]) if info[:id_text]
end
limits_jacket(str) click to toggle source

Limits checks, jacket name

# File lib/sgfa/binder.rb, line 43
def self.limits_jacket(str)
  Error.limits(str, 1, LimJacketMax, LimJacketInv, 'Jacket name')
end
limits_perms(ary) click to toggle source

Limits checks, permission array

# File lib/sgfa/binder.rb, line 73
def self.limits_perms(ary)
  if !ary.is_a?(Array)
    raise Error::Limits, 'Permission array required' 
  end
  ary.each do |prm|
    Error.limits(prm, 1, LimPermMax, LimPermInv, 'Permission')
  end
end
limits_setting(str) click to toggle source

Limits checks, value setting

# File lib/sgfa/binder.rb, line 110
def self.limits_setting(str)
  Error.limits(str, 1, LimSettingMax, LimSettingInv, 'Value setting')
end
limits_title(str) click to toggle source

Limits checks, jacket title

# File lib/sgfa/binder.rb, line 58
def self.limits_title(str)
  Error.limits(str, 1, LimTitleMax, LimTitleInv, 'Jacket title')
end
limits_value(str) click to toggle source

Limits checks, value name

# File lib/sgfa/binder.rb, line 93
def self.limits_value(str)
  stc = str.is_a?(Symbol) ? str.to_s : str
  Error.limits(stc, 1, LimValueMax, LimValueInv, 'Value name')
end

Public Instance Methods

backup_pull(bsto, opts={}) click to toggle source

Pull from backup store

@param bsto [Store] Backup store @param opts [Hash] Options @option opts [Hash] :log The log. Defaults to SDTERR at warn level

# File lib/sgfa/binder.rb, line 476
def backup_pull(bsto, opts={})

  log = opts[:log]
  if !log
    log = Logger.new(STDERR)
    log.level = Logger::WARN
  end

  @lock.do_ex do

    # control jacket
    ctl = _jacket_open(0)
    begin
      log.info('Backup pull control %s' % @id_hash)
      ctl.restore(bsto, log: log)
      log.info('Backup pull update cache state')
      _update(ctl)
    ensure
      ctl.close()
    end
    _cache_write()

    # all other jackets
    @jackets.values.each do |info|
      begin
        jck = _jacket_open(info[:num])
      rescue Error::NonExistent
        log.info('Backup pull create jacket %s' % info[:id_hash])
        _jacket_create_raw(info)
        jck = _jacket_open(info[:num])
      end
      begin
        log.info('Backup pull jacket %s' % info[:id_hash])
        jck.restore(bsto, log: log)
      ensure
        jck.close
      end
    end

  end 

end
backup_push(bsto, opts={}) click to toggle source

Push to a backup store

@param bsto [Store] Backup store @param opts [Hash] Options @option opts [Hash] :log The log. Defaults to STDERR at warn level. @option opts [Hash] :prev Jacket id_hash to previously pushed max history @return [Hash] Jacket id_hash to max history backed up

# File lib/sgfa/binder.rb, line 429
def backup_push(bsto, opts={})

  stat = {}
  prev = opts[:prev] || {}
  log = opts[:log]
  if !log
    log = Logger.new(STDERR)
    log.level = Logger::WARN
  end
  
  # control jacket push
  jcks = nil
  _shared do
    ctl = _jacket_open(0)
    begin
      min = (prev[@id_hash] || 0) + 1
      log.info('Backup push control %s at %d' % [@id_hash, min])
      stat[@id_hash] = ctl.backup(bsto, min_history: min, log: log)
    ensure
      ctl.close
    end
    jcks = @jackets.values
  end

  # all other jackets
  jcks.each do |info|
    jck = _jacket_open(info[:num])
    begin
      id = info[:id_hash]
      min = (prev[id] || 0) + 1
      log.info('Backup push jacket %s at %d' % [id, min])
      stat[id] = jck.backup(bsto, min_history: min, log: log)
    ensure
      jck.close
    end
  end

  return stat
end
binder_info(tr) click to toggle source

Get info

@param tr (see jacket_create) @return [Hash] Containing :id_hash, :id_text, :jackets, :values, :users

# File lib/sgfa/binder.rb, line 254
def binder_info(tr)
  _shared do
    _perms(tr, ['info'])    
    {
      :id_hash => @id_hash.dup,
      :id_text => @id_text.dup,
      :values => @values,
      :jackets => @jackets,
      :users => @users,
    }
  end
end
binder_user(tr, user, perms) click to toggle source

Set user or group permissions

@param tr (see jacket_create) @param perms [Array] New user/group permissions

# File lib/sgfa/binder.rb, line 220
def binder_user(tr, user, perms)
  History.limits_user(user)
  Binder.limits_perms(perms)
  _control(tr) do |jck|
    ent = _control_user(tr, user, perms)
    jck.write(tr[:user], [ent])
    @users
  end
end
binder_values(tr, vals) click to toggle source

Set values

@param tr (see jacket_create) @param vals [Hash] New values

# File lib/sgfa/binder.rb, line 236
def binder_values(tr, vals)
  vals.each do |vn, vs|
    Binder.limits_value(vn)
    Binder.limits_setting(vs)
  end
  _control(tr) do |jck|
    ent = _control_values(tr, vals)
    jck.write(tr[:user], [ent])
    @values
  end
end
jacket_create(tr, title, perms) click to toggle source

Create a jacket

@param tr [Hash] Common transaction info @option tr [String] :jacket Jacket name @option tr [String] :user User name @option tr [Array] :groups List of groups to which :user belongs @option tr [String] :title Title of the entry @option tr [String] :body Body of the entry @param title [String] Title of the jacket @param perms [Array] Permissions for the jacket

# File lib/sgfa/binder.rb, line 176
def jacket_create(tr, title, perms)
  Binder.limits_title(title)
  Binder.limits_perms(perms)
  _control(tr) do |jck|
    _perms(tr, ['manage'])
    num = @jackets.size + 1
    id_text, id_hash = _jacket_create(num)
    ent = _control_jacket(tr, num, tr[:jacket], id_hash, id_text,
      title, perms)
    jck.write(tr[:user], [ent])
    @jackets
  end
end
jacket_edit(tr, name, title, perms) click to toggle source

Edit a jacket

@param tr (see jacket_create) @param name [String] New jacket name @param title [String] New jacket title @param perms [Array] New jacket permissions

# File lib/sgfa/binder.rb, line 198
def jacket_edit(tr, name, title, perms)
  Binder.limits_jacket(name)
  Binder.limits_title(title)
  Binder.limits_perms(perms)
  _control(tr) do |jck|
    jnam = tr[:jacket]
    raise Error::NonExistent, 'Jacket does not exist' if !@jackets[jnam]
    jacket = @jackets[jnam]
    num = jacket['num']
    ent = _control_jacket(tr, num, name, jacket['id_hash'],
      jacket['id_text'], title, perms)
    jck.write(tr[:user], [ent])
    @jackets
  end
end
read_attach(tr, enum, anum, hnum) click to toggle source

Read an attachment

@param tr (see jacket_create) @param enum [Integer] Entry number @param anum [Integer] Attachment number @param hnum [Integer] History number @return [File] Attachment

# File lib/sgfa/binder.rb, line 381
def read_attach(tr, enum, anum, hnum)
  _jacket(tr, 'read') do |jck|
    cur = jck.read_entry(enum, 0)
    pl = cur.perms
    _perms(tr, pl) if !pl.empty?
    jck.read_attach(enum, anum, hnum)
  end
end
read_entry(tr, enum, rnum=0) click to toggle source

Read an entry

@param tr (see jacket_create) @param enum [Integer] Entry number @param rnum [Integer] Revision number @return [Entry] the Requested entry

# File lib/sgfa/binder.rb, line 348
def read_entry(tr, enum, rnum=0)
  _jacket(tr, 'read') do |jck|
    cur = jck.read_entry(enum, 0)
    pl = cur.perms
    _perms(tr, pl) if !pl.empty?
    if rnum == 0
      cur
    else
      jck.read_entry(enum, rnum)
    end
  end
end
read_history(tr, hnum) click to toggle source

Read a history item

@param tr (see jacket_create) @param hnum [Integer] History number @return [History] History item requested

# File lib/sgfa/binder.rb, line 368
def read_history(tr, hnum)
  _jacket(tr, 'info'){|jck| jck.read_history(hnum) }
end
read_list(tr) click to toggle source

Read list of tags

@param tr (see jacket_create)

# File lib/sgfa/binder.rb, line 276
def read_list(tr)
  _jacket(tr, 'info'){|jck| jck.read_list }
end
read_log(tr, offs, max) click to toggle source

Read history log

@param tr (see jacket_create) @param offs [Integer] Offset to begin reading @param max [Integer] Maximum number of histories to read

# File lib/sgfa/binder.rb, line 322
def read_log(tr, offs, max)
  _jacket(tr, 'info') do |jck|
    cur = jck.read_history()
    hmax = cur ? cur.history : 0
    start = (offs <= hmax) ? hmax - offs : 0
    stop = (start - max > 0) ? (start - (max-1)) : 1
    ary = []
    if start != 0
      start.downto(stop) do |hnum|
        hst = jck.read_history(hnum)
        ary.push [hst.history, hst.time, hst.user, hst.entries.size,
          hst.attachments.size]
      end
    end
    [hmax, ary]
  end
end
read_tag(tr, tag, offs, max, opts={}) click to toggle source

Read a tag

@param tr (see jacket_create) @param tag [String] Tag name @param offs [Integer] Offset to begin reading @param max [Integer] Maximum number of entries to read @param opts [Hash] Options hash @option opts [Boolean] :raw Get the raw entry when permissions allow @return [Array] [ enum, rnum, hnum, time, title, num_tags, num_attach,

\] Or, if opts[:raw], each Array item will consist of the Entry or,
if user does not have read permission, the info array.
# File lib/sgfa/binder.rb, line 293
def read_tag(tr, tag, offs, max, opts={})
  raw = opts[:raw]
  _jacket(tr, 'read') do |jck|
    size, ents = jck.read_tag(tag, offs, max)
    lst = ents.map do |ent|
      if raw
        pl = ent.perms
        begin
          _perms(tr, pl)
          item = ent
        rescue Error::Permission
          item = nil
        end
      end
      item ||= [ent.entry, ent.revision, ent.history, ent.time, ent.title,
        ent.tags.size, ent.attachments.size]
      item
    end
    [size, lst]
  end
end
write(tr, ents) click to toggle source

Write entries

@param tr (see jacket_create) @param ents [Array] List of entries to write @return (see Jacket#write) @raise [Error::Permission] if user lacks require permissions @raise [Error::Conflict] if entry revision is not one up from current

# File lib/sgfa/binder.rb, line 399
def write(tr, ents)
  olde = ents.select{|ent| ent.entry }
  enums = olde.map{|ent| ent.entry }
  _jacket(tr, 'write') do |jck|
    cur = jck.read_array(enums)
    pl = []
    cur.each{|ent| pl.concat ent.perms }
    _perms(tr, pl) if !pl.empty?
    enums.each_index do |idx|
      if cur[idx].revision + 1 != olde[idx].revision
        raise Error::Conflict, 'Entry revision conflict'
      end
    end
    jck.write(tr[:user], ents)
  end
end

Private Instance Methods

_cache_clear() click to toggle source

Clear cache

# File lib/sgfa/binder.rb, line 821
def _cache_clear
  @jackets = nil
  @users = nil
  @values = nil
end
_control(tr) { |ctl| ... } click to toggle source

Edit control jacket

@param tr (see jacket_create)

# File lib/sgfa/binder.rb, line 714
def _control(tr)
  ret = nil
  @lock.do_ex do
    _cache_read()
    _perms(tr, ['manage'])
    begin
      ctl = _jacket_open(0)
      begin
        ret = yield(ctl)
      ensure
        ctl.close()
      end
      _cache_write()
    ensure
      _cache_clear()
    end
  end # @lock_do
  return ret
end
_control_jacket(tr, num, name, id_hash, id_text, title, perms) click to toggle source

Set jacket info

# File lib/sgfa/binder.rb, line 753
def _control_jacket(tr, num, name, id_hash, id_text, title, perms)
  info = {
    num: num,
    name: name,
    id_hash: id_hash,
    id_text: id_text,
    title: title,
    perms: perms,
  }
  json = JSON.pretty_generate(info)

  ent = Entry.new
  ent.tag( 'jacket: %d' % num )
  ent.title = tr[:title]
  ent.body = tr[:body] + "\n" + json + "\n"

  @jackets.delete(tr[:jacket])
  @jackets[name] = info
  
  return ent
end
_control_user(tr, user, perms) click to toggle source

Set user permissions in the control jacket

# File lib/sgfa/binder.rb, line 778
def _control_user(tr, user, perms)
  info = {
    name: user,
    perms: perms,
  }
  json = JSON.pretty_generate(info)

  ent = Entry.new
  ent.tag( 'user: %s' % user )
  ent.title = tr[:title]
  ent.body = tr[:body] + "\n" + json + "\n"

  @users[user.dup] = perms.map{|pr| pr.dup }

  return ent
end
_control_values(tr, vals) click to toggle source

Set binder values in the control jacket

# File lib/sgfa/binder.rb, line 798
def _control_values(tr, vals)
  json = JSON.pretty_generate(vals)

  ent = Entry.new
  ent.tag( 'values' )
  ent.title = tr[:title]
  ent.body = tr[:body] + "\n" + json + "\n"

  vals.each do |val, sta|
    vas = val.is_a?(Symbol) ? val : val.to_s
    if sta
      @values[val] = sta
    else
      @values.delete(val)
    end
  end

  return ent
end
_create(ctl, tr, info) click to toggle source

Shared creation stuff

@param ctl [Jacket] Control jacket @param tr (see jacket_create) @param info [Hash] New binder creation options @option info [Array] :jackets List of jackets [name, title, perms] @option info [Array] :users List of users [name, perms] @option info [Hash] :values List of values name => setting

# File lib/sgfa/binder.rb, line 615
def _create(ctl, tr, info)

  # check all the values are okay
  History.limits_user(tr[:user])
  Entry.limits_title(tr[:title])
  Entry.limits_body(tr[:body])
  Binder.limits_create(info)
  
  ents = []

  # jackets
  num = 0
  info[:jackets].each do |jin|
    num += 1
    trj = {
      :title => 'Create binder initial jacket \'%s\'' % jin[:name],
      :jacket => jin[:name],
      :body => "Create binder initial jacket\n\n",
    }
    id_text, id_hash = _jacket_create(num)
    ents.push _control_jacket(trj, num, jin[:name], id_hash, id_text,
      jin[:title], jin[:perms])
  end

  # users
  info[:users].each do |uin|
    tru = {
      :title => 'Create binder initial user \'%s\'' % uin[:name],
      :body => "Create binder initial user\n\n",
    }
    ents.push _control_user(tr, uin[:name], uin[:perms])
  end

  # values
  ents.push _control_values(tr, info[:values])
  ctl.write(tr[:user], ents)
end
_get_json(ent) click to toggle source

Get json from a body

# File lib/sgfa/binder.rb, line 587
def _get_json(ent)
  lines = ent.body.lines
  st = lines.index{|li| li[0] == '{' || li[0] == '[' }
  if !st || (lines[-1][0] != '}' && lines[-1][0] != ']')
    puts ent.body.inspect
    raise Error::Corrupt, 'Control jacket entry does not contain JSON'
  end 
  json = lines[st..-1].join

  info = nil
  begin
    info = JSON.parse(json)
  rescue
    raise Error::Corrupt, 'Control jacket entry JSON parse error'
  end
  return info
end
_jacket(tr, perm) { |jck| ... } click to toggle source

Access a jacket

@param tr (see jacket_create) @param perm [String] Basic permission needed (write, read, info)

# File lib/sgfa/binder.rb, line 692
def _jacket(tr, perm)
  ret = nil
  _shared do
    jnam = tr[:jacket]
    raise Error::NonExistent, 'Jacket does not exist' if !@jackets[jnam]
    pl = [perm].concat @jackets[jnam][:perms]
    _perms(tr, pl)
    jck = _jacket_open(@jackets[jnam][:num])
    begin
      ret = yield(jck)
    ensure
      jck.close
    end
  end
  return ret
end
_perms(tr, plst) click to toggle source

Permission check

@param tr (see jacket_create) @param plst [Array] Permissions required @raise [Error::Permissions] if require permissions not met

# File lib/sgfa/binder.rb, line 660
def _perms(tr, plst)
  if tr[:perms]
    usr_has = tr[:perms]
  else
    usr = tr[:user]
    grp = tr[:groups]
    usr_has = []
    usr_has.concat(@users[usr]) if @users[usr]
    grp.each{|gr| usr_has.concat(@users[gr]) if @users[gr] }
    if usr_has.include?('write')
      usr_has.concat ['read', 'info']
    elsif usr_has.include?('read') || usr_has.include?('manage')
      usr_has.push 'info'
    end
    usr_has.uniq!
    tr[:perms] = usr_has
  end

  miss = []
  plst.each{|pr| miss.push(pr) if !usr_has.include?(pr) }

  if !miss.empty?
    raise Error::Permission, 'User lacks permission(s): ' + miss.join(', ')
  end
end
_shared() { || ... } click to toggle source

Shared access to the binder

# File lib/sgfa/binder.rb, line 737
def _shared
  ret = nil
  @lock.do_sh do
    _cache_read()
    begin
      ret = yield
    ensure
      _cache_clear()
    end
  end # @lock.do_sh
  return ret
end
_update(ctl) click to toggle source

Update cache

# File lib/sgfa/binder.rb, line 525
def _update(ctl)

  values = {}
  users = {}
  jackets = {}

  ctl.read_list.each do |tag|

    # values
    if tag == 'values'

      # process all values entries
      offs = 0
      while true
        size, ary = ctl.read_tag(tag, offs, 2)
        break if ary.empty?
        ary.each do |ent|
          info = _get_json(ent)
          info.each do |val, sta|
            next if values.has_key?(val)
            values[val] = sta
          end
        end
        offs += 2
      end

      # clear unset values
      values.delete_if{ |val, sta| !sta.is_a?(String) }

    # jacket
    elsif /^jacket:/.match(tag)
      size, ary = ctl.read_tag(tag, 0, 1)
      info = _get_json(ary.first)
      jackets[info['name']] = {
        num: info['num'],
        name: info['name'],
        id_hash: info['id_hash'],
        id_text: info['id_text'],
        title: info['title'],
        perms: info['perms'],
      }

    # user
    elsif /^user:/.match(tag)
      size, ary = ctl.read_tag(tag, 0, 1)
      info = _get_json(ary.first)
      name = info['name']
      perms = info['perms']
      users[name] = perms

    end
  end

  @users = users
  @values = values
  @jackets = jackets

end