sgfa 0.1.0

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.
@@ -0,0 +1,627 @@
1
+ #
2
+ # Simple Group of Filing Applications
3
+ # Binder
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 'entry'
17
+ require_relative 'history'
18
+
19
+ module Sgfa
20
+
21
+ #####################################################################
22
+ # The basic administrative unit in the Sgfa system. A binder provides
23
+ # access control for a collection of {Jacket}s. In addition it allows
24
+ # values to be set for the Binder itself, to use in managing a collection
25
+ # of Binders.
26
+ class Binder
27
+
28
+
29
+ #########################################################
30
+ # @!group Limit checks
31
+
32
+ # Maximum characters in jacket name
33
+ LimJacketMax = 64
34
+
35
+ # Invalid chars in jacket name
36
+ LimJacketInv = /[[:cntrl:]]|[\/\\\*\?]|(^_)/
37
+
38
+ private_constant :LimJacketMax, :LimJacketInv
39
+
40
+
41
+ #####################################
42
+ # Limits checks, jacket name
43
+ def self.limits_jacket(str)
44
+ Error.limits(str, 1, LimJacketMax, LimJacketInv, 'Jacket name')
45
+ end # def self.limits_jacket()
46
+
47
+
48
+ # Max characters in jacket title
49
+ LimTitleMax = 128
50
+
51
+ # Invalid chars in jacket title
52
+ LimTitleInv = /[[:cntrl:]]/
53
+
54
+ private_constant :LimTitleMax, :LimTitleInv
55
+
56
+ #####################################
57
+ # Limits checks, jacket title
58
+ def self.limits_title(str)
59
+ Error.limits(str, 1, LimTitleMax, LimTitleInv, 'Jacket title')
60
+ end # def self.limits_title()
61
+
62
+
63
+ # Max chars in permission
64
+ LimPermMax = 64
65
+
66
+ # Invalid chars in permission
67
+ LimPermInv = /[[:cntrl:]\/\\\*\?,]|(^_)/
68
+
69
+ private_constant :LimPermMax, :LimPermInv
70
+
71
+ #####################################
72
+ # Limits checks, permission array
73
+ def self.limits_perms(ary)
74
+ if !ary.is_a?(Array)
75
+ raise Error::Limits, 'Permission array required'
76
+ end
77
+ ary.each do |prm|
78
+ Error.limits(prm, 1, LimPermMax, LimPermInv, 'Permission')
79
+ end
80
+ end # def self.limits_perms()
81
+
82
+
83
+ # Max chars in value name
84
+ LimValueMax = 64
85
+
86
+ # Invalid chars in value name
87
+ LimValueInv = /[[:cntrl:]]/
88
+
89
+ private_constant :LimValueMax, :LimValueInv
90
+
91
+ #####################################
92
+ # Limits checks, value name
93
+ def self.limits_value(str)
94
+ stc = str.is_a?(Symbol) ? str.to_s : str
95
+ Error.limits(stc, 1, LimValueMax, LimValueInv, 'Value name')
96
+ end # def self.limits_value()
97
+
98
+
99
+ # Max chars in value setting
100
+ LimSettingMax = 128
101
+
102
+ # Invalid chars in value setting
103
+ LimSettingInv = /[[:cntrl:]]/
104
+
105
+ private_constant :LimSettingMax, :LimSettingInv
106
+
107
+
108
+ #####################################
109
+ # Limits checks, value setting
110
+ def self.limits_setting(str)
111
+ Error.limits(str, 1, LimSettingMax, LimSettingInv, 'Value setting')
112
+ end # def self.limits_setting()
113
+
114
+
115
+ #####################################
116
+ # Limits check, create values
117
+ def self.limits_create(info)
118
+ if !info.is_a?(Hash)
119
+ raise Error::Limits, 'Binder create info is not a hash'
120
+ end
121
+
122
+ if !info[:jackets].is_a?(Array)
123
+ raise Error::Limits, 'Binder create info :jackets is not an array'
124
+ end
125
+ info[:jackets].each do |jin|
126
+ if !jin.is_a?(Hash)
127
+ raise Error::Limits, 'Binder create info jacket not a hash'
128
+ end
129
+ Binder.limits_jacket(jin[:name])
130
+ Binder.limits_title(jin[:title])
131
+ Binder.limits_perms(jin[:perms])
132
+ end
133
+
134
+ if !info[:users].is_a?(Array)
135
+ raise Error::Limits, 'Binder create info :users is not an array'
136
+ end
137
+ info[:users].each do |uin|
138
+ if !uin.is_a?(Hash)
139
+ raise Error::Limits, 'Binder create info user not a hash'
140
+ end
141
+ History.limits_user(uin[:name])
142
+ Binder.limits_perms(uin[:perms])
143
+ end
144
+
145
+ if !info[:values].is_a?(Array)
146
+ raise Error::Limits, 'Binder create info :values is not an array'
147
+ end
148
+ info[:values].each do |ary|
149
+ if !ary.is_a?(Array)
150
+ raise Error::Limits, 'Binder create info :value items are not arrays'
151
+ end
152
+ vn, vs = ary
153
+ Binder.limits_value(vn)
154
+ Binder.limits_setting(vs)
155
+ end
156
+
157
+ Jacket.limits_id(info[:id_text]) if info[:id_text]
158
+ end # def limits_create()
159
+
160
+
161
+ #########################################################
162
+ # @!group Binder
163
+
164
+
165
+ #####################################
166
+ # Create a jacket
167
+ #
168
+ # @param tr [Hash] Common transaction info
169
+ # @option tr [String] :jacket Jacket name
170
+ # @option tr [String] :user User name
171
+ # @option tr [Array] :groups List of groups to which :user belongs
172
+ # @option tr [String] :title Title of the entry
173
+ # @option tr [String] :body Body of the entry
174
+ # @param title [String] Title of the jacket
175
+ # @param perms [Array] Permissions for the jacket
176
+ def jacket_create(tr, title, perms)
177
+ Binder.limits_title(title)
178
+ Binder.limits_perms(perms)
179
+ _control(tr) do |jck|
180
+ _perms(tr, ['manage'])
181
+ num = @jackets.size + 1
182
+ id_text, id_hash = _jacket_create(num)
183
+ ent = _control_jacket(tr, num, tr[:jacket], id_hash, id_text,
184
+ title, perms)
185
+ jck.write(tr[:user], [ent])
186
+ @jackets
187
+ end
188
+ end # def jacket_create()
189
+
190
+
191
+ #####################################
192
+ # Edit a jacket
193
+ #
194
+ # @param tr (see #jacket_create)
195
+ # @param name [String] New jacket name
196
+ # @param title [String] New jacket title
197
+ # @param perms [Array] New jacket permissions
198
+ def jacket_edit(tr, name, title, perms)
199
+ Binder.limits_jacket(name)
200
+ Binder.limits_title(title)
201
+ Binder.limits_perms(perms)
202
+ _control(tr) do |jck|
203
+ jnam = tr[:jacket]
204
+ raise Error::NonExistent, 'Jacket does not exist' if !@jackets[jnam]
205
+ jacket = @jackets[jnam]
206
+ num = jacket['num']
207
+ ent = _control_jacket(tr, num, name, jacket['id_hash'],
208
+ jacket['id_text'], title, perms)
209
+ jck.write(tr[:user], [ent])
210
+ @jackets
211
+ end
212
+ end # def jacket_edit()
213
+
214
+
215
+ #####################################
216
+ # Set user or group permissions
217
+ #
218
+ # @param tr (see #jacket_create)
219
+ # @param perms [Array] New user/group permissions
220
+ def binder_user(tr, user, perms)
221
+ History.limits_user(user)
222
+ Binder.limits_perms(perms)
223
+ _control(tr) do |jck|
224
+ ent = _control_user(tr, user, perms)
225
+ jck.write(tr[:user], [ent])
226
+ @users
227
+ end
228
+ end # def binder_user()
229
+
230
+
231
+ #####################################
232
+ # Set values
233
+ #
234
+ # @param tr (see #jacket_create)
235
+ # @param vals [Hash] New values
236
+ def binder_values(tr, vals)
237
+ vals.each do |vn, vs|
238
+ Binder.limits_value(vn)
239
+ Binder.limits_setting(vs)
240
+ end
241
+ _control(tr) do |jck|
242
+ ent = _control_values(tr, vals)
243
+ jck.write(tr[:user], [ent])
244
+ @values
245
+ end
246
+ end # def binder_values()
247
+
248
+
249
+ #####################################
250
+ # Get info
251
+ #
252
+ # @param tr (see #jacket_create)
253
+ # @return [Hash] Containing :id_hash, :id_text, :jackets, :values, :users
254
+ def binder_info(tr)
255
+ _shared do
256
+ _perms(tr, ['info'])
257
+ {
258
+ :id_hash => @id_hash.dup,
259
+ :id_text => @id_text.dup,
260
+ :values => @values,
261
+ :jackets => @jackets,
262
+ :users => @users,
263
+ }
264
+ end
265
+ end # def binder_info()
266
+
267
+
268
+ #########################################################
269
+ # @!group Jacket
270
+
271
+
272
+ #####################################
273
+ # Read list of tags
274
+ #
275
+ # @param tr (see #jacket_create)
276
+ def read_list(tr)
277
+ _jacket(tr, 'info'){|jck| jck.read_list }
278
+ end # def read_list()
279
+
280
+
281
+ #####################################
282
+ # Read a tag
283
+ #
284
+ # @param tr (see #jacket_create)
285
+ # @param tag [String] Tag name
286
+ # @param offs [Integer] Offset to begin reading
287
+ # @param max [Integer] Maximum number of entries to read
288
+ def read_tag(tr, tag, offs, max)
289
+ _jacket(tr, 'read') do |jck|
290
+ size, ents = jck.read_tag(tag, offs, max)
291
+ lst = ents.map do |ent|
292
+ [ent.entry, ent.revision, ent.time, ent.title, ent.tags.size,
293
+ ent.attachments.size]
294
+ end
295
+ [size, lst]
296
+ end
297
+ end # def read_tag()
298
+
299
+
300
+ #####################################
301
+ # Read history log
302
+ #
303
+ # @param tr (see #jacket_create)
304
+ # @param offs [Integer] Offset to begin reading
305
+ # @param max [Integer] Maximum number of histories to read
306
+ def read_log(tr, offs, max)
307
+ _jacket(tr, 'info') do |jck|
308
+ cur = jck.read_history()
309
+ hmax = cur ? cur.history : 0
310
+ start = (offs <= hmax) ? hmax - offs : 0
311
+ stop = (start - max > 0) ? (start - (max-1)) : 1
312
+ ary = []
313
+ if start != 0
314
+ start.downto(stop) do |hnum|
315
+ hst = jck.read_history(hnum)
316
+ ary.push [hst.history, hst.time, hst.user, hst.entries.size,
317
+ hst.attachments.size]
318
+ end
319
+ end
320
+ [hmax, ary]
321
+ end
322
+ end # def read_log()
323
+
324
+
325
+ #####################################
326
+ # Read an entry
327
+ #
328
+ # @param tr (see #jacket_create)
329
+ # @param enum [Integer] Entry number
330
+ # @param rnum [Integer] Revision number
331
+ # @return [Entry] the Requested entry
332
+ def read_entry(tr, enum, rnum=0)
333
+ _jacket(tr, 'read') do |jck|
334
+ cur = jck.read_entry(enum, 0)
335
+ pl = cur.perms
336
+ _perms(tr, pl) if !pl.empty?
337
+ if rnum == 0
338
+ cur
339
+ else
340
+ jck.read_entry(enum, rnum)
341
+ end
342
+ end
343
+ end # def read_entry()
344
+
345
+
346
+ #####################################
347
+ # Read a history item
348
+ #
349
+ # @param tr (see #jacket_create)
350
+ # @param hnum [Integer] History number
351
+ # @return [History] History item requested
352
+ def read_history(tr, hnum)
353
+ _jacket(tr, 'info'){|jck| jck.read_history(hnum) }
354
+ end # def read_history()
355
+
356
+
357
+ #####################################
358
+ # Read an attachment
359
+ #
360
+ # @param tr (see #jacket_create)
361
+ # @param enum [Integer] Entry number
362
+ # @param anum [Integer] Attachment number
363
+ # @param hnum [Integer] History number
364
+ # @return [File] Attachment
365
+ def read_attach(tr, enum, anum, hnum)
366
+ _jacket(tr, 'read') do |jck|
367
+ cur = jck.read_entry(enum, 0)
368
+ pl = cur.perms
369
+ _perms(tr, pl) if !pl.empty?
370
+ jck.read_attach(enum, anum, hnum)
371
+ end
372
+ end # def read_attach()
373
+
374
+
375
+ #####################################
376
+ # Write entries
377
+ #
378
+ # @param tr (see #jacket_create)
379
+ # @param ents [Array] List of entries to write
380
+ # @return (see Jacket#write)
381
+ # @raise [Error::Permission] if user lacks require permissions
382
+ # @raise [Error::Conflict] if entry revision is not one up from current
383
+ def write(tr, ents)
384
+ olde = ents.select{|ent| ent.entry }
385
+ enums = olde.map{|ent| ent.entry }
386
+ _jacket(tr, 'write') do |jck|
387
+ cur = jck.read_array(enums)
388
+ pl = []
389
+ cur.each{|ent| pl.concat ent.perms }
390
+ _perms(tr, pl) if !pl.empty?
391
+ enums.each_index do |idx|
392
+ if cur[idx].revision + 1 != olde[idx].revision
393
+ raise Error::Conflict, 'Entry revision conflict'
394
+ end
395
+ end
396
+ jck.write(tr[:user], ents)
397
+ end
398
+ end # def write()
399
+
400
+
401
+ private
402
+
403
+
404
+ #####################################
405
+ # Shared creation stuff
406
+ #
407
+ # @param ctl [Jacket] Control jacket
408
+ # @param tr (see #jacket_create)
409
+ # @param info [Hash] New binder creation options
410
+ # @option info [Array] :jackets List of jackets [name, title, perms]
411
+ # @option info [Array] :users List of users [name, perms]
412
+ # @option info [Hash] :values List of values name => setting
413
+ def _create(ctl, tr, info)
414
+
415
+ # check all the values are okay
416
+ History.limits_user(tr[:user])
417
+ Entry.limits_title(tr[:title])
418
+ Entry.limits_body(tr[:body])
419
+ Binder.limits_create(info)
420
+
421
+ ents = []
422
+
423
+ # jackets
424
+ num = 0
425
+ info[:jackets].each do |jin|
426
+ num += 1
427
+ trj = {
428
+ :title => 'Create binder initial jacket \'%s\'' % jin[:name],
429
+ :jacket => jin[:name],
430
+ :body => "Create binder initial jacket\n\n",
431
+ }
432
+ id_text, id_hash = _jacket_create(num)
433
+ ents.push _control_jacket(trj, num, jin[:name], id_hash, id_text,
434
+ jin[:title], jin[:perms])
435
+ end
436
+
437
+ # users
438
+ info[:users].each do |uin|
439
+ tru = {
440
+ :title => 'Create binder initial user \'%s\'' % uin[:name],
441
+ :body => "Create binder initial user\n\n",
442
+ }
443
+ ents.push _control_user(tr, uin[:name], uin[:perms])
444
+ end
445
+
446
+ # values
447
+ ents.push _control_values(tr, info[:values])
448
+ ctl.write(tr[:user], ents)
449
+ end # def _create()
450
+
451
+
452
+ #####################################
453
+ # Permission check
454
+ #
455
+ # @param tr (see #jacket_create)
456
+ # @param plst [Array] Permissions required
457
+ # @raise [Error::Permissions] if require permissions not met
458
+ def _perms(tr, plst)
459
+ if tr[:perms]
460
+ usr_has = tr[:perms]
461
+ else
462
+ usr = tr[:user]
463
+ grp = tr[:groups]
464
+ usr_has = []
465
+ usr_has.concat(@users[usr]) if @users[usr]
466
+ grp.each{|gr| usr_has.concat(@users[gr]) if @users[gr] }
467
+ if usr_has.include?('write')
468
+ usr_has.concat ['read', 'info']
469
+ elsif usr_has.include?('read') || usr_has.include?('manage')
470
+ usr_has.push 'info'
471
+ end
472
+ usr_has.uniq!
473
+ tr[:perms] = usr_has
474
+ end
475
+
476
+ miss = []
477
+ plst.each{|pr| miss.push(pr) if !usr_has.include?(pr) }
478
+
479
+ if !miss.empty?
480
+ raise Error::Permission, 'User lacks permission(s): ' + miss.join(', ')
481
+ end
482
+ end # def _perms()
483
+
484
+
485
+ #####################################
486
+ # Access a jacket
487
+ #
488
+ # @param tr (see #jacket_create)
489
+ # @param perm [String] Basic permission needed (write, read, info)
490
+ def _jacket(tr, perm)
491
+ ret = nil
492
+ _shared do
493
+ jnam = tr[:jacket]
494
+ raise Error::NonExistent, 'Jacket does not exist' if !@jackets[jnam]
495
+ pl = [perm].concat @jackets[jnam][:perms]
496
+ _perms(tr, pl)
497
+ jck = _jacket_open(@jackets[jnam][:num])
498
+ begin
499
+ ret = yield(jck)
500
+ ensure
501
+ jck.close
502
+ end
503
+ end
504
+ return ret
505
+ end # def _jacket()
506
+
507
+
508
+ #####################################
509
+ # Edit control jacket
510
+ #
511
+ # @param tr (see #jacket_create)
512
+ def _control(tr)
513
+ ret = nil
514
+ @lock.do_ex do
515
+ _cache_read()
516
+ _perms(tr, ['manage'])
517
+ begin
518
+ ctl = _jacket_open(0)
519
+ begin
520
+ ret = yield(ctl)
521
+ ensure
522
+ ctl.close()
523
+ end
524
+ _cache_write()
525
+ ensure
526
+ _cache_clear()
527
+ end
528
+ end # @lock_do
529
+ return ret
530
+ end # def _control()
531
+
532
+
533
+ #####################################
534
+ # Shared access to the binder
535
+ def _shared
536
+ ret = nil
537
+ @lock.do_sh do
538
+ _cache_read()
539
+ begin
540
+ ret = yield
541
+ ensure
542
+ _cache_clear()
543
+ end
544
+ end # @lock.do_sh
545
+ return ret
546
+ end # def _shared
547
+
548
+
549
+ #####################################
550
+ # Set jacket info
551
+ def _control_jacket(tr, num, name, id_hash, id_text, title, perms)
552
+ info = {
553
+ num: num,
554
+ name: name,
555
+ id_hash: id_hash,
556
+ id_text: id_text,
557
+ title: title,
558
+ perms: perms,
559
+ }
560
+ json = JSON.pretty_generate(info)
561
+
562
+ ent = Entry.new
563
+ ent.tag( 'jacket: %d' % num )
564
+ ent.title = tr[:title]
565
+ ent.body = tr[:body] + "\n" + json + "\n"
566
+
567
+ @jackets.delete(tr[:jacket])
568
+ @jackets[name] = info
569
+
570
+ return ent
571
+ end # def _control_jacket()
572
+
573
+
574
+ #####################################
575
+ # Set user permissions in the control jacket
576
+ def _control_user(tr, user, perms)
577
+ info = {
578
+ name: user,
579
+ perms: perms,
580
+ }
581
+ json = JSON.pretty_generate(info)
582
+
583
+ ent = Entry.new
584
+ ent.tag( 'user: %s' % user )
585
+ ent.title = tr[:title]
586
+ ent.body = tr[:body] + "\n" + json + "\n"
587
+
588
+ @users[user.dup] = perms.map{|pr| pr.dup }
589
+
590
+ return ent
591
+ end # def _control_user()
592
+
593
+
594
+ #####################################
595
+ # Set binder values in the control jacket
596
+ def _control_values(tr, vals)
597
+ json = JSON.pretty_generate(vals)
598
+
599
+ ent = Entry.new
600
+ ent.tag( 'values' )
601
+ ent.title = tr[:title]
602
+ ent.body = tr[:body] + "\n" + json + "\n"
603
+
604
+ vals.each do |val, sta|
605
+ vas = val.is_a?(Symbol) ? val : val.to_s
606
+ if sta
607
+ @values[val] = sta
608
+ else
609
+ @values.delete(val)
610
+ end
611
+ end
612
+
613
+ return ent
614
+ end # def _control_values()
615
+
616
+
617
+ #####################################
618
+ # Clear cache
619
+ def _cache_clear
620
+ @jackets = nil
621
+ @users = nil
622
+ @values = nil
623
+ end # def _cache_clear
624
+
625
+ end # class Binder
626
+
627
+ end # module Sgfa