sgfa 0.1.0

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