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.
- checksums.yaml +4 -4
- data/data/sgfa_web.css +37 -34
- data/lib/sgfa/binder.rb +205 -3
- data/lib/sgfa/binder_fs.rb +55 -20
- data/lib/sgfa/cli/binder.rb +248 -31
- data/lib/sgfa/cli/jacket.rb +206 -0
- data/lib/sgfa/entry.rb +96 -0
- data/lib/sgfa/error.rb +2 -2
- data/lib/sgfa/jacket.rb +314 -9
- data/lib/sgfa/jacket_fs.rb +18 -4
- data/lib/sgfa/state_fs.rb +33 -9
- data/lib/sgfa/store_s3.rb +119 -0
- data/lib/sgfa/web/base.rb +7 -0
- data/lib/sgfa/web/binder.rb +294 -85
- metadata +4 -3
data/lib/sgfa/entry.rb
CHANGED
@@ -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
|
#####################################
|
data/lib/sgfa/error.rb
CHANGED
@@ -38,11 +38,11 @@ module Error
|
|
38
38
|
|
39
39
|
# size
|
40
40
|
size = str.size
|
41
|
-
if
|
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
|
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
|
data/lib/sgfa/jacket.rb
CHANGED
@@ -289,15 +289,8 @@ class Jacket
|
|
289
289
|
|
290
290
|
elst.each do |enum|
|
291
291
|
rnum = @state.get(enum)
|
292
|
-
|
293
|
-
|
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
|