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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e56f45ef9e949c37978cee9dfc394769eeb6baa2
|
4
|
+
data.tar.gz: c4be3d1955e1700dcfa59c02a75cc6259c9ed3b0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5e888580f637df355364aad7e997e1d3c548693bf584f7eaedec7fdc3cefd72b195117d9d8a87073866f545f28ad11dc7cbfe5f4f2b5348a1522feea7e68cc25
|
7
|
+
data.tar.gz: 9df09e1204ae56856b24bf30ad6075be1bbad8728ae38bfde47487f21c01c313cbbeead33cf8e07c08d61e6e57d2f1630f1ab708c15d310bfaa289a69e8f9a0d
|
data/data/sgfa_web.css
CHANGED
@@ -119,7 +119,17 @@ div#sgfa_web table.list td.hash {
|
|
119
119
|
}
|
120
120
|
|
121
121
|
|
122
|
-
/* edit
|
122
|
+
/* edit */
|
123
|
+
div#sgfa_web form.edit input {
|
124
|
+
font-family: inherit;
|
125
|
+
font-size: inherit;
|
126
|
+
}
|
127
|
+
|
128
|
+
div#sgfa_web form.edit textarea {
|
129
|
+
font-family: inherit;
|
130
|
+
font-size: inherit;
|
131
|
+
}
|
132
|
+
|
123
133
|
div#sgfa_web form.edit fieldset {
|
124
134
|
width: 98%;
|
125
135
|
margin: auto;
|
@@ -130,40 +140,12 @@ div#sgfa_web form.edit fieldset legend {
|
|
130
140
|
font-weight: bold;
|
131
141
|
}
|
132
142
|
|
133
|
-
div#sgfa_web form.edit p {
|
134
|
-
clear: both;
|
135
|
-
padding: 3px;
|
136
|
-
}
|
137
|
-
|
138
143
|
div#sgfa_web form.edit label {
|
139
144
|
float: left;
|
140
145
|
width: 8em;
|
141
146
|
text-align: right;
|
142
147
|
}
|
143
148
|
|
144
|
-
/* edit tag table */
|
145
|
-
div#sgfa_web table.edit_tag th {
|
146
|
-
text-align: center;
|
147
|
-
text-decoration: underline;
|
148
|
-
}
|
149
|
-
|
150
|
-
div#sgfa_web table.edit_tag tr {
|
151
|
-
border: thick solid red;
|
152
|
-
}
|
153
|
-
|
154
|
-
|
155
|
-
/* edit file table */
|
156
|
-
div#sgfa_web table.edit_file th {
|
157
|
-
text-align: center;
|
158
|
-
text-decoration: underline;
|
159
|
-
}
|
160
|
-
|
161
|
-
div#sgfa_web table.edit_file td.fname {
|
162
|
-
min-width: 10em;
|
163
|
-
}
|
164
|
-
|
165
|
-
|
166
|
-
/* edit form */
|
167
149
|
div#sgfa_web form.edit input[type=submit] {
|
168
150
|
border: 1px solid grey;
|
169
151
|
border-radius: 5px;
|
@@ -172,22 +154,30 @@ div#sgfa_web form.edit input[type=submit] {
|
|
172
154
|
}
|
173
155
|
|
174
156
|
div#sgfa_web form.edit textarea {
|
175
|
-
font-family: inherit;
|
176
|
-
font-size: inherit;
|
177
157
|
width: 50em;
|
178
158
|
height: 15em;
|
179
159
|
}
|
180
160
|
|
181
161
|
div#sgfa_web form.edit input.title {
|
182
|
-
font-family: inherit;
|
183
|
-
font-size: inherit;
|
184
162
|
width: 50em;
|
185
163
|
}
|
186
164
|
|
165
|
+
div#sgfa_web form.edit input.attname {
|
166
|
+
width: 30em;
|
167
|
+
}
|
168
|
+
|
187
169
|
div#sgfa_web form.edit input.tag {
|
188
|
-
width:
|
170
|
+
width: 30em;
|
189
171
|
}
|
190
172
|
|
173
|
+
div#sgfa_web form.edit button.add_row {
|
174
|
+
border: 1px solid grey;
|
175
|
+
border-radius: 5px;
|
176
|
+
padding: 2px;
|
177
|
+
background: radial-gradient(rgb(245, 245, 245), rgb(200, 200, 200));
|
178
|
+
}
|
179
|
+
|
180
|
+
|
191
181
|
|
192
182
|
/* entry */
|
193
183
|
div#sgfa_web div.entry_left {
|
@@ -195,6 +185,17 @@ div#sgfa_web div.entry_left {
|
|
195
185
|
width: 80%;
|
196
186
|
}
|
197
187
|
|
188
|
+
div#sgfa_web div.mainbody div.pages {
|
189
|
+
width: 100%;
|
190
|
+
border-top: 2px solid;
|
191
|
+
}
|
192
|
+
|
193
|
+
div#sgfa_web div.mainbody div.tagname {
|
194
|
+
width: 100%;
|
195
|
+
font-size: 150%;
|
196
|
+
border-bottom: 2px solid;
|
197
|
+
}
|
198
|
+
|
198
199
|
div#sgfa_web div.mainbody div.title {
|
199
200
|
width: 100%;
|
200
201
|
font-size: 125%;
|
@@ -225,6 +226,7 @@ div#sgfa_web div.mainbody div.sidebar div.tags {
|
|
225
226
|
}
|
226
227
|
|
227
228
|
div#sgfa_web div.attach {
|
229
|
+
border-bottom: thin solid;
|
228
230
|
}
|
229
231
|
|
230
232
|
div#sgfa_web div.mainbody div.sidebar div.user {
|
@@ -235,6 +237,7 @@ div#sgfa_web div.hash {
|
|
235
237
|
width: 100%;
|
236
238
|
clear: both;
|
237
239
|
border-top: thin solid;
|
240
|
+
padding-bottom: 3px;
|
238
241
|
font-family: monospace;
|
239
242
|
font-size: 75%;
|
240
243
|
}
|
data/lib/sgfa/binder.rb
CHANGED
@@ -285,12 +285,28 @@ class Binder
|
|
285
285
|
# @param tag [String] Tag name
|
286
286
|
# @param offs [Integer] Offset to begin reading
|
287
287
|
# @param max [Integer] Maximum number of entries to read
|
288
|
-
|
288
|
+
# @param opts [Hash] Options hash
|
289
|
+
# @option opts [Boolean] :raw Get the raw entry when permissions allow
|
290
|
+
# @return [Array] \[ enum, rnum, hnum, time, title, num_tags, num_attach,
|
291
|
+
# \] Or, if opts[:raw], each Array item will consist of the Entry or,
|
292
|
+
# if user does not have read permission, the info array.
|
293
|
+
def read_tag(tr, tag, offs, max, opts={})
|
294
|
+
raw = opts[:raw]
|
289
295
|
_jacket(tr, 'read') do |jck|
|
290
296
|
size, ents = jck.read_tag(tag, offs, max)
|
291
297
|
lst = ents.map do |ent|
|
292
|
-
|
293
|
-
ent.
|
298
|
+
if raw
|
299
|
+
pl = ent.perms
|
300
|
+
begin
|
301
|
+
_perms(tr, pl)
|
302
|
+
item = ent
|
303
|
+
rescue Error::Permission
|
304
|
+
item = nil
|
305
|
+
end
|
306
|
+
end
|
307
|
+
item ||= [ent.entry, ent.revision, ent.history, ent.time, ent.title,
|
308
|
+
ent.tags.size, ent.attachments.size]
|
309
|
+
item
|
294
310
|
end
|
295
311
|
[size, lst]
|
296
312
|
end
|
@@ -398,9 +414,195 @@ class Binder
|
|
398
414
|
end # def write()
|
399
415
|
|
400
416
|
|
417
|
+
#########################################################
|
418
|
+
# @!group Backup
|
419
|
+
|
420
|
+
|
421
|
+
#####################################
|
422
|
+
# Push to a backup store
|
423
|
+
#
|
424
|
+
# @param bsto [Store] Backup store
|
425
|
+
# @param opts [Hash] Options
|
426
|
+
# @option opts [Hash] :log The log. Defaults to STDERR at warn level.
|
427
|
+
# @option opts [Hash] :prev Jacket id_hash to previously pushed max history
|
428
|
+
# @return [Hash] Jacket id_hash to max history backed up
|
429
|
+
def backup_push(bsto, opts={})
|
430
|
+
|
431
|
+
stat = {}
|
432
|
+
prev = opts[:prev] || {}
|
433
|
+
log = opts[:log]
|
434
|
+
if !log
|
435
|
+
log = Logger.new(STDERR)
|
436
|
+
log.level = Logger::WARN
|
437
|
+
end
|
438
|
+
|
439
|
+
# control jacket push
|
440
|
+
jcks = nil
|
441
|
+
_shared do
|
442
|
+
ctl = _jacket_open(0)
|
443
|
+
begin
|
444
|
+
min = (prev[@id_hash] || 0) + 1
|
445
|
+
log.info('Backup push control %s at %d' % [@id_hash, min])
|
446
|
+
stat[@id_hash] = ctl.backup(bsto, min_history: min, log: log)
|
447
|
+
ensure
|
448
|
+
ctl.close
|
449
|
+
end
|
450
|
+
jcks = @jackets.values
|
451
|
+
end
|
452
|
+
|
453
|
+
# all other jackets
|
454
|
+
jcks.each do |info|
|
455
|
+
jck = _jacket_open(info[:num])
|
456
|
+
begin
|
457
|
+
id = info[:id_hash]
|
458
|
+
min = (prev[id] || 0) + 1
|
459
|
+
log.info('Backup push jacket %s at %d' % [id, min])
|
460
|
+
stat[id] = jck.backup(bsto, min_history: min, log: log)
|
461
|
+
ensure
|
462
|
+
jck.close
|
463
|
+
end
|
464
|
+
end
|
465
|
+
|
466
|
+
return stat
|
467
|
+
end # def backup_push()
|
468
|
+
|
469
|
+
|
470
|
+
#####################################
|
471
|
+
# Pull from backup store
|
472
|
+
#
|
473
|
+
# @param bsto [Store] Backup store
|
474
|
+
# @param opts [Hash] Options
|
475
|
+
# @option opts [Hash] :log The log. Defaults to SDTERR at warn level
|
476
|
+
def backup_pull(bsto, opts={})
|
477
|
+
|
478
|
+
log = opts[:log]
|
479
|
+
if !log
|
480
|
+
log = Logger.new(STDERR)
|
481
|
+
log.level = Logger::WARN
|
482
|
+
end
|
483
|
+
|
484
|
+
@lock.do_ex do
|
485
|
+
|
486
|
+
# control jacket
|
487
|
+
ctl = _jacket_open(0)
|
488
|
+
begin
|
489
|
+
log.info('Backup pull control %s' % @id_hash)
|
490
|
+
ctl.restore(bsto, log: log)
|
491
|
+
log.info('Backup pull update cache state')
|
492
|
+
_update(ctl)
|
493
|
+
ensure
|
494
|
+
ctl.close()
|
495
|
+
end
|
496
|
+
_cache_write()
|
497
|
+
|
498
|
+
# all other jackets
|
499
|
+
@jackets.values.each do |info|
|
500
|
+
begin
|
501
|
+
jck = _jacket_open(info[:num])
|
502
|
+
rescue Error::NonExistent
|
503
|
+
log.info('Backup pull create jacket %s' % info[:id_hash])
|
504
|
+
_jacket_create_raw(info)
|
505
|
+
jck = _jacket_open(info[:num])
|
506
|
+
end
|
507
|
+
begin
|
508
|
+
log.info('Backup pull jacket %s' % info[:id_hash])
|
509
|
+
jck.restore(bsto, log: log)
|
510
|
+
ensure
|
511
|
+
jck.close
|
512
|
+
end
|
513
|
+
end
|
514
|
+
|
515
|
+
end
|
516
|
+
|
517
|
+
end # def backup_pull()
|
518
|
+
|
519
|
+
|
401
520
|
private
|
402
521
|
|
403
522
|
|
523
|
+
#####################################
|
524
|
+
# Update cache
|
525
|
+
def _update(ctl)
|
526
|
+
|
527
|
+
values = {}
|
528
|
+
users = {}
|
529
|
+
jackets = {}
|
530
|
+
|
531
|
+
ctl.read_list.each do |tag|
|
532
|
+
|
533
|
+
# values
|
534
|
+
if tag == 'values'
|
535
|
+
|
536
|
+
# process all values entries
|
537
|
+
offs = 0
|
538
|
+
while true
|
539
|
+
size, ary = ctl.read_tag(tag, offs, 2)
|
540
|
+
break if ary.empty?
|
541
|
+
ary.each do |ent|
|
542
|
+
info = _get_json(ent)
|
543
|
+
info.each do |val, sta|
|
544
|
+
next if values.has_key?(val)
|
545
|
+
values[val] = sta
|
546
|
+
end
|
547
|
+
end
|
548
|
+
offs += 2
|
549
|
+
end
|
550
|
+
|
551
|
+
# clear unset values
|
552
|
+
values.delete_if{ |val, sta| !sta.is_a?(String) }
|
553
|
+
|
554
|
+
# jacket
|
555
|
+
elsif /^jacket:/.match(tag)
|
556
|
+
size, ary = ctl.read_tag(tag, 0, 1)
|
557
|
+
info = _get_json(ary.first)
|
558
|
+
jackets[info['name']] = {
|
559
|
+
num: info['num'],
|
560
|
+
name: info['name'],
|
561
|
+
id_hash: info['id_hash'],
|
562
|
+
id_text: info['id_text'],
|
563
|
+
title: info['title'],
|
564
|
+
perms: info['perms'],
|
565
|
+
}
|
566
|
+
|
567
|
+
# user
|
568
|
+
elsif /^user:/.match(tag)
|
569
|
+
size, ary = ctl.read_tag(tag, 0, 1)
|
570
|
+
info = _get_json(ary.first)
|
571
|
+
name = info['name']
|
572
|
+
perms = info['perms']
|
573
|
+
users[name] = perms
|
574
|
+
|
575
|
+
end
|
576
|
+
end
|
577
|
+
|
578
|
+
@users = users
|
579
|
+
@values = values
|
580
|
+
@jackets = jackets
|
581
|
+
|
582
|
+
end # def _update()
|
583
|
+
|
584
|
+
|
585
|
+
#####################################
|
586
|
+
# Get json from a body
|
587
|
+
def _get_json(ent)
|
588
|
+
lines = ent.body.lines
|
589
|
+
st = lines.index{|li| li[0] == '{' || li[0] == '[' }
|
590
|
+
if !st || (lines[-1][0] != '}' && lines[-1][0] != ']')
|
591
|
+
puts ent.body.inspect
|
592
|
+
raise Error::Corrupt, 'Control jacket entry does not contain JSON'
|
593
|
+
end
|
594
|
+
json = lines[st..-1].join
|
595
|
+
|
596
|
+
info = nil
|
597
|
+
begin
|
598
|
+
info = JSON.parse(json)
|
599
|
+
rescue
|
600
|
+
raise Error::Corrupt, 'Control jacket entry JSON parse error'
|
601
|
+
end
|
602
|
+
return info
|
603
|
+
end # def _get_json()
|
604
|
+
|
605
|
+
|
404
606
|
#####################################
|
405
607
|
# Shared creation stuff
|
406
608
|
#
|
data/lib/sgfa/binder_fs.rb
CHANGED
@@ -26,16 +26,15 @@ module Sgfa
|
|
26
26
|
class BinderFs < Binder
|
27
27
|
|
28
28
|
#####################################
|
29
|
-
# Create a
|
29
|
+
# Create a raw binder
|
30
|
+
#
|
31
|
+
# @note You almost certainly want to use {#create} not this method
|
30
32
|
#
|
31
33
|
# @param path [String] Path to the binder
|
32
|
-
# @param
|
33
|
-
# @
|
34
|
-
|
35
|
-
|
36
|
-
raise Error::Sanity, 'Binder already open' if @path
|
37
|
-
Binder.limits_create(init)
|
38
|
-
|
34
|
+
# @param id_text [String] The text ID
|
35
|
+
# @return [String] The hash ID
|
36
|
+
def create_raw(path, id_text)
|
37
|
+
|
39
38
|
# create directory
|
40
39
|
begin
|
41
40
|
Dir.mkdir(path)
|
@@ -45,8 +44,45 @@ class BinderFs < Binder
|
|
45
44
|
|
46
45
|
# create control binder
|
47
46
|
dn_ctrl = File.join(path, '0')
|
48
|
-
id_hash = JacketFs.create(dn_ctrl,
|
49
|
-
|
47
|
+
id_hash = JacketFs.create(dn_ctrl, id_text)
|
48
|
+
|
49
|
+
# write info
|
50
|
+
info = {
|
51
|
+
'sgfa_binder_ver' => 1,
|
52
|
+
'id_hash' => id_hash,
|
53
|
+
'id_text' => id_text,
|
54
|
+
}
|
55
|
+
json = JSON.pretty_generate(info) + "\n"
|
56
|
+
fn_info = File.join(path, 'sgfa_binder.json')
|
57
|
+
File.open(fn_info, 'w', :encoding => 'utf-8'){|fi| fi.write json }
|
58
|
+
|
59
|
+
# create empty cache
|
60
|
+
@path = path
|
61
|
+
@id_hash = id_hash
|
62
|
+
@jackets = {}
|
63
|
+
@users = {}
|
64
|
+
@values = {}
|
65
|
+
_cache_write
|
66
|
+
@path = nil
|
67
|
+
@id_hash = nil
|
68
|
+
|
69
|
+
return id_hash
|
70
|
+
end # def create_raw()
|
71
|
+
|
72
|
+
|
73
|
+
#####################################
|
74
|
+
# Create a new binder
|
75
|
+
#
|
76
|
+
# @param path [String] Path to the binder
|
77
|
+
# @param tr (see Binder#jacket_create)
|
78
|
+
# @param init [Hash] The binder initialization options
|
79
|
+
# @return [String] The hash ID of the new binder
|
80
|
+
def create(path, tr, init)
|
81
|
+
raise Error::Sanity, 'Binder already open' if @path
|
82
|
+
Binder.limits_create(init)
|
83
|
+
|
84
|
+
# create completely empty binder
|
85
|
+
id_hash = create_raw(path, init[:id_text])
|
50
86
|
|
51
87
|
# do the common creation
|
52
88
|
@path = path
|
@@ -54,22 +90,13 @@ class BinderFs < Binder
|
|
54
90
|
@jackets = {}
|
55
91
|
@users = {}
|
56
92
|
@values = {}
|
93
|
+
jck = _jacket_open(0)
|
57
94
|
_create(jck, tr, init)
|
58
95
|
jck.close
|
59
96
|
_cache_write
|
60
97
|
@path = nil
|
61
98
|
@id_hash = nil
|
62
99
|
|
63
|
-
# write info
|
64
|
-
info = {
|
65
|
-
'sgfa_binder_ver' => 1,
|
66
|
-
'id_hash' => id_hash,
|
67
|
-
'id_text' => init[:id_text],
|
68
|
-
}
|
69
|
-
json = JSON.pretty_generate(info) + "\n"
|
70
|
-
fn_info = File.join(path, 'sgfa_binder.json')
|
71
|
-
File.open(fn_info, 'w', :encoding => 'utf-8'){|fi| fi.write json }
|
72
|
-
|
73
100
|
return id_hash
|
74
101
|
end # def create()
|
75
102
|
|
@@ -143,6 +170,14 @@ class BinderFs < Binder
|
|
143
170
|
end # def _jacket_create()
|
144
171
|
|
145
172
|
|
173
|
+
#####################################
|
174
|
+
# Create a jacket from backup
|
175
|
+
def _jacket_create_raw(info)
|
176
|
+
jp = File.join(@path, info[:num].to_s)
|
177
|
+
JacketFs.create_raw(jp, info[:id_text], info[:id_hash])
|
178
|
+
end # def _jacket_create_raw()
|
179
|
+
|
180
|
+
|
146
181
|
#####################################
|
147
182
|
# Open a jacket
|
148
183
|
def _jacket_open(num)
|