sgfa 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.txt +674 -0
- data/README.txt +14 -0
- data/bin/sgfa +15 -0
- data/data/sgfa_web.css +240 -0
- data/lib/sgfa/binder.rb +627 -0
- data/lib/sgfa/binder_fs.rb +203 -0
- data/lib/sgfa/cli/binder.rb +160 -0
- data/lib/sgfa/cli/jacket.rb +299 -0
- data/lib/sgfa/cli.rb +36 -0
- data/lib/sgfa/demo/web_binders.rb +111 -0
- data/lib/sgfa/demo/web_css.rb +60 -0
- data/lib/sgfa/entry.rb +697 -0
- data/lib/sgfa/error.rb +95 -0
- data/lib/sgfa/history.rb +445 -0
- data/lib/sgfa/jacket.rb +556 -0
- data/lib/sgfa/jacket_fs.rb +136 -0
- data/lib/sgfa/lock_fs.rb +141 -0
- data/lib/sgfa/state_fs.rb +342 -0
- data/lib/sgfa/store_fs.rb +214 -0
- data/lib/sgfa/web/base.rb +225 -0
- data/lib/sgfa/web/binder.rb +1190 -0
- data/lib/sgfa.rb +37 -0
- metadata +68 -0
data/lib/sgfa/jacket.rb
ADDED
@@ -0,0 +1,556 @@
|
|
1
|
+
#
|
2
|
+
# Simple Group of Filing Applications
|
3
|
+
# Jacket
|
4
|
+
#
|
5
|
+
# Copyright (C) 2015 by Graham A. Field.
|
6
|
+
#
|
7
|
+
# See LICENSE.txt for licensing information.
|
8
|
+
#
|
9
|
+
# This program is distributed WITHOUT ANY WARRANTY; without even the
|
10
|
+
# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
11
|
+
|
12
|
+
require 'digest/sha2'
|
13
|
+
require 'logger'
|
14
|
+
|
15
|
+
require_relative 'error'
|
16
|
+
require_relative 'history'
|
17
|
+
require_relative 'entry'
|
18
|
+
|
19
|
+
module Sgfa
|
20
|
+
|
21
|
+
|
22
|
+
#####################################################################
|
23
|
+
# A basic filing container which holds {Entry} items with attachments
|
24
|
+
# and maintains a record of changes made in the form of a linked chain
|
25
|
+
# of {History} items.
|
26
|
+
#
|
27
|
+
# This class provides the shared services in common to all
|
28
|
+
# implementations, which are provided by child classes.
|
29
|
+
#
|
30
|
+
# To be functional, a child class must provide:
|
31
|
+
# * @lock - a lock to protect access to the Jacket
|
32
|
+
# * @state - keep track of current history and entry revisions and
|
33
|
+
# track all tags active in the Jacket.
|
34
|
+
# * @store - actually keeps all the items in the Jacket.
|
35
|
+
# * implementations for open, close, create, etc.
|
36
|
+
#
|
37
|
+
class Jacket
|
38
|
+
|
39
|
+
# Max length of text ID
|
40
|
+
TextIdMax = 128
|
41
|
+
|
42
|
+
# Invalid characters in text ID
|
43
|
+
TextIdChars = /[[:cntrl:]]/
|
44
|
+
|
45
|
+
private_constant :TextIdMax, :TextIdChars
|
46
|
+
|
47
|
+
|
48
|
+
#####################################
|
49
|
+
# Limits on Text ID
|
50
|
+
def self.limits_id(txt)
|
51
|
+
Error.limits(txt, 1, TextIdMax, TextIdChars, 'Jacket text ID')
|
52
|
+
end # def self.limits_id()
|
53
|
+
|
54
|
+
|
55
|
+
##########################################
|
56
|
+
# Initialize new jacket
|
57
|
+
def initialize
|
58
|
+
@id_hash = nil
|
59
|
+
@item_hash = Digest::SHA256.new
|
60
|
+
end # def initialize
|
61
|
+
|
62
|
+
|
63
|
+
##########################################
|
64
|
+
# Get Hash ID
|
65
|
+
#
|
66
|
+
# @return [String] Hash ID of the jacket
|
67
|
+
# @raise [Error::Sanity] if jacket not open
|
68
|
+
def id_hash
|
69
|
+
raise Error::Sanity, 'Jacket not open' if !@id_hash
|
70
|
+
return @id_hash.dup
|
71
|
+
end # def id_hash
|
72
|
+
|
73
|
+
|
74
|
+
##########################################
|
75
|
+
# Get Text ID
|
76
|
+
#
|
77
|
+
# @return [String] Text ID of the jacket
|
78
|
+
# @raise [Error::Sanity] if jacket not open
|
79
|
+
def id_text
|
80
|
+
raise Error::Sanity, 'Jacket not open' if !@id_hash
|
81
|
+
return @id_text.dup
|
82
|
+
end # def id_text
|
83
|
+
|
84
|
+
|
85
|
+
|
86
|
+
##########################################
|
87
|
+
# History item
|
88
|
+
#
|
89
|
+
# @param [Fixnum] hnum History number
|
90
|
+
# @return [Array] [:history, item]
|
91
|
+
#
|
92
|
+
# @raise [Error::Sanity] if jacket not open
|
93
|
+
def item_history(hnum)
|
94
|
+
raise Error::Sanity, 'Jacket not open' if !@id_hash
|
95
|
+
txt = "%s history %d\n" % [@id_hash, hnum]
|
96
|
+
[:history, @item_hash.reset.update(txt).hexdigest]
|
97
|
+
end # def item_history()
|
98
|
+
|
99
|
+
|
100
|
+
##########################################
|
101
|
+
# Entry item
|
102
|
+
#
|
103
|
+
# @param [Fixnum] enum Entry number
|
104
|
+
# @param [Fixnum] rnum Revision number
|
105
|
+
# @return [Array] [:entry, item]
|
106
|
+
#
|
107
|
+
# @raise [Error::Sanity] if jacket not open
|
108
|
+
def item_entry(enum, rnum)
|
109
|
+
raise Error::Sanity, 'Jacket not open' if !@id_hash
|
110
|
+
txt = "%s entry %d %d\n" % [@id_hash, enum, rnum]
|
111
|
+
[:entry, @item_hash.reset.update(txt).hexdigest]
|
112
|
+
end
|
113
|
+
|
114
|
+
|
115
|
+
##########################################
|
116
|
+
# Attach item
|
117
|
+
#
|
118
|
+
# @param [Fixnum] enum Entry number
|
119
|
+
# @param [Fixnum] anum Attach number
|
120
|
+
# @param [Fixnum] hnum History number
|
121
|
+
# @return [Array] [:file, item]
|
122
|
+
#
|
123
|
+
# @raise [Error::Sanity] if jacket not open
|
124
|
+
def item_attach(enum, anum, hnum)
|
125
|
+
raise Error::Sanity, 'Jacket not open' if !@id_hash
|
126
|
+
txt = "%s attach %d %d %d\n" % [@id_hash, enum, anum, hnum]
|
127
|
+
[:file, @item_hash.reset.update(txt).hexdigest]
|
128
|
+
end # def item_attach()
|
129
|
+
|
130
|
+
|
131
|
+
#####################################
|
132
|
+
# Read an entry
|
133
|
+
#
|
134
|
+
# @param enum [Integer] The entry number to read
|
135
|
+
# @param rnum [Integer] The revision number, defaults to current
|
136
|
+
# @raise [Error::Sanity] if Jacket is not open
|
137
|
+
# @raise [Error::Corrupt] if the current Entry is missing
|
138
|
+
# @raise [Error::NonExistent] if the Entry is missing
|
139
|
+
# @raise (see Entry#canonical=)
|
140
|
+
# @return [Entry] the {Entry} item
|
141
|
+
def read_entry(enum, rnum=0)
|
142
|
+
raise Error::Sanity, 'Jacket is not open' if !@id_hash
|
143
|
+
|
144
|
+
# current entry
|
145
|
+
if rnum == 0
|
146
|
+
rnum = @lock.do_sh{ @state.get(enum) }
|
147
|
+
current = true
|
148
|
+
else
|
149
|
+
current = false
|
150
|
+
end
|
151
|
+
|
152
|
+
# read, process, and close
|
153
|
+
ent = _read_entry(enum, rnum)
|
154
|
+
if !ent
|
155
|
+
if current
|
156
|
+
raise Error::Corrupt, 'Jacket current entry not present'
|
157
|
+
else
|
158
|
+
raise Error::NonExistent, 'Jacket entry does not exist'
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
return ent
|
163
|
+
end # def read_entry()
|
164
|
+
|
165
|
+
|
166
|
+
#####################################
|
167
|
+
# Read an array of current entries
|
168
|
+
#
|
169
|
+
# @param enums [Array] Entry number list
|
170
|
+
# @return [Array] of {Entry} items
|
171
|
+
# @raise [Error::Corrupt] if current entries not present
|
172
|
+
# @raise [Error::NonExistent] if entry does not exist
|
173
|
+
def read_array(enums)
|
174
|
+
raise Error::Sanity, 'Jacket is not open' if !@id_hash
|
175
|
+
|
176
|
+
# get current entries
|
177
|
+
rnums = @lock.do_sh{ enums.map{|enum| @state.get(enum) } }
|
178
|
+
|
179
|
+
# get entries
|
180
|
+
ents = []
|
181
|
+
enums.each_index do |idx|
|
182
|
+
ent = _read_entry(enums[idx], rnums[idx])
|
183
|
+
raise Error::Corrupt, 'Jacket current entry not present' if !ent
|
184
|
+
ents.push ent
|
185
|
+
end
|
186
|
+
|
187
|
+
return ents
|
188
|
+
end # def read_array()
|
189
|
+
|
190
|
+
|
191
|
+
#####################################
|
192
|
+
# Read individual entry
|
193
|
+
def _read_entry(enum, rnum)
|
194
|
+
ent = Entry.new
|
195
|
+
type, item = item_entry(enum, rnum)
|
196
|
+
fi = @store.read(type, item)
|
197
|
+
return nil if !fi
|
198
|
+
begin
|
199
|
+
ent.canonical = fi.read
|
200
|
+
ensure
|
201
|
+
fi.close
|
202
|
+
end
|
203
|
+
return ent
|
204
|
+
end # def _read_entry()
|
205
|
+
private :_read_entry
|
206
|
+
|
207
|
+
|
208
|
+
#####################################
|
209
|
+
# Read history item
|
210
|
+
#
|
211
|
+
# @param hnum [Integer] the history number to read, defaults to most recent
|
212
|
+
# @raise [Error::Sanity] if Jacket is not open
|
213
|
+
# @raise [Error::NonExistent] if the history does not exist
|
214
|
+
# @raise (see History#canonical=)
|
215
|
+
# @return [History] the {History} item
|
216
|
+
def read_history(hnum=0)
|
217
|
+
raise Error::Sanity, 'Jacket is not open' if !@id_hash
|
218
|
+
|
219
|
+
hst = History.new
|
220
|
+
hnum = @lock.do_sh{ @state.get(0) } if hnum == 0
|
221
|
+
return nil if hnum == 0
|
222
|
+
type, item = item_history(hnum)
|
223
|
+
fi = @store.read(type, item)
|
224
|
+
raise Error::NonExistent, 'Jacket history does not exist' if !fi
|
225
|
+
begin
|
226
|
+
hst.canonical = fi.read
|
227
|
+
ensure
|
228
|
+
fi.close
|
229
|
+
end
|
230
|
+
|
231
|
+
return hst
|
232
|
+
end # def read_history()
|
233
|
+
|
234
|
+
|
235
|
+
#####################################
|
236
|
+
# Read attachment
|
237
|
+
#
|
238
|
+
# @note Remember to close the returned file
|
239
|
+
#
|
240
|
+
# @param enum [Integer] the entry number
|
241
|
+
# @param anum [Integer] the attachemnt number
|
242
|
+
# @param hnum [Integer] the history number
|
243
|
+
# @return [File] the attachment opened read only
|
244
|
+
# @raise [Error::Sanity] if Jacket is not open
|
245
|
+
# @raise [Error::NonExistent] if attachment does not exist
|
246
|
+
def read_attach(enum, anum, hnum)
|
247
|
+
raise Error::Sanity, 'Jacket is not open' if !@id_hash
|
248
|
+
|
249
|
+
type, item = item_attach(enum, anum, hnum)
|
250
|
+
fi = @store.read(type, item)
|
251
|
+
raise Error::NonExistent, 'Jacket attachment does not exist' if !fi
|
252
|
+
return fi
|
253
|
+
end # def read_attach()
|
254
|
+
|
255
|
+
|
256
|
+
#####################################
|
257
|
+
# Read a tag, just getting the list of entry numbers
|
258
|
+
#
|
259
|
+
# @note You probably want to use {#read_tag} instead of this method.
|
260
|
+
#
|
261
|
+
# @param tag [String] Tag name
|
262
|
+
# @param offs [Integer] Offset to begin reading
|
263
|
+
# @param max [Integer] Maximum entries to return
|
264
|
+
# @return [Array] Total number of entries in the tag, and possibly empty
|
265
|
+
# array of entry numbers
|
266
|
+
# @raise [Error::Sanity] if Jacket is not open
|
267
|
+
# @raise [Error::NonExistent] if tag does not exist
|
268
|
+
# @raise [Error::Corrupt] if file format for tag is bad #
|
269
|
+
def read_tag_raw(tag, offs, max)
|
270
|
+
raise Error::Sanity, 'Jacket is not open' if !@id_hash
|
271
|
+
return @lock.do_sh{ @state.tag(tag, offs, max) }
|
272
|
+
end # def read_tag_raw()
|
273
|
+
|
274
|
+
|
275
|
+
#####################################
|
276
|
+
# Read a tag
|
277
|
+
#
|
278
|
+
# @param (see #read_tag_raw)
|
279
|
+
# @return [Array] Total number of entries in the tag, possibly empty
|
280
|
+
# array of {Entry} items
|
281
|
+
# @raise (see #read_tag_raw)
|
282
|
+
# @raise [Error::Corrupt] if current entry is not available
|
283
|
+
def read_tag(tag, offs, max)
|
284
|
+
raise Error::Sanity, 'Jacket is not open' if !@id_hash
|
285
|
+
ents = []
|
286
|
+
size = nil
|
287
|
+
@lock.do_sh do
|
288
|
+
size, elst = @state.tag(tag, offs, max)
|
289
|
+
|
290
|
+
elst.each do |enum|
|
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
|
301
|
+
ents.push ent
|
302
|
+
end
|
303
|
+
end
|
304
|
+
return size, ents
|
305
|
+
end # def read_tag()
|
306
|
+
|
307
|
+
|
308
|
+
#####################################
|
309
|
+
# Read list of all tags
|
310
|
+
#
|
311
|
+
# @return [Array] list of tag names
|
312
|
+
# @raise [Error::Sanity] if Jacket is not open
|
313
|
+
# @raise [Error::Corrupt] if tag list is missing
|
314
|
+
def read_list
|
315
|
+
raise Error::Sanity, 'Jacket is not open' if !@id_hash
|
316
|
+
return @lock.do_sh{ @state.list }
|
317
|
+
end # def read_list
|
318
|
+
|
319
|
+
|
320
|
+
#####################################
|
321
|
+
# Write entires to a Jacket
|
322
|
+
#
|
323
|
+
# @param user [String] User
|
324
|
+
# @param ents [Array] {Entry}s to write
|
325
|
+
# @param tme [Time] Time of the write
|
326
|
+
# @return [History] The history item just created
|
327
|
+
# @raise [Error::Sanity] if Jacket is not open
|
328
|
+
# @raise [Error::Conflict] if entry revision is not one up from current
|
329
|
+
# @raise (see History#process)
|
330
|
+
def write(user, ents, tme=nil)
|
331
|
+
raise Error::Sanity, 'Jacket is not open' if !@id_hash
|
332
|
+
|
333
|
+
hst = nil
|
334
|
+
|
335
|
+
@lock.do_ex do
|
336
|
+
# Check entries to ensure they don't conflict
|
337
|
+
ents.each do |ent|
|
338
|
+
next if !ent.entry
|
339
|
+
if ent.revision != @state.get(ent.entry) + 1
|
340
|
+
raise Error::Conflict, 'Entry revision conflict'
|
341
|
+
end
|
342
|
+
end
|
343
|
+
|
344
|
+
# Update history
|
345
|
+
hnum = @state.get(0)
|
346
|
+
if hnum == 0
|
347
|
+
hst = History.new(@id_hash)
|
348
|
+
cng = hst.process(1, '00000000'*8, 0, user, ents, tme)
|
349
|
+
else
|
350
|
+
prv = History.new
|
351
|
+
type, item = item_history(hnum)
|
352
|
+
fi = @store.read(type, item)
|
353
|
+
raise Error::Corrupt, 'Missing history' if !fi
|
354
|
+
begin
|
355
|
+
prv.canonical = fi.read
|
356
|
+
ensure
|
357
|
+
fi.close
|
358
|
+
end
|
359
|
+
hst, cng = prv.next(user, ents, tme)
|
360
|
+
end
|
361
|
+
hnum = hst.history
|
362
|
+
|
363
|
+
# store entries
|
364
|
+
hle = []
|
365
|
+
cng[:entry].each do |ent|
|
366
|
+
type, item = item_entry(ent.entry, ent.revision)
|
367
|
+
hle.push [item, ent]
|
368
|
+
fi = @store.temp
|
369
|
+
begin
|
370
|
+
fi.write ent.canonical
|
371
|
+
rescue
|
372
|
+
fi.close!
|
373
|
+
raise
|
374
|
+
end
|
375
|
+
@store.write(type, item, fi)
|
376
|
+
@state.set(ent.entry, ent.revision)
|
377
|
+
end
|
378
|
+
|
379
|
+
# store attachments
|
380
|
+
hla = []
|
381
|
+
cng[:attach].each do |enum, anum, file|
|
382
|
+
type, item = item_attach(enum, anum, hnum)
|
383
|
+
hla.push [item, enum, anum, hnum]
|
384
|
+
@store.write(type, item, file)
|
385
|
+
end
|
386
|
+
|
387
|
+
# tags
|
388
|
+
@state.update(cng[:tag])
|
389
|
+
|
390
|
+
# store history and set state
|
391
|
+
type, item = item_history(hnum)
|
392
|
+
fi = @store.temp
|
393
|
+
fi.write hst.canonical
|
394
|
+
@store.write(type, item, fi)
|
395
|
+
@state.set(0, hnum)
|
396
|
+
|
397
|
+
end
|
398
|
+
|
399
|
+
return hst
|
400
|
+
end # def write()
|
401
|
+
|
402
|
+
|
403
|
+
#####################################
|
404
|
+
# Validate history chain
|
405
|
+
#
|
406
|
+
# @param opts [Hash] Option hash
|
407
|
+
# @option opts [Boolean] :hash_entry Validate entries by checking their
|
408
|
+
# hash
|
409
|
+
# @option opts [Boolean] :hash_attach Validate attachments by checking
|
410
|
+
# their hash
|
411
|
+
# @option opts [Fixnum] :max_history History number to stop checking.
|
412
|
+
# Defaults to not stopping until missing history items stop the check.
|
413
|
+
# @option opts [Fixnum] :min_history History number to start checking.
|
414
|
+
# Defaults to 1.
|
415
|
+
# @option opts [Fixnum] :miss_history Number of allowable missing history
|
416
|
+
# items before checking stops. Defaults to zero.
|
417
|
+
# @option opts [String] :max_hash Known good hash for :max_history item
|
418
|
+
# @option opts [Logger] :log The logger to use. Defaults to STDERR log
|
419
|
+
# @return [Boolean] true if valid history chain
|
420
|
+
def check(opts={})
|
421
|
+
max = opts[:max_history] || 1000000000
|
422
|
+
min = opts[:min_history] || 1
|
423
|
+
stop = opts[:miss_history] || 0
|
424
|
+
if opts[:log]
|
425
|
+
log = opts[:log]
|
426
|
+
else
|
427
|
+
log = Logger.new(STDERR)
|
428
|
+
log.level = Logger::WARN
|
429
|
+
end
|
430
|
+
|
431
|
+
log.info('Begin validate jacket %s at %d' % [@id_hash, min])
|
432
|
+
|
433
|
+
miss = 0
|
434
|
+
hnum = min-1
|
435
|
+
prev = nil
|
436
|
+
good = true
|
437
|
+
while (hnum += 1) <= max
|
438
|
+
|
439
|
+
# get history item
|
440
|
+
begin
|
441
|
+
hst = read_history(hnum)
|
442
|
+
rescue Error::NonExistent
|
443
|
+
miss += 1
|
444
|
+
if miss <= stop
|
445
|
+
next
|
446
|
+
else
|
447
|
+
hnum = hnum-miss+1
|
448
|
+
break
|
449
|
+
end
|
450
|
+
rescue Error::Corrupt => exp
|
451
|
+
log.error('History item corrupt %d' % hnum)
|
452
|
+
miss += 1
|
453
|
+
if miss <= stop
|
454
|
+
next
|
455
|
+
else
|
456
|
+
num = hnum-miss+1
|
457
|
+
break
|
458
|
+
end
|
459
|
+
end
|
460
|
+
|
461
|
+
# missing history items
|
462
|
+
if miss != 0
|
463
|
+
good = false
|
464
|
+
if miss == 1
|
465
|
+
log.error('History item missing %d' % (hnum-1))
|
466
|
+
else
|
467
|
+
log.error('HIstory items missing %d-%d' % [hnum-miss, hnum-1])
|
468
|
+
end
|
469
|
+
miss = 0
|
470
|
+
prev = nil
|
471
|
+
end
|
472
|
+
|
473
|
+
# check previous
|
474
|
+
if prev
|
475
|
+
if prev != hst.previous
|
476
|
+
good = false
|
477
|
+
log.error('History chain broken %d' % hnum)
|
478
|
+
else
|
479
|
+
log.debug('History chain matches %d' % hnum)
|
480
|
+
end
|
481
|
+
elsif hnum != min
|
482
|
+
log.warn('History chain not checked %d' % hnum)
|
483
|
+
end
|
484
|
+
prev = hst.hash
|
485
|
+
|
486
|
+
# entries
|
487
|
+
if opts[:hash_entry]
|
488
|
+
hst.entries.each do |enum, rnum, hash|
|
489
|
+
begin
|
490
|
+
ent = read_entry(enum, rnum)
|
491
|
+
rescue Error::NonExistent
|
492
|
+
log.info('Entry missing %d-%d' % [enum, rnum])
|
493
|
+
next
|
494
|
+
rescue Error::Corrupt
|
495
|
+
log.error('Entry corrupt %d-%d' % [enum, rnum])
|
496
|
+
good = false
|
497
|
+
next
|
498
|
+
end
|
499
|
+
if ent.hash != hash
|
500
|
+
log.error('Entry invalid %d-%d' % [enum, rnum])
|
501
|
+
good = false
|
502
|
+
else
|
503
|
+
log.debug('Entry is valid %d-%d' % [enum, rnum])
|
504
|
+
end
|
505
|
+
end
|
506
|
+
end
|
507
|
+
|
508
|
+
# attachments
|
509
|
+
if opts[:hash_attach]
|
510
|
+
hst.attachments.each do |enum, anum, hash|
|
511
|
+
begin
|
512
|
+
fil = read_attach(enum, anum, hnum)
|
513
|
+
rescue Error::NonExistent
|
514
|
+
log.info('Attachment missing %d-%d-%d' % [enum, anum, hnum])
|
515
|
+
next
|
516
|
+
end
|
517
|
+
begin
|
518
|
+
calc = Digest::SHA256.file(fil.path).hexdigest
|
519
|
+
ensure
|
520
|
+
fil.close
|
521
|
+
end
|
522
|
+
if calc != hash
|
523
|
+
log.error('Attachment invalid %d-%d-%d' % [enum, anum, hnum])
|
524
|
+
good = false
|
525
|
+
else
|
526
|
+
log.debug('Attachment is valid %d-%d-%d' % [enum, anum, hnum])
|
527
|
+
end
|
528
|
+
end
|
529
|
+
end
|
530
|
+
|
531
|
+
# last history check known good hash
|
532
|
+
if hnum == max && opts[:max_hash]
|
533
|
+
if hst.hash != opts[:max_hash]
|
534
|
+
log.error('Max history does not match known hash')
|
535
|
+
good = false
|
536
|
+
else
|
537
|
+
log.debug('Max history matches known hash')
|
538
|
+
end
|
539
|
+
end
|
540
|
+
|
541
|
+
end
|
542
|
+
|
543
|
+
log.info('History chain validation max %d' % (hnum-1))
|
544
|
+
|
545
|
+
if opts[:max_history] && hnum != opts[:max_history]
|
546
|
+
return false
|
547
|
+
else
|
548
|
+
return good
|
549
|
+
end
|
550
|
+
|
551
|
+
end # def check()
|
552
|
+
|
553
|
+
|
554
|
+
end # class Jacket
|
555
|
+
|
556
|
+
end # module Sgfa
|
@@ -0,0 +1,136 @@
|
|
1
|
+
#
|
2
|
+
# Simple Group of Filing Applications
|
3
|
+
# Jacket implemented using filesystem storage and locking
|
4
|
+
#
|
5
|
+
# Copyright (C) 2015 by Graham A. Field.
|
6
|
+
#
|
7
|
+
# See LICENSE.txt for licensing information.
|
8
|
+
#
|
9
|
+
# This program is distributed WITHOUT ANY WARRANTY; without even the
|
10
|
+
# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
11
|
+
|
12
|
+
require 'json'
|
13
|
+
|
14
|
+
require_relative 'error'
|
15
|
+
require_relative 'jacket'
|
16
|
+
require_relative 'lock_fs'
|
17
|
+
require_relative 'state_fs'
|
18
|
+
require_relative 'store_fs'
|
19
|
+
|
20
|
+
module Sgfa
|
21
|
+
|
22
|
+
|
23
|
+
#####################################################################
|
24
|
+
# An implementation of {Jacket} using file system storage and locking
|
25
|
+
# provided by {LockFs}, {StoreFs}, and {StateFs}
|
26
|
+
class JacketFs < Jacket
|
27
|
+
|
28
|
+
|
29
|
+
#####################################
|
30
|
+
# Create a new jacket
|
31
|
+
#
|
32
|
+
# @param path [String] Path to create the jacket
|
33
|
+
# @param id_text [String] Text ID of the jacket
|
34
|
+
# @return [String] Hash ID of the jacket
|
35
|
+
# @raise [Error::Limits] if id_text exceeds allowed limits
|
36
|
+
# @raise [Error::Conflict] if path already exists
|
37
|
+
def self.create(path, id_text)
|
38
|
+
Jacket.limits_id(id_text)
|
39
|
+
|
40
|
+
# info
|
41
|
+
id_hash = Digest::SHA256.new.update(id_text).hexdigest
|
42
|
+
info = {
|
43
|
+
'sgfa_jacket_ver' => 1,
|
44
|
+
'id_hash' => id_hash,
|
45
|
+
'id_text' => id_text,
|
46
|
+
}
|
47
|
+
json = JSON.pretty_generate(info) + "\n"
|
48
|
+
|
49
|
+
# create
|
50
|
+
begin
|
51
|
+
Dir.mkdir(path)
|
52
|
+
rescue Errno::EEXIST
|
53
|
+
raise Error::Conflict, 'Jacket path already exists'
|
54
|
+
end
|
55
|
+
fn_info = File.join(path, 'sgfa_jacket.json')
|
56
|
+
File.open(fn_info, 'w', :encoding => 'utf-8'){|fi| fi.write json }
|
57
|
+
|
58
|
+
# create state and store
|
59
|
+
StateFs.create(File.join(path, 'state'))
|
60
|
+
StoreFs.create(File.join(path, 'store'))
|
61
|
+
|
62
|
+
return id_hash
|
63
|
+
end # def self.create()
|
64
|
+
|
65
|
+
|
66
|
+
#####################################
|
67
|
+
# Initialize new Jacket, optionally opening
|
68
|
+
#
|
69
|
+
# @param path [String] path Path to the jacket
|
70
|
+
# @raise (see #open)
|
71
|
+
def initialize(path=nil)
|
72
|
+
super()
|
73
|
+
@path = nil
|
74
|
+
@state = nil
|
75
|
+
@store = nil
|
76
|
+
@lock = nil
|
77
|
+
open(path) if path
|
78
|
+
end # def initialize
|
79
|
+
|
80
|
+
|
81
|
+
#####################################
|
82
|
+
# Open a jacket
|
83
|
+
#
|
84
|
+
# @param path [String] Path to the jacket
|
85
|
+
# @return [JacketFs] self
|
86
|
+
# @raise [Error::Sanity] if jacket already open
|
87
|
+
# @raise [Error::Corrupt] if Jacket info is corrupt
|
88
|
+
# @raise [Error::NonExistent] if path does not exist and contain a valid
|
89
|
+
# jacket info file
|
90
|
+
def open(path)
|
91
|
+
raise Error::Sanity, 'Jacket already open' if @path
|
92
|
+
|
93
|
+
@lock = LockFs.new
|
94
|
+
begin
|
95
|
+
json = @lock.open(File.join(path, 'sgfa_jacket.json'))
|
96
|
+
info = JSON.parse(json, :symbolize_names => true)
|
97
|
+
rescue Errno::ENOENT
|
98
|
+
raise Error::NonExistent, 'Jacket does not exist'
|
99
|
+
rescue JSON::NestingError, JSON::ParserError
|
100
|
+
@lock.close
|
101
|
+
@lock = nil
|
102
|
+
raise Error::Corrupt, 'Jacket info corrupt'
|
103
|
+
end
|
104
|
+
|
105
|
+
@store = StoreFs.new(File.join(path, 'store'))
|
106
|
+
@state = StateFs.new(File.join(path, 'state'))
|
107
|
+
|
108
|
+
@id_text = info[:id_text]
|
109
|
+
@id_hash = info[:id_hash]
|
110
|
+
@path = path.dup
|
111
|
+
|
112
|
+
return self
|
113
|
+
end # def open()
|
114
|
+
|
115
|
+
|
116
|
+
#####################################
|
117
|
+
# Close a jacket
|
118
|
+
#
|
119
|
+
# @raise [Error::Sanity] if jacket not open
|
120
|
+
def close
|
121
|
+
raise Error::Sanity, 'Jacket not open' if !@path
|
122
|
+
@lock.close
|
123
|
+
@lock = nil
|
124
|
+
@state.close
|
125
|
+
@state = nil
|
126
|
+
@store.close
|
127
|
+
@store = nil
|
128
|
+
@id_hash = nil
|
129
|
+
@id_text = nil
|
130
|
+
@path = nil
|
131
|
+
end # def close
|
132
|
+
|
133
|
+
|
134
|
+
end # class JacketFs
|
135
|
+
|
136
|
+
end # module Sgfa
|