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