sgfa 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -35,6 +35,9 @@ module Sgfa
35
35
  # * attachments - list of attached files with names (may be empty)
36
36
  # * max_attach - the maximum number of attachments ever belonging to
37
37
  # the entry
38
+ #
39
+ # Within the body, you can add stats using a special line format.
40
+ # See the description for {#stats}.
38
41
  #
39
42
  class Entry
40
43
 
@@ -94,6 +97,32 @@ class Entry
94
97
  end # def self.limits_attach()
95
98
 
96
99
 
100
+ # Maximum Stat name
101
+ LimStatMax = 64
102
+
103
+ # Invalid Stat name characters
104
+ LimStatInv = /[[:cntrl:]@]|^_/
105
+
106
+ #####################################
107
+ # Limit check, stat name
108
+ def self.limits_stat(str)
109
+ Error.limits(str, 1, LimStatMax, LimStatInv, 'Stat name')
110
+ end # def self.limits_stat()
111
+
112
+
113
+ # Maximum Stat account
114
+ LimAcctMax = 64
115
+
116
+ # Invalid Stat account chars
117
+ LimAcctInv = /[[:cntrl:]@]|^_/
118
+
119
+ #####################################
120
+ # Limit check, stat account
121
+ def self.limits_acct(str)
122
+ Error.limits(str, 1, LimAcctMax, LimAcctInv, 'Stat account')
123
+ end # def self.limits_acct()
124
+
125
+
97
126
  #########################################################
98
127
  # @!group Read attributes
99
128
 
@@ -299,6 +328,73 @@ class Entry
299
328
  @tags = @tags.uniq
300
329
  @tags.select{|tag| tag.start_with?('perm: ')}.map{|tag| tag[6..-1]}
301
330
  end # def perms
331
+
332
+
333
+ #####################################
334
+ # Get stats
335
+ #
336
+ # Stats are stored in the body of an entry in a special format.
337
+ # A stat line must be in the format:
338
+ # * newline
339
+ # * \'#\'
340
+ # * whitespace
341
+ # * <type string> cannot include \'@\'
342
+ # * whitespace
343
+ # * \'@\'
344
+ # * whitespace
345
+ # * <value> floating point, decimal optional
346
+ # * anything beyond is a comment and is ignored
347
+ #
348
+ # Immediately following a stat line, there may be optional account line(s)
349
+ # in the format:
350
+ # * newline
351
+ # * \'#\'
352
+ # * whitespace
353
+ # * <account string> cannot include \'@\'
354
+ #
355
+ # @return [Array] of stats in the format \[type, value, \[account, ..\]\]
356
+ def stats
357
+ return nil if !@body
358
+
359
+ stats = []
360
+ lines = @body.lines
361
+ ln = lines.shift
362
+ while ln
363
+
364
+ # find a stat line
365
+ ma = /^#\s+([^@]+)\s+@\s+(\d+(\.\d*)?)/.match(ln.chomp)
366
+ ln = lines.shift
367
+ next if !ma
368
+
369
+ # check the stat line
370
+ type = ma[1]
371
+ begin
372
+ Entry.limits_stat(type)
373
+ rescue Error::Limits
374
+ next
375
+ end
376
+ value = ma[2].to_f
377
+
378
+ # collect accounts
379
+ accounts = []
380
+ while ln
381
+ ma = /^#\s+([^@]+)\s*$/.match(ln.chomp)
382
+ break if !ma
383
+ acct = ma[1]
384
+ ln = lines.shift
385
+ begin
386
+ Entry.limits_acct(acct)
387
+ rescue Error::Limits
388
+ next
389
+ end
390
+ accounts.push acct
391
+ end
392
+
393
+ stats.push [type, value, accounts]
394
+ end
395
+
396
+ return stats
397
+ end # def stats
302
398
 
303
399
 
304
400
  #####################################
@@ -38,11 +38,11 @@ module Error
38
38
 
39
39
  # size
40
40
  size = str.size
41
- if str.size > max
41
+ if size > max
42
42
  raise Error::Limits, '%s length %d is too large, limit %d' %
43
43
  [desc, size, max]
44
44
  end
45
- if str.size < min
45
+ if size < min
46
46
  raise Error::Limits, '%s length %d is too small, limit %d' %
47
47
  [desc, size, min]
48
48
  end
@@ -289,15 +289,8 @@ class Jacket
289
289
 
290
290
  elst.each do |enum|
291
291
  rnum = @state.get(enum)
292
- type, item = item_entry(enum, rnum)
293
- fi = @store.read(type, item)
294
- raise Error::Corrupt, 'Jacket current entry not present' if !fi
295
- ent = Entry.new
296
- begin
297
- ent.canonical = fi.read
298
- ensure
299
- fi.close
300
- end
292
+ ent = _read_entry(enum, rnum)
293
+ raise Error::Corrupt, 'Jacket current entry not present' if !ent
301
294
  ents.push ent
302
295
  end
303
296
  end
@@ -418,6 +411,8 @@ class Jacket
418
411
  # @option opts [Logger] :log The logger to use. Defaults to STDERR log
419
412
  # @return [Boolean] true if valid history chain
420
413
  def check(opts={})
414
+ raise Error::Sanity, 'Jacket is not open' if !@id_hash
415
+
421
416
  max = opts[:max_history] || 1000000000
422
417
  min = opts[:min_history] || 1
423
418
  stop = opts[:miss_history] || 0
@@ -551,6 +546,316 @@ class Jacket
551
546
  end # def check()
552
547
 
553
548
 
549
+ #####################################
550
+ # Backup to an alternate store
551
+ #
552
+ # @param bsto [Store] Backup store
553
+ # @param opts [Hash] Options
554
+ # @option opts [Fixnum] :max_history Last history to backup. Defaults
555
+ # to the current maximum history.
556
+ # @option opts [Fixnum] :min_history First history to backup. Defaults
557
+ # to 1.
558
+ # @option opts [Boolean] :skip_history Do not push history items
559
+ # @option opts [Boolean] :skip_entry Do not push entry items
560
+ # @option opts [Boolean] :skip_attach Do not push attachments
561
+ # @option opts [Boolean] :always Do not stat item, always push
562
+ # @option opts [Logger] :log The log. Defaults to STDERR at warn level.
563
+ # @return [Fixnum] The last history backed up.
564
+ def backup(bsto, opts={})
565
+ raise Error::Sanity, 'Jacket is not open' if !@id_hash
566
+
567
+ max = opts[:max_history] || @lock.do_sh{ @state.get(0) }
568
+ min = opts[:min_history] || 1
569
+ do_h = !opts[:skip_history]
570
+ do_e = !opts[:skip_entry]
571
+ do_a = !opts[:skip_attach]
572
+ stat = !opts[:always]
573
+ if opts[:log]
574
+ log = opts[:log]
575
+ else
576
+ log = Logger.new(STDERR)
577
+ log.level = Logger::WARN
578
+ end
579
+ hst = History.new
580
+
581
+ min.upto(max) do |hnum|
582
+
583
+ # history items
584
+ type, item = item_history(hnum)
585
+ blob = @store.read(type, item)
586
+ if !blob
587
+ log.error('Backup history item missing %d' % hnum)
588
+ next
589
+ end
590
+ begin
591
+ hst.canonical = blob.read
592
+ if stat
593
+ size = bsto.size(type, item)
594
+ if size
595
+ log.info('Backup history item already exists %d' % hnum)
596
+ next
597
+ end
598
+ end
599
+ if do_h
600
+ temp = bsto.temp
601
+ blob.rewind
602
+ IO.copy_stream(blob, temp)
603
+ bsto.write(type, item, temp)
604
+ log.info('Backup push history item %d' % hnum)
605
+ end
606
+ ensure
607
+ blob.close
608
+ end
609
+
610
+ # entries
611
+ if do_e
612
+ hst.entries.each do |enum, rnum, hash|
613
+ type, item = item_entry(enum, rnum)
614
+ blob = @store.read(type, item)
615
+ if !blob
616
+ log.info('Backup entry missing %d-%d' % [enum, rnum])
617
+ next
618
+ end
619
+ begin
620
+ temp = bsto.temp
621
+ IO.copy_stream(blob, temp)
622
+ bsto.write(type, item, temp)
623
+ log.info('Backup push entry %d-%d' % [enum, rnum])
624
+ ensure
625
+ blob.close
626
+ end
627
+ end
628
+ end
629
+
630
+ # attachments
631
+ if do_a
632
+ hst.attachments.each do |enum, anum, hash|
633
+ type, item = item_attach(enum, anum, hnum)
634
+ blob = @store.read(type, item)
635
+ if !blob
636
+ log.info('Backup attachment missing %d-%d-%d' % [enum, anum, hnum])
637
+ next
638
+ end
639
+ begin
640
+ temp = bsto.temp
641
+ IO.copy_stream(blob, temp)
642
+ bsto.write(type, item, temp)
643
+ log.info('Backup push attachment %d-%d-%d' % [enum, anum, hnum])
644
+ ensure
645
+ blob.close
646
+ end
647
+ end
648
+ end
649
+
650
+ end
651
+
652
+ return max
653
+
654
+ end # def backup()
655
+
656
+
657
+ #####################################
658
+ # Backup restore from an alternate store
659
+ #
660
+ # @param bsto [Store] The backup store
661
+ # @param opts [Hash] Options
662
+ # @option opts [Fixnum] :max_history Last history to restore. Defaults to
663
+ # everything until a history item is not found.
664
+ # @option opts [Fixnum] :min_history First history to restore. Defaults to
665
+ # current maximum history plus one.
666
+ # @option opts [Boolean] :skip_entry Do not pull entry items.
667
+ # @option opts [Boolean] :skip_attach Do not pull attachments.
668
+ # @option opts [Boolean] :always Do not stat local item, always pull.
669
+ # @option opts [Logger] :log The log. Defaults to STDERR at warn level.
670
+ #
671
+ # @todo Do locking. Really restore is not going to occur with other
672
+ # processes accessing it, but...
673
+ #
674
+ def restore(bsto, opts={})
675
+ raise Error::Sanity, 'Jacket is not open' if !@id_hash
676
+
677
+ max = opts[:max_history] || 1000000000
678
+ min = opts[:min_history] || @lock.do_sh{ @state.get(0) } + 1
679
+ do_e = !opts[:skip_entry]
680
+ do_a = !opts[:skip_attach]
681
+ stat = !opts[:always]
682
+ if opts[:log]
683
+ log = opts[:log]
684
+ else
685
+ log = Logger.new(STDERR)
686
+ log.level = Logger::WARN
687
+ end
688
+ hst = History.new
689
+
690
+ miss = 0
691
+ hnum = min -1
692
+ while (hnum += 1) <= max
693
+
694
+ # history item
695
+ type, item = item_history(hnum)
696
+ if stat
697
+ size = @store.size(type, item)
698
+ if size
699
+ log.info('Restore history item already exists %d' % hnum)
700
+ next
701
+ end
702
+ end
703
+ blob = bsto.read(type, item)
704
+ if !blob
705
+ if max == 1000000000
706
+ log.debug('Restore finished at %d' % (hnum-1))
707
+ break
708
+ else
709
+ log.error('Restore history item missing %d' % hnum)
710
+ next
711
+ end
712
+ end
713
+ begin
714
+ hst.canonical = blob.read
715
+ blob.rewind
716
+ temp = @store.temp
717
+ IO.copy_stream(blob, temp)
718
+ @store.write(type, item, temp)
719
+ log.info('Restore history item %d' % hnum)
720
+ ensure
721
+ blob.close
722
+ end
723
+
724
+ # entries
725
+ if do_e
726
+ hst.entries.each do |enum, rnum, hash|
727
+ type, item = item_entry(enum, rnum)
728
+ blob = bsto.read(type, item)
729
+ if !blob
730
+ log.info('Restore entry missing %d-%d' % [enum, rnum])
731
+ next
732
+ end
733
+ begin
734
+ temp = @store.temp
735
+ IO.copy_stream(blob, temp)
736
+ @store.write(type, item, temp)
737
+ log.info('Restore entry %d-%d' % [enum, rnum])
738
+ ensure
739
+ blob.close
740
+ end
741
+ end
742
+ end
743
+
744
+ # attachments
745
+ if do_a
746
+ hst.attachments.each do |enum, anum, hash|
747
+ type, item = item_attach(enum, anum, hnum)
748
+ blob = bsto.read(type, item)
749
+ if !blob
750
+ log.info('Restore attach missing %d-%d-%d' % [enum, anum, hnum])
751
+ next
752
+ end
753
+ begin
754
+ temp = @store.temp
755
+ IO.copy_stream(blob, temp)
756
+ @store.write(type, item, temp)
757
+ log.info('Restore attach %d-%d-%d' % [enum, anum, hnum])
758
+ ensure
759
+ blob.close
760
+ end
761
+ end
762
+ end
763
+
764
+ end
765
+
766
+ # update state
767
+ update(min, hnum-1)
768
+
769
+ end # def restore()
770
+
771
+
772
+ # Number of entries to process before doing a tag state update
773
+ UpdateChunk = 250
774
+
775
+
776
+ # The tag which includes all entries
777
+ TagAll = '_all'
778
+
779
+
780
+ #####################################
781
+ # Update state
782
+ #
783
+ # @param [Fixnum] min History to start the update
784
+ # @param [Fixnum] max History to stop the update
785
+ #
786
+ def update(min, max)
787
+ raise Error::Sanity, 'Jacket is not open' if !@id_hash
788
+
789
+ # blow away state entirely
790
+ @state.reset if min <= 1
791
+
792
+ tags = {}
793
+ current = {}
794
+ count = 0
795
+ hst = History.new
796
+ max.downto(min) do |hnum|
797
+
798
+ # history
799
+ type, item = item_history(hnum)
800
+ fi = @store.read(type, item)
801
+ raise Error::Corrupt, 'Jacket history does not exist %d' % hnum if !fi
802
+ begin
803
+ hst.canonical = fi.read
804
+ ensure
805
+ fi.close
806
+ end
807
+
808
+ # entries
809
+ hst.entries.each do |enum, rnum, hash|
810
+ next if current[enum]
811
+ current[enum] = true
812
+ count += 1
813
+
814
+ # get new entry
815
+ type, item = item_entry(enum, rnum)
816
+ ent = _read_entry(enum, rnum)
817
+ if !ent
818
+ raise Error::Corrupt, 'Jacket current entry not present'
819
+ end
820
+
821
+ # update from old entry
822
+ if min > 1 && rnum >= 2
823
+ oldr = @state.get(enum)
824
+ olde = _read_entry(enum, oldr)
825
+ if !olde
826
+ raise Error::Corrupt, 'Jacket current entry not present'
827
+ end
828
+ tdel = olde.tags - ent.tags
829
+ tdel.each do |tag|
830
+ tags[tag] ||= {}
831
+ tags[tag][enum] = nil
832
+ end
833
+
834
+ end
835
+ @state.set(enum, rnum)
836
+
837
+ # update tags
838
+ tags[TagAll] ||= {}
839
+ tags[TagAll][enum] = ent.time_str
840
+ ent.tags.each do |tag|
841
+ tags[tag] ||= {}
842
+ tags[tag][enum] = ent.time_str
843
+ end
844
+ end
845
+
846
+ # tag state update
847
+ if count >= UpdateChunk || hnum == min
848
+ @state.update(tags)
849
+ tags = {}
850
+ count = 0
851
+ end
852
+ end
853
+
854
+ @state.set(0, max)
855
+
856
+ end # def update()
857
+
858
+
554
859
  end # class Jacket
555
860
 
556
861
  end # module Sgfa