class Sgfa::Entry

The Entry is the basic item which is being filed in a {Jacket}.

The Entry has attributes:

Within the body, you can add stats using a special line format. See the description for {#stats}.

Constants

LimAcctInv

Invalid Stat account chars

LimAcctMax

Maximum Stat account

LimAttachInv

Invalid attachment name characters

LimAttachMax

Maximum attachment name

LimBodyInv

Invalid chars in body

LimBodyMax

Max chars in body

LimStatInv

Invalid Stat name characters

LimStatMax

Maximum Stat name

LimTagInv

Invalid chars in a tag

LimTagMax

Max chars in a tag

LimTitleInv

Invalid chars in title

LimTitleMax

Max chars in title

TimeStrReg

Regex to parse time string

Public Class Methods

limits_acct(str) click to toggle source

Limit check, stat account

# File lib/sgfa/entry.rb, line 121
def self.limits_acct(str)
  Error.limits(str, 1, LimAcctMax, LimAcctInv, 'Stat account')
end
limits_attach(str) click to toggle source

Limit check, attachment name

# File lib/sgfa/entry.rb, line 95
def self.limits_attach(str)
  Error.limits(str, 1, LimAttachMax, LimAttachInv, 'Attachment name')
end
limits_body(str) click to toggle source

Limit check, body

# File lib/sgfa/entry.rb, line 69
def self.limits_body(str)
  Error.limits(str, 1, LimBodyMax, LimBodyInv, 'Entry body')
end
limits_stat(str) click to toggle source

Limit check, stat name

# File lib/sgfa/entry.rb, line 108
def self.limits_stat(str)
  Error.limits(str, 1, LimStatMax, LimStatInv, 'Stat name')
end
limits_tag(str) click to toggle source

Limit check, tag

# File lib/sgfa/entry.rb, line 82
def self.limits_tag(str)
  Error.limits(str, 1, LimTagMax, LimTagInv, 'Tag')
end
limits_title(str) click to toggle source

Limit check, title

# File lib/sgfa/entry.rb, line 56
def self.limits_title(str)
  Error.limits(str, 1, LimTitleMax, LimTitleInv, 'Entry title')
end
new() click to toggle source

Create a new entry

# File lib/sgfa/entry.rb, line 591
def initialize
  reset
end

Public Instance Methods

attach(name, file, hash=nil) click to toggle source

Add an attachment

@note (see replace)

@param name [String] Attachment name @param file [File] Temporary file to attach @param hash [String] The SHA256 hash of the file @raise [Error::Limits] if name exceeds allowed values

# File lib/sgfa/entry.rb, line 725
def attach(name, file, hash=nil)
  Entry.limits_attach(name)
  hsto = hash ? hash.dup : Digest::SHA256.file(file.path).hexdigest
  @attach_max += 1
  @attach[@attach_max] = [0, name.dup]
  @attach_file[@attach_max] = [file, hsto]
  _change()
end
attach_max() click to toggle source

Get max attachment

@return [Integer, Boolean] The maximum attachment number, or false

if not set
# File lib/sgfa/entry.rb, line 419
def attach_max
  if @attach_max
    return @attach_max
  else
    return false
  end
end
attachments() click to toggle source

Get attached files

@return [Array] of attachment information. Each entry is an array of

\[attach_num, history_num, name\]
# File lib/sgfa/entry.rb, line 405
def attachments
  res = []
  @attach.each do |anum, ary|
    res.push [anum, ary[0], ary[1].dup]
  end
  return res
end
body() click to toggle source

Get body

@return [String, Boolean] Entry body, or false if not set

# File lib/sgfa/entry.rb, line 306
def body
  if @body
    return @body.dup
  else
    return false
  end
end
body=(bdy) click to toggle source

Set body

@param bdy [String] Entry body @raise [Error::Limits] if bdy exceeds allowed values

# File lib/sgfa/entry.rb, line 578
def body=(bdy)
  Entry.limits_body(bdy)
  @body = bdy.dup
  _change
end
canonical() click to toggle source

Generate canonical encoded string

@return [String] Canonical output @raise [Error::Sanity] if the entry is not complete enought to

generate canonical output.
# File lib/sgfa/entry.rb, line 149
def canonical
  if !@canon
    raise Error::Sanity, 'Entry not complete' if !@history

    txt =  "jckt %s\n" % @jacket
    txt << "entr %d\n" % @entry
    txt << "revn %d\n" % @revision
    txt << "hist %d\n" % @history
    txt << "amax %d\n" % @attach_max
    txt << "time %s\n" % @time_str
    txt << "titl %s\n" % @title
    @tags.sort.each{ |tag| txt << "tags %s\n" % tag }
    @attach.to_a.sort{|aa, bb| aa[0] <=> bb[0] }.each do |anum, ary|
      txt << "atch %d %d %s\n" % [anum, ary[0], ary[1]]
    end
    txt << "\n"
    txt << @body
    @canon = txt
  end
  return @canon.dup
end
canonical=(str) click to toggle source

Set entry using canonical encoding

@param str [String] Canonical encoded entry @raise [Error::Corrupt] if encoding does not follow canonical rules

# File lib/sgfa/entry.rb, line 438
def canonical=(str)
  @hash = nil
  @canon = str.dup
  lines = str.lines

  ma = /^jckt ([0-9a-f]{64})$/.match lines.shift
  raise(Error::Corrupt, 'Canonical entry jacket error') if !ma
  @jacket = ma[1]

  ma = /^entr (\d+)$/.match lines.shift
  raise(Error::Corrupt, 'Canonical entry entry error') if !ma
  @entry = ma[1].to_i
  
  ma = /^revn (\d+)$/.match lines.shift
  raise(Error::Corrupt, 'Canonical entry revision error') if !ma
  @revision = ma[1].to_i

  ma = /^hist (\d+)$/.match lines.shift
  raise(Error::Corrupt, 'Canonical entry history error') if !ma
  @history = ma[1].to_i

  ma = /^amax (\d+)$/.match lines.shift
  raise(Error::Corrupt, 'Caononical entry attach_max error') if !ma
  @attach_max = ma[1].to_i

  ma = /^time (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})$/.match lines.shift
  raise(Error::Corrupt, 'Canonical entry time error') if !ma
  @time_str = ma[1]
  @time = nil

  ma = /^titl (.*)$/.match lines.shift
  raise(Error::Corrupt, 'Canonical entry title error') if !ma
  Entry.limits_title(ma[1])
  @title = ma[1]

  li = lines.shift
  @tags = []
  while( li && ma = /^tags (.+)$/.match(li) )
    Entry.limits_tag(ma[1])
    @tags.push ma[1]
    li = lines.shift
  end

  @attach = {}
  while( li && ma = /^atch (\d+) (\d+) (.+)$/.match(li) )
    Entry.limits_attach(ma[3])
    @attach[ma[1].to_i] = [ma[2].to_i, ma[3]]
    li = lines.shift
  end

  unless li && li.strip.empty?
    raise Error::Corrupt, 'Canonical entry body error'
  end

  txt = ''
  lines.each{ |li| txt << li }
  Entry.limits_body(txt)
  @body = txt

  _final

rescue
  reset
  raise
end
delete(anum) click to toggle source

Delete an attachment

@param anum [Integer] anum Attachment number to delete @raise [Error::Sanity] if attachment does not exist

# File lib/sgfa/entry.rb, line 690
def delete(anum)
  raise(Error::Sanity, 'Non-existent attachment') if !@attach[anum]
  @attach.delete(anum)
  _change()
end
entry() click to toggle source

Get entry number

@return [Integer, Boolean] The entry number, or false if not set

# File lib/sgfa/entry.rb, line 215
def entry
  if @entry
    return @entry
  else 
    return false
  end
end
entry=(enum) click to toggle source

Set entry number @param enum [Integer] The entry number @raise [ArgumentError] if enum is negative @raise [Error::Sanity] if changing an already set entry number

# File lib/sgfa/entry.rb, line 528
def entry=(enum)
  raise(ArgumentError, 'Entry number invalid') if enum < 0
  if @entry
    raise(Error::Sanity, 'Changing entry number') if @entry != enum
  else
    @entry = enum
    _change
  end
end
hash() click to toggle source

Get entry item hash

@return [String] The hash of the entry @raise (see canonical)

# File lib/sgfa/entry.rb, line 135
def hash
  if !@hash
    @hash = Digest::SHA256.new.update(canonical).hexdigest
  end
  return @hash.dup
end
history() click to toggle source

Get history number

@return [Integer, Boolean] The history number, or false if not set

# File lib/sgfa/entry.rb, line 241
def history
  if @history
    return @history
  else
    return false
  end
end
jacket() click to toggle source

Get jacket @return [String, Boolean] The jacket hash ID or false if not set

# File lib/sgfa/entry.rb, line 202
def jacket
  if @jacket
    return @jacket.dup
  else
    return false
  end
end
jacket=(jck) click to toggle source

Set jacket

@param jck [String] The jacket hash ID @raise [Error::Limits] if jck is not a valid hash @raise [Error::Sanity] if changing an already set jacket hash ID

# File lib/sgfa/entry.rb, line 511
def jacket=(jck)
  ma = /^([0-9a-f]{64})$/.match jck
  raise(Error::Limits, 'Jacket hash not valid') if !ma
  if @jacket
    raise(Error::Sanity, 'Jacket already set') if @jacket != jck
  else
    @jacket = jck.dup
    _change
  end
end
json() click to toggle source

Generate JSON encoded string

@return [String] JSON output @raise [Error::Sanity] if the entry is not complete enought to

generate canonical output.
# File lib/sgfa/entry.rb, line 178
def json
  if !@json
    enc = {
      'hash' => hash,
      'jacket' => @jacket,
      'entry' => @entry,
      'revision' => @revision,
      'history' => @history,
      'max_attach' => @attach_max,
      'time' => @time_str,
      'title' => @title,
      'tags' => @tags.sort,
      'attachments' => @attach,
      'body' => @body,
    }
    @json = JSON.generate(enc)
  end
  return @json.dup
end
perms() click to toggle source

Get permissions

# File lib/sgfa/entry.rb, line 327
def perms
  @tags = @tags.uniq
  @tags.select{|tag| tag.start_with?('perm: ')}.map{|tag| tag[6..-1]}
end
rename(anum, name) click to toggle source

Rename an attachment

@param anum [Integer] Attachment number to rename @param name [String] New attachment name @raise [Error::Sanity] if attachment does not exist @raise [Error::Limits] if name exceeds allowed values

# File lib/sgfa/entry.rb, line 677
def rename(anum, name)
  raise(Error::Sanity, 'Non-existent attachment') if !@attach[anum]
  Entry.limits_attach(name)
  @attach[anum][1] = name.dup
  _change()
end
replace(anum, file, hash=nil) click to toggle source

Replace an attachment

@note If hash is not provided, it will be calculated. This can take

a long time for large files.

@param anum [Integer] Attachment number to replace @param file [File] Temporary file to attach @param hash [String] The SHA256 hash of the file @raise [Error::Sanity] if attachment does not exist

# File lib/sgfa/entry.rb, line 707
def replace(anum, file, hash=nil)
  raise(Error::Sanity, 'Non-existent attachment') if !@attach[anum]
  hsto = hash ? hash.dup : Digest::SHA256.file(file.path).hexdigest
  @attach[anum][0] = 0
  @attach_file[anum] = [file, hsto]
  _change()
end
reset() click to toggle source

Reset to blank entry

@return [Entry] self

# File lib/sgfa/entry.rb, line 600
def reset()
  @hash = nil
  @canon = nil
  @jacket = nil
  @entry = nil
  @revision = 1
  @history = nil
  @title = nil
  @time = nil
  @time_str = nil
  @body = nil
  @tags = []
  @attach = {}
  @attach_file = {}
  @attach_max = 0

  @time_old = nil
  @tags_old = []

  return self
end
revision() click to toggle source

Get revision number

@return [Integer, Boolean] The revision number, or false if not set

# File lib/sgfa/entry.rb, line 228
def revision
  if @revision
    return @revision
  else
    return false
  end
end
stats() click to toggle source

Get stats

Stats are stored in the body of an entry in a special format. A stat line must be in the format:

  • newline

  • '#'

  • whitespace

  • <type string> cannot include '@'

  • whitespace

  • '@'

  • whitespace

  • <value> floating point, decimal optional

  • anything beyond is a comment and is ignored

Immediately following a stat line, there may be optional account line(s) in the format:

  • newline

  • '#'

  • whitespace

  • <account string> cannot include '@'

@return [Array] of stats in the format [type, value, [account, ..]]

# File lib/sgfa/entry.rb, line 356
def stats
  return nil if !@body

  stats = []
  lines = @body.lines
  ln = lines.shift
  while ln

    # find a stat line
    ma = /^#\s+([^@]+)\s+@\s+(\d+(\.\d*)?)/.match(ln.chomp)
    ln = lines.shift
    next if !ma

    # check the stat line
    type = ma[1]
    begin
      Entry.limits_stat(type)
    rescue Error::Limits
      next
    end
    value = ma[2].to_f

    # collect accounts
    accounts = []
    while ln
      ma = /^#\s+([^@]+)\s*$/.match(ln.chomp)
      break if !ma
      acct = ma[1]
      ln = lines.shift
      begin
        Entry.limits_acct(acct)
      rescue Error::Limits
        next
      end
      accounts.push acct
    end

    stats.push [type, value, accounts]
  end

  return stats
end
tag(tnam) click to toggle source

Set tag

@param tnam [String] Tag name to set @raise [Error::Limits] if tnam exceeds allowed values

# File lib/sgfa/entry.rb, line 740
def tag(tnam)
  name = _tag_normalize(tnam)
  @tags.push name
  _change()
end
tags() click to toggle source

Get tags

@return [Array] of tag names

# File lib/sgfa/entry.rb, line 319
def tags
  @tags = @tags.uniq
  @tags.map{|tag| tag.dup }
end
time() click to toggle source

Get time

@return [Time, Boolean] Time of the entry, or false if not set

# File lib/sgfa/entry.rb, line 272
def time
  if !@time
    return false if !@time_str
    ma = TimeStrReg.match(@time_str)
    raise Error::Limits, 'Invalid time string' if !ma
    ary = ma[1,6].map{|str| str.to_i}
    begin
      @time = Time.utc(*ary)
    rescue
      raise Error::Limits, 'Invalid time string'
    end
  end
  return @time.dup
end
time=(tme) click to toggle source

Set time

@param tme [Time] Time of the entry

# File lib/sgfa/entry.rb, line 555
def time=(tme)
  @time = tme.utc
  @time_str = nil
end
time_str() click to toggle source

Get time string

@return [String, Boolean] Encoded time string of the entry, or false

if time not set
# File lib/sgfa/entry.rb, line 293
def time_str
  if !@time_str
    return false if !@time
    @time_str = @time.strftime('%F %T')
  end
  return @time_str.dup
end
time_str=(tme) click to toggle source

Set encoded time string

@param tme [String] Encoded time string @raise [Error::Limits] if tme is not properly written

# File lib/sgfa/entry.rb, line 566
def time_str=(tme)
  @time = nil
  @time_str = tme.dup
  time
end
title() click to toggle source

Get title

@return [String, Boolean] Title of the entry, or false if not set

# File lib/sgfa/entry.rb, line 254
def title
  if @title
    return @title.dup
  else
    return false
  end
end
title=(ttl) click to toggle source

Set title

@param ttl [String] Title of the entry @raise [Error::Limits] if ttl exceeds allowed values

# File lib/sgfa/entry.rb, line 544
def title=(ttl)
  Entry.limits_title(ttl)
  @title = ttl.dup
  _change
end
untag(tnam) click to toggle source

Clear tag

@param tnam [String] tnam Tag name to clear @raise [Error::Limits] if tnam exceeds allowed values

# File lib/sgfa/entry.rb, line 752
def untag(tnam)
  name = _tag_normalize(tnam)
  @tags.delete name
  _change()
end
update(hnum) click to toggle source

Update an entry

@note If time has not been set, it defaults to the current time

The changes returned include:

  • :time - true if time changed

  • :tags_add - list of new tags

  • :tags_del - list of tags deleted

  • :files - hash of attachment number => [file, hash]

@param hnum [Integer] History number @raise [Error::Sanity] if no changes have been made to an entry @raise [Error::Sanity] if entry does not have at least jacket,

entry, title, and body set

@return [Hash] Describing changes

# File lib/sgfa/entry.rb, line 639
def update(hnum)
  raise Error::Sanity, 'Update entry with no changes' if @history
  if !@jacket && !@entry && !@title && !@body
    raise Error::Sanity, 'Update incomplete entry'
  end

  @history = hnum
  change = {}

  if !@time_str
    @time = Time.new.utc if !@time
    @time_str = @time.strftime('%F %T')
  end
  change[:time] = @time_str != @time_old

  @tags = @tags.uniq
  change[:tags_add] = @tags - @tags_old
  change[:tags_del] = @tags_old - @tags

  @attach.each{ |anum, ary| ary[0] = hnum if ary[0] == 0 }
  change[:files] = @attach_file
  
  _final()
  return change
end

Private Instance Methods

_change() click to toggle source

Change to entry

# File lib/sgfa/entry.rb, line 762
def _change()
  @revision = @revision + 1 if @history
  @history = nil
  @hash = nil
  @canon = nil
end
_final() click to toggle source

Finalize entry

# File lib/sgfa/entry.rb, line 771
def _final
  @time_old = @time_str ? @time_str.dup : nil
  @tags_old = @tags.map{|tg| tg.dup }
  @attach_file = {}
end
_tag_normalize(tnam) click to toggle source

Normalize tag name

# File lib/sgfa/entry.rb, line 779
def _tag_normalize(tnam)
  idx = tnam.index(':')
  if idx
    pre = tnam[0, idx].strip
    post = tnam[idx+1..-1].strip
    return pre + ': ' + post
  else
    return tnam.strip
  end
end