sgfa 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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