icfs 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.
data/lib/icfs.rb ADDED
@@ -0,0 +1,109 @@
1
+ #
2
+ # Investigative Case File System
3
+ #
4
+ # Copyright 2019 by Graham A. Field
5
+ #
6
+ # This program is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU General Public License version 3.
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
+
14
+ ##########################################################################
15
+ # Investigative Case File System
16
+ #
17
+ # @todo Delete Items and move into this module
18
+ # @todo Verification tool written
19
+ # @todo Archive/move case tool written
20
+ #
21
+ module ICFS
22
+
23
+
24
+ # no tags
25
+ TagNone = '[none]'.freeze
26
+
27
+ # edits an action
28
+ TagAction = '[action]'.freeze
29
+
30
+ # edits an index
31
+ TagIndex = '[index]'.freeze
32
+
33
+ # edits the case
34
+ TagCase = '[case]'.freeze
35
+
36
+
37
+ # permission to read case
38
+ PermRead = '[read]'.freeze
39
+
40
+ # permission to write case
41
+ PermWrite = '[write]'.freeze
42
+
43
+ # permission to manage case
44
+ PermManage = '[manage]'.freeze
45
+
46
+ # permission to manage actions
47
+ PermAction = '[action]'.freeze
48
+
49
+ # global permission to search
50
+ PermSearch = '{[search]}'.freeze
51
+
52
+
53
+ # user group
54
+ UserCase = '[case]'.freeze
55
+
56
+
57
+ ###############################################
58
+ # Hash a string
59
+ def self.hash(str)
60
+ Digest::SHA256.hexdigest(str)
61
+ end
62
+
63
+
64
+ ###############################################
65
+ # Hash a tempfile
66
+ #
67
+ def self.hash_temp(tf)
68
+ Digest::SHA256.file(tf.path).hexdigest
69
+ end
70
+
71
+
72
+
73
+
74
+ ##########################################################################
75
+ # Error
76
+ #
77
+ module Error
78
+
79
+
80
+ ##########################################################################
81
+ # Invalid values
82
+ class Value < ArgumentError; end
83
+
84
+ ##########################################################################
85
+ # Item not found
86
+ class NotFound < RuntimeError; end
87
+
88
+ ##########################################################################
89
+ # Do not have required permissions
90
+ class Perms < RuntimeError; end
91
+
92
+ ##########################################################################
93
+ # Conflict with pre-existing values
94
+ class Conflict < RuntimeError; end
95
+
96
+ ##########################################################################
97
+ # Interface errors
98
+ class Interface < RuntimeError; end
99
+
100
+ end # module ICFS::Error
101
+
102
+ end # module ICFS
103
+
104
+ require_relative 'icfs/validate'
105
+ require_relative 'icfs/cache'
106
+ require_relative 'icfs/store'
107
+ require_relative 'icfs/items'
108
+ require_relative 'icfs/api'
109
+ require_relative 'icfs/users'
data/lib/icfs/api.rb ADDED
@@ -0,0 +1,1436 @@
1
+ #
2
+ # Investigative Case File System
3
+ #
4
+ # Copyright 2019 by Graham A. Field
5
+ #
6
+ # This program is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU General Public License version 3.
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
+ #
13
+ module ICFS
14
+
15
+ ##########################################################################
16
+ # Api
17
+ #
18
+ # @todo Add event logging
19
+ #
20
+ class Api
21
+
22
+ # Validate a size
23
+ ValSize = {
24
+ method: :integer,
25
+ min: 2,
26
+ max: 100
27
+ }.freeze
28
+
29
+
30
+ # Validate a page
31
+ ValPage = {
32
+ method: :integer,
33
+ min: 1,
34
+ max: 10
35
+ }.freeze
36
+
37
+
38
+ # Validate a purpose
39
+ ValPurpose = {
40
+ method: :string,
41
+ min: 1,
42
+ max: 32,
43
+ invalid: /[[:cntrl:]]/.freeze
44
+ }.freeze
45
+
46
+
47
+ ###############################################
48
+ # New API
49
+ #
50
+ # @param stats [Array<String>] Global stats
51
+ # @param users [Users] the User/role/group interface
52
+ # @param cache [Cache] the cache
53
+ # @param store [Store] the store
54
+ #
55
+ def initialize(stats, users, cache, store)
56
+ @users = users
57
+ @cache = cache
58
+ @store = store
59
+ @gstats = stats.map{|st| st.dup.freeze }.freeze
60
+ reset
61
+ end # def initialize
62
+
63
+
64
+ ###############################################
65
+ # Set the user
66
+ #
67
+ # @param uname [String] the user name
68
+ #
69
+ def user=(uname)
70
+ @user = uname.dup.freeze
71
+ urgp = @users.read(uname)
72
+ raise(Error::NotFound, 'User name not found'.freeze) if !urgp
73
+ raise(Error::Value, 'Not a user'.freeze) if urgp['type'] != 'user'
74
+ @roles = urgp['roles'].each{|rn| rn.freeze }
75
+ @groups = urgp['groups'].each{ |gn| gn.freeze }
76
+ @perms = urgp['perms'].each{|pn| pn.freeze }
77
+
78
+ @urg = Set.new
79
+ @urg.add user
80
+ @urg.merge roles
81
+ @urg.merge groups
82
+ @urg.freeze
83
+
84
+ @ur = Set.new
85
+ @ur.add user
86
+ @ur.merge roles
87
+ @ur.freeze
88
+
89
+ reset
90
+ end # def user=()
91
+
92
+
93
+ ###############################################
94
+ # User
95
+ #
96
+ attr_reader :user
97
+
98
+
99
+ ###############################################
100
+ # Roles
101
+ #
102
+ attr_reader :roles
103
+
104
+
105
+ ###############################################
106
+ # Groups
107
+ #
108
+ attr_reader :groups
109
+
110
+
111
+ ###############################################
112
+ # Global perms
113
+ #
114
+ attr_reader :perms
115
+
116
+
117
+ ###############################################
118
+ # Globals stats
119
+ attr_reader :gstats
120
+
121
+
122
+ ###############################################
123
+ # User, Roles, Groups set
124
+ #
125
+ attr_reader :urg
126
+
127
+
128
+ ###############################################
129
+ # Reset the cached cases and access
130
+ #
131
+ def reset
132
+ @cases = {}
133
+ @access = {}
134
+ @actions = {}
135
+ @tasked = {}
136
+ end
137
+
138
+
139
+ ###############################################
140
+ # Get a tempfile
141
+ #
142
+ def tempfile
143
+ @store.tempfile
144
+ end
145
+
146
+
147
+ ###############################################
148
+ # Get a stats list
149
+ #
150
+ # @param cid [String] caseid
151
+ # @return [Set<String>] the stats, global and case
152
+ # @raise [Error::NotFound] if case not found
153
+ #
154
+ def stats_list(cid)
155
+ cse = case_read(cid)
156
+ stats = Set.new
157
+ stats.merge( cse['stats'] ) if cse['stats']
158
+ stats.merge( @gstats )
159
+ return stats
160
+ end
161
+
162
+
163
+ ###############################################
164
+ # Get an access list
165
+ #
166
+ # @param cid [String] caseid
167
+ # @return [Set<String>] the perms granted the user for this case
168
+ # @raise [Error::NotFound] if case not found
169
+ #
170
+ def access_list(cid)
171
+ if !@access.key?(cid)
172
+
173
+ # get grants for the case
174
+ cse = case_read(cid)
175
+ al = Set.new
176
+ cse['access'].each do |ac|
177
+ gs = Set.new(ac['grant'])
178
+ al.add(ac['perm']) if @urg.intersect?(gs)
179
+ end
180
+
181
+ # higher perms imply lower ones
182
+ al.add(ICFS::PermRead) if al.include?(ICFS::PermManage)
183
+ al.add(ICFS::PermWrite) if al.include?(ICFS::PermAction)
184
+ al.add(ICFS::PermRead) if al.include?(ICFS::PermWrite)
185
+
186
+ # merge in global perms
187
+ al.merge @perms
188
+
189
+ @access[cid] = al
190
+ end
191
+ return @access[cid]
192
+ end # def access_list()
193
+
194
+
195
+ ###############################################
196
+ # See if we are tasked
197
+ def tasked?(cid, anum)
198
+ id = '%s.%d'.freeze % [cid, anum]
199
+ unless @tasked.key?(id)
200
+ act = _action_read(cid, anum)
201
+
202
+ tasked = false
203
+ act['tasks'].each do |tk|
204
+ if @ur.include?(tk['assigned'])
205
+ tasked = true
206
+ break
207
+ end
208
+ end
209
+
210
+ @tasked[id] = tasked
211
+ end
212
+
213
+ return @tasked[id]
214
+ end # def tasked?()
215
+
216
+
217
+ ###############################################
218
+ # Check if we can read an entry or action
219
+ def _can_read?(cid, anum)
220
+
221
+ # have read permission on the case or
222
+ # are assigned to the action
223
+ if access_list(cid).include?( ICFS::PermRead )
224
+ return true
225
+ elsif anum && tasked?(cid, anum)
226
+ return true
227
+ else
228
+ return false
229
+ end
230
+
231
+ # handle an action that isn't found
232
+ rescue Error::NotFound
233
+ return false
234
+ end # def _can_read?()
235
+ private :_can_read?
236
+
237
+
238
+ ###############################################
239
+ # Check if we have search permissions for a case
240
+ #
241
+ def _search?(query)
242
+ if( @perms.include?(ICFS::PermSearch) || ( query[:caseid] &&
243
+ access_list(query[:caseid]).include?(ICFS::PermRead) ) )
244
+ return true
245
+ else
246
+ return false
247
+ end
248
+ end # def _search?()
249
+ private :_search?
250
+
251
+
252
+ ##############################################################
253
+ # Read Items
254
+ ##############################################################
255
+
256
+
257
+ ###############################################
258
+ # Read a case
259
+ #
260
+ # @param cid [String] caseid
261
+ # @param lnum [Integer] log it was recorded
262
+ # @return [Case] the case
263
+ # @raise [Error::NotFound] if not case not found
264
+ #
265
+ def case_read(cid, lnum=0)
266
+ if lnum != 0
267
+ json = @store.case_read(cid, lnum)
268
+ return Validate.parse(json, 'case'.freeze, Items::ItemCase)
269
+ end
270
+
271
+ if !@cases.key?(cid)
272
+ json = @cache.case_read(cid)
273
+ cur = Validate.parse(json, 'case'.freeze, Items::ItemCase)
274
+ @cases[cid] = cur
275
+ end
276
+ return @cases[cid]
277
+ end # end case_read()
278
+
279
+
280
+ ###############################################
281
+ # Read a log
282
+ #
283
+ # @param cid [String] caseid
284
+ # @param lnum [Integer] log number
285
+ # @raise [Error::NotFound] if log is not found
286
+ # @raise [Error::Perms] if user does not have permissions
287
+ #
288
+ def log_read(cid, lnum)
289
+
290
+ # get access list
291
+ al = access_list(cid)
292
+ if !al.include?(ICFS::PermRead)
293
+ raise(Error::Perms, 'missing perms: %s'.freeze % ICFS::PermRead)
294
+ end
295
+
296
+ # read
297
+ json = @cache.log_read(cid, lnum)
298
+ return Validate.parse(json, 'log'.freeze, Items::ItemLog)
299
+ end # def log_read()
300
+
301
+
302
+ ###############################################
303
+ # Read an entry
304
+ #
305
+ # @param cid [String] caseid
306
+ # @param enum [Integer] the entry number
307
+ # @param lnum [Integer] the log number or 0 for current
308
+ # @raise [Error::NotFound] if it does not exist
309
+ # @raise [Error::Perms] if user does not have permissions
310
+ #
311
+ def entry_read(cid, enum, lnum=0)
312
+
313
+ # get access list and current entry
314
+ al = access_list(cid)
315
+ json = @cache.entry_read(cid, enum)
316
+ ec = Validate.parse(json, 'entry'.freeze, Items::ItemEntry)
317
+
318
+ # see if we can read the entry
319
+ need = Set.new
320
+ need.add( ICFS::PermRead ) unless _can_read?(cid, ec['action'] )
321
+ need.merge(ec['perms']) if ec['perms']
322
+ need.subtract(al)
323
+ unless need.empty?
324
+ raise(Error::Perms, 'missing perms: %s'.freeze %
325
+ need.to_a.sort.join(', ') )
326
+ end
327
+
328
+ # return requested version
329
+ if( lnum == 0 || ec['log'] == lnum )
330
+ return ec
331
+ else
332
+ json = @store.entry_read(cid, enum, lnum)
333
+ return Validate.parse(json, 'entry'.freeze, Items::ItemEntry)
334
+ end
335
+ end # def entry_read()
336
+
337
+
338
+ ###############################################
339
+ # Read a file
340
+ #
341
+ # @param cid [String] caseid
342
+ # @param enum [Integer] the entry number
343
+ # @param lnum [Integer] the log number
344
+ # @param fnum [Integer] the file number
345
+ # @raise [Error::NotFound] if it does not exist
346
+ # @raise [Error::Perms] if user does not have permissions
347
+ #
348
+ def file_read(cid, enum, lnum, fnum)
349
+ entry_read(cid, enum)
350
+ fi = @store.file_read(cid, enum, lnum, fnum)
351
+ raise(Error::NotFound, 'file not found'.freeze) if !fi
352
+ return fi
353
+ end # def file_read()
354
+
355
+
356
+ ###############################################
357
+ # Read an action
358
+ #
359
+ # Internal version.
360
+ #
361
+ def _action_read(cid, anum)
362
+ id = '%s.%d'.freeze % [cid, anum]
363
+ unless @actions.key?(id)
364
+ json = @cache.action_read(cid, anum)
365
+ act = Validate.parse(json, 'action'.freeze, Items::ItemAction)
366
+ @actions[id] = act
367
+ end
368
+ return @actions[id]
369
+ end # _action_read()
370
+ private :_action_read
371
+
372
+
373
+ ###############################################
374
+ # Read an action
375
+ #
376
+ # @param cid [String] caseid
377
+ # @param anum [Integer] the action number
378
+ # @param lnum [Integer] the log number or 0 for current
379
+ # @return [Action] requested action
380
+ # @raise [Error::NotFound] if action is not found
381
+ # @raise [Error::Perms] if user does not have permissions
382
+ #
383
+ def action_read(cid, anum, lnum=0)
384
+
385
+ # get current action
386
+ ac = _action_read(cid, anum)
387
+
388
+ # see if we can read the action
389
+ unless _can_read?(cid, anum)
390
+ raise(Error::Perms, 'missing perms: %s'.freeze % ICFS::PermRead )
391
+ end
392
+
393
+ # return the requested version
394
+ if( lnum == 0 || ac['log'] == lnum )
395
+ return ac
396
+ else
397
+ json = @store.action_read( cid, anum, lnum)
398
+ return Validate.parse(json, 'action'.freeze, Items::ItemAction)
399
+ end
400
+ end # def action_read()
401
+
402
+
403
+ ###############################################
404
+ # Read an index
405
+ #
406
+ # @param cid [String]
407
+ # @param xnum [Integer] the index number
408
+ # @param lnum [Integer] the log number
409
+ # @raise [Error::NotFound] if it does not exist
410
+ # @raise [Error::Perms] if user does not have permissions
411
+ #
412
+ def index_read(cid, xnum, lnum=0)
413
+
414
+ # get access list
415
+ al = access_list(cid)
416
+ if !al.include?(ICFS::PermRead)
417
+ raise(Error::Perms, 'missing perms: %s'.freeze % ICFS::PermRead )
418
+ end
419
+
420
+ # read curent index
421
+ json = @cache.index_read(cid, xnum)
422
+ xc = Validate.parse(json, 'index'.freeze, Items::ItemIndex)
423
+
424
+ # return the requested version
425
+ if( lnum == 0 || xc['log'] == lnum )
426
+ return xc
427
+ else
428
+ json = @store.index_read(cid, xnum, lnum)
429
+ return Validate.parse(json, 'index'.freeze, Items::ItemIndex)
430
+ end
431
+ end # def index_read()
432
+
433
+
434
+ ###############################################
435
+ # Read a current
436
+ #
437
+ # @param cid [String] caseid
438
+ #
439
+ def current_read(cid)
440
+
441
+ al = access_list(cid)
442
+ if !al.include?(ICFS::PermRead)
443
+ raise(Error::Perms, 'missing perms: %s'.freeze % ICFS::PermRead)
444
+ end
445
+
446
+ json = @cache.current_read(cid)
447
+ return Validate.parse(json, 'current'.current, Items::ItemCurrent)
448
+ end # end def current_read()
449
+
450
+
451
+ ##############################################################
452
+ # Searches
453
+ ##############################################################
454
+
455
+
456
+ # Validate a case search query
457
+ ValCaseSearch = {
458
+ method: :hash,
459
+ optional: {
460
+ title: Items::FieldTitle,
461
+ tags: Items::FieldTagAny,
462
+ status: Validate::IsBoolean,
463
+ template: Validate::IsBoolean,
464
+ grantee: Items::FieldUsergrp,
465
+ perm: Items::FieldPermAny,
466
+ size: ValSize,
467
+ page: ValPage,
468
+ purpose: ValPurpose,
469
+ }.freeze,
470
+ }.freeze
471
+
472
+
473
+ ###############################################
474
+ # Search for a case
475
+ #
476
+ # @param query [Hash] a query
477
+ #
478
+ def case_search(query)
479
+ Validate.validate(query, 'Case Search'.freeze, ValCaseSearch)
480
+ @cache.case_search(query)
481
+ end
482
+
483
+
484
+ # Validate a log search
485
+ ValLogSearch = {
486
+ method: :hash,
487
+ optional: {
488
+ caseid: Items::FieldCaseid,
489
+ after: Validate::IsIntPos,
490
+ before: Validate::IsIntPos,
491
+ user: Items::FieldUsergrp,
492
+ entry: Validate::IsIntPos,
493
+ index: Validate::IsIntPos,
494
+ action: Validate::IsIntPos,
495
+ size: ValSize,
496
+ page: ValPage,
497
+ purpose: ValPurpose,
498
+ sort: {
499
+ method: :string,
500
+ allowed: Set[
501
+ 'time_desc'.freeze,
502
+ 'time_asc'.freeze,
503
+ ].freeze,
504
+ whitelist: true,
505
+ }.freeze
506
+ }.freeze
507
+ }.freeze
508
+
509
+
510
+ ###############################################
511
+ # Search for a log
512
+ #
513
+ # @param query [Hash] a query
514
+ #
515
+ def log_search(query)
516
+ Validate.validate(query, 'Log Search'.freeze, ValLogSearch)
517
+ @cache.log_search(query)
518
+ end
519
+
520
+
521
+ # Validate an entry search query
522
+ ValEntrySearch = {
523
+ method: :hash,
524
+ optional: {
525
+ title: Items::FieldTitle,
526
+ content: Items::FieldContent,
527
+ tags: Items::FieldTagAny,
528
+ caseid: Items::FieldCaseid,
529
+ action: Validate::IsIntPos,
530
+ index: Validate::IsIntPos,
531
+ after: Validate::IsIntPos,
532
+ before: Validate::IsIntPos,
533
+ stat: Items::FieldStat,
534
+ credit: Items::FieldUsergrp,
535
+ size: ValSize,
536
+ page: ValPage,
537
+ purpose: ValPurpose,
538
+ sort: {
539
+ method: :string,
540
+ allowed: Set[
541
+ 'time_desc'.freeze,
542
+ 'time_asc'.freeze,
543
+ ].freeze,
544
+ whitelist: true,
545
+ }.freeze
546
+ }.freeze
547
+ }.freeze
548
+
549
+
550
+
551
+ ###############################################
552
+ # Search for entries
553
+ #
554
+ def entry_search(query)
555
+ Validate.validate(query, 'Entry Search'.freeze, ValEntrySearch)
556
+
557
+ # check permissions
558
+ # - have global search permissions / read access to the case
559
+ # - are searching for an action they can read
560
+ unless( _search?(query) || (query[:caseid] &&
561
+ query[:action] && tasked?(query[:caseid], query[:action])))
562
+ raise(Error::Perms, 'Does not have permission to search'.freeze)
563
+ end
564
+
565
+ # run the query
566
+ res = @cache.entry_search(query)
567
+
568
+ # check perms for each entry
569
+ res[:list].each do |se|
570
+ ent = se[:object]
571
+
572
+ # can not read the case/action - basically nothing
573
+ unless _can_read?(ent[:caseid], ent[:action])
574
+ ent[:time] = nil
575
+ ent[:title] = nil
576
+ ent[:perms] = nil
577
+ ent[:action] = nil
578
+ ent[:tags] = nil
579
+ ent[:files] = nil
580
+ ent[:stats] = nil
581
+ se[:snippet] = nil
582
+ next
583
+ end
584
+
585
+ # can read the case/action, missing perms for this entry
586
+ # leave time, perms, and action
587
+ al = access_list(ent[:caseid])
588
+ if !(Set.new(ent[:perms]) - al).empty?
589
+ ent[:title] = nil
590
+ ent[:tags] = nil
591
+ ent[:files] = nil
592
+ ent[:stats] = nil
593
+ se[:snippet] = nil
594
+ end
595
+ end
596
+
597
+ return res
598
+ end # def entry_search()
599
+
600
+
601
+ # Validate a task search
602
+ ValActionSearch = {
603
+ method: :hash,
604
+ required: {
605
+ assigned: {
606
+ method: :any,
607
+ check: [
608
+ Items::FieldUsergrp,
609
+ {
610
+ method: :equals,
611
+ check: ICFS::UserCase
612
+ }
613
+ ].freeze
614
+ }.freeze
615
+ }.freeze,
616
+ optional: {
617
+ caseid: Items::FieldCaseid,
618
+ title: Items::FieldTitle,
619
+ status: Validate::IsBoolean,
620
+ flag: Validate::IsBoolean,
621
+ before: Validate::IsIntPos,
622
+ after: Validate::IsIntPos,
623
+ tags: Items::FieldTagAny,
624
+ size: ValSize,
625
+ page: ValPage,
626
+ purpose: ValPurpose,
627
+ sort: {
628
+ method: :string,
629
+ allowed: Set[
630
+ 'time_desc'.freeze,
631
+ 'time_asc'.freeze,
632
+ ].freeze,
633
+ whitelist: true,
634
+ }.freeze
635
+ }.freeze
636
+ }.freeze
637
+
638
+
639
+ ###############################################
640
+ # Search for actions
641
+ #
642
+ def action_search(query)
643
+ Validate.validate(query, 'Action Search'.freeze, ValActionSearch)
644
+
645
+ # permissions check
646
+ # - have global search permissions / read access to the case
647
+ # - searching for role you have
648
+ unless( _search?(query) || @ur.include?(query[:assigned]) ||
649
+ (query[:assigned] == ICFS::UserCase && query[:caseid] &&
650
+ access_list(query[:caseid]).include?(ICFS::PermAction) ))
651
+ raise(Error::Perms, 'Does not have permission to search'.freeze)
652
+ end
653
+
654
+ # run the search
655
+ return @cache.action_search(query)
656
+ end # def action_search()
657
+
658
+
659
+ # Validate an index search query
660
+ ValIndexSearch = {
661
+ method: :hash,
662
+ optional: {
663
+ caseid: Items::FieldCaseid,
664
+ title: Items::FieldTitle,
665
+ prefix: Items::FieldTitle,
666
+ content: Items::FieldContent,
667
+ tags: Items::FieldTagAny,
668
+ size: ValSize,
669
+ page: ValPage,
670
+ purpose: ValPurpose,
671
+ sort: {
672
+ method: :string,
673
+ allowed: Set[
674
+ 'title_desc'.freeze,
675
+ 'title_asc'.freeze,
676
+ 'index_desc'.freeze,
677
+ 'index_asc'.freeze,
678
+ ].freeze,
679
+ whitelist: true,
680
+ }.freeze
681
+ }.freeze
682
+ }.freeze
683
+
684
+
685
+ ###############################################
686
+ # Search for indexes
687
+ #
688
+ def index_search(query)
689
+ Validate.validate(query, 'Index Search'.freeze, ValIndexSearch)
690
+
691
+ # permissions check
692
+ # - have global search permissions / read access to the case
693
+ unless _search?(query)
694
+ raise(Error::Perms, 'Do not have permission to search'.freeze)
695
+ end
696
+
697
+ # run the query
698
+ res = @cache.index_search(query)
699
+
700
+ # check perms for each index
701
+ res[:list].each do |se|
702
+ idx = se[:object]
703
+
704
+ unless access_list(idx[:caseid].include?(ICFS::PermRead))
705
+ idx[:title] = nil
706
+ idx[:tags] = nil
707
+ end
708
+ end
709
+
710
+ return res
711
+ end
712
+
713
+
714
+ # validate the stats query
715
+ ValStatsSearch = {
716
+ method: :hash,
717
+ optional: {
718
+ caseid: Items::FieldCaseid,
719
+ after: Validate::IsIntPos,
720
+ before: Validate::IsIntPos,
721
+ credit: Items::FieldUsergrp,
722
+ purpose: ValPurpose,
723
+ }.freeze
724
+ }.freeze
725
+
726
+
727
+ ###############################################
728
+ # Analyze stats
729
+ #
730
+ def stats(query)
731
+ Validate.validate(query, 'Stats Search'.freeze, ValStatsSearch)
732
+
733
+ # permissions check
734
+ # - have global search permissions / read access to the case
735
+ # - are searching for a user/role/group you have
736
+ unless _search?(query) || (query[:credit] && @urg.include?(query[:credit]))
737
+ raise(Error::Perms, 'Do not have permissions to search'.freeze)
738
+ end
739
+
740
+ @cache.stats(query)
741
+ end
742
+
743
+
744
+ # Case Tags search validation
745
+ ValCaseTags = {
746
+ method: :hash,
747
+ optional: {
748
+ status: Validate::IsBoolean,
749
+ template: Validate::IsBoolean,
750
+ grantee: Items::FieldUsergrp,
751
+ purpose: ValPurpose,
752
+ }.freeze,
753
+ }.freeze
754
+
755
+
756
+ ###############################################
757
+ # Get case tags
758
+ #
759
+ def case_tags(query)
760
+ Validate.validate(query, 'Case Tags Search'.freeze, ValCaseTags)
761
+ return @cache.case_tags(query)
762
+ end # def case_tags()
763
+
764
+
765
+ # Entry Tags search validation
766
+ ValEntryTags = {
767
+ method: :hash,
768
+ required: {
769
+ caseid: Items::FieldCaseid,
770
+ }.freeze,
771
+ optional: {
772
+ purpose: ValPurpose,
773
+ }.freeze,
774
+ }.freeze
775
+
776
+
777
+ ###############################################
778
+ # Get entry tags
779
+ #
780
+ def entry_tags(query)
781
+ Validate.validate(query, 'Entry Tags Search'.freeze, ValEntryTags)
782
+
783
+ # permissions
784
+ # - read access to case
785
+ unless access_list(query[:caseid]).include?(ICFS::PermRead)
786
+ raise(Error::Perms, 'missing perms: %s'.freeze % ICFS::PermRead)
787
+ end
788
+ return @cache.entry_tags(query)
789
+ end # def entry_tags()
790
+
791
+
792
+ # Task Tags search validation
793
+ ValActionTags = {
794
+ method: :hash,
795
+ required: {
796
+ assigned: {
797
+ method: :any,
798
+ check: [
799
+ Items::FieldUsergrp,
800
+ {
801
+ method: :equals,
802
+ check: ICFS::UserCase
803
+ }.freeze
804
+ ].freeze
805
+ }.freeze,
806
+ }.freeze,
807
+ optional: {
808
+ caseid: Items::FieldCaseid,
809
+ status: Validate::IsBoolean,
810
+ flag: Validate::IsBoolean,
811
+ before: Validate::IsIntPos,
812
+ after: Validate::IsIntPos,
813
+ purpose: ValPurpose,
814
+ }.freeze,
815
+ }.freeze
816
+
817
+
818
+ ###############################################
819
+ # Get action tags
820
+ #
821
+ def action_tags(query)
822
+ Validate.validate(query, 'Task Tags Search'.freeze, ValActionTags)
823
+
824
+ # only allow searches for user/roles you have
825
+ unless @ur.include?(query[:assigned]) ||
826
+ (query[:assigned] == ICFS::UserCase && query[:caseid] &&
827
+ access_list(query[:caseid]).include?(ICFS::PermAction) )
828
+ raise(Error::Perms, 'May not search for other\'s tasks'.freeze)
829
+ end
830
+
831
+ # run the search
832
+ return @cache.action_tags(query)
833
+ end # def action_tags()
834
+
835
+
836
+ # Validate a index tag search
837
+ ValIndexTags = {
838
+ method: :hash,
839
+ required: {
840
+ caseid: Items::FieldCaseid,
841
+ }.freeze,
842
+ optional: {
843
+ purpose: ValPurpose,
844
+ }.freeze
845
+ }.freeze
846
+
847
+
848
+ ###############################################
849
+ # Get index tags
850
+ #
851
+ def index_tags(query)
852
+ Validate.validate(query, 'Index Tags'.freeze, ValIndexTags)
853
+ unless access_list(query[:caseid]).include?(ICFS::PermRead)
854
+ raise(Error::Perms, 'missing perms: %s'.freeze % ICFS::PermRead)
855
+ end
856
+ return @cache.index_tags(query)
857
+ end
858
+
859
+
860
+ ##############################################################
861
+ # Record
862
+ ##############################################################
863
+
864
+
865
+ ###############################################
866
+ # Create a new case
867
+ #
868
+ # @param ent [Hash] the first entry
869
+ # @param cse [Hash] the case
870
+ # @param tid [String] the template name
871
+ #
872
+ def case_create(ent, cse, tid=nil)
873
+
874
+ ####################
875
+ # Sanity checks
876
+
877
+ # form & values
878
+ Validate.validate(ent, 'entry'.freeze, Items::ItemEntryNew)
879
+ Validate.validate(cse, 'case'.freeze, Items::ItemCaseEdit)
880
+
881
+ # access users/roles/groups are valid
882
+ cse["access"].each do |acc|
883
+ acc["grant"].each do |gnt|
884
+ urg = @users.read(gnt)
885
+ if !urg
886
+ raise(Error::NotFound, 'User/role/group %s not found'.freeze % urg)
887
+ end
888
+ end
889
+ end
890
+
891
+ # permissions
892
+ perms = Set[ ICFS::PermManage ]
893
+ perms.merge(ent['perms']) if ent['perms']
894
+
895
+ # template
896
+ if tid
897
+ tmpl = case_read(tid)
898
+ unless tmpl['template']
899
+ raise(Error::Perms, 'Not a template'.freeze)
900
+ end
901
+
902
+ al = access_list(tid)
903
+ unless al.include?(ICFS::PermManage)
904
+ raise(Error::Perms, 'May not create cases from this template'.freeze)
905
+ end
906
+ end
907
+
908
+ # no action/indexes
909
+ if ent['action']
910
+ raise(Error::Value, 'No Action for a new case entry'.freeze)
911
+ end
912
+ if ent['index']
913
+ raise(Error::Value, 'No Index for a new case entry'.freeze)
914
+ end
915
+
916
+
917
+ ####################
918
+ # Prep
919
+
920
+ # case
921
+ cid = ent['caseid']
922
+ cse['icfs'] = 1
923
+ cse['caseid'] = cid
924
+ cse['log'] = 1
925
+ cse['tags'] ||= [ ICFS::TagNone ]
926
+ citem = Validate.generate(cse, 'case'.freeze, Items::ItemCase)
927
+
928
+ # entry
929
+ ent['icfs'] = 1
930
+ ent['entry'] = 1
931
+ ent['log'] = 1
932
+ ent['tags'] ||= [ ]
933
+ ent['tags'] << ICFS::TagCase
934
+ ent['user'] = @user
935
+ files, fhash = _pre_files(ent)
936
+
937
+ # log
938
+ log = {
939
+ 'icfs' => 1,
940
+ 'caseid' => cid,
941
+ 'log' => 1,
942
+ 'prev' => '0'*64,
943
+ 'user' => @user,
944
+ 'entry' => {
945
+ 'num' => 1,
946
+ },
947
+ 'case_hash' => ICFS.hash(citem),
948
+ }
949
+ log['files_hash'] = fhash if fhash
950
+
951
+ # current
952
+ cur = {
953
+ 'icfs' => 1,
954
+ 'caseid' => cid,
955
+ 'log' => 1,
956
+ 'entry' => 1,
957
+ 'action' => 0,
958
+ 'index' => 0
959
+ }
960
+
961
+ ####################
962
+ # Write the case
963
+
964
+ # take lock
965
+ @cache.lock_take(cid)
966
+ begin
967
+ if @cache.case_read(cid)
968
+ raise(Error::Conflict, 'Case already exists'.freeze)
969
+ end
970
+
971
+ now = Time.now.to_i
972
+
973
+ # finish items
974
+ ent['time'] ||= now
975
+ ent['files'].each{|fi| fi['log'] ||= 1 } if ent['files']
976
+ eitem = Validate.generate(ent, 'entry'.freeze, Items::ItemEntry)
977
+ log['time'] = now
978
+ log['entry']['hash'] = ICFS.hash(eitem)
979
+ litem = Validate.generate(log, 'log'.freeze, Items::ItemLog)
980
+ cur['hash'] = ICFS.hash(litem)
981
+ nitem = Validate.generate(cur, 'current'.freeze, Items::ItemCurrent)
982
+
983
+ # write to cache
984
+ @cache.entry_write(cid, 1, eitem)
985
+ @cache.log_write(cid, 1, litem)
986
+ @cache.case_write(cid, citem)
987
+ @cache.current_write(cid, nitem)
988
+
989
+ # write to store
990
+ @store.entry_write(cid, 1, 1, eitem)
991
+ @store.log_write(cid, 1, litem)
992
+ @store.case_write(cid, 1, citem)
993
+
994
+ # release lock
995
+ ensure
996
+ @cache.lock_release(cid)
997
+ end
998
+
999
+ # files
1000
+ files.each_index{|ix| @store.file_write(cid, 1, 1, ix+1, files[ix]) }
1001
+
1002
+ end # def case_create()
1003
+
1004
+
1005
+ ###############################################
1006
+ # Write items to a case
1007
+ #
1008
+ # @param ent [Hash] Entry to record, required
1009
+ # @param act [Hash, Nilclass] Action to record, optional
1010
+ # @param idx [Hash, Nilclass] Index to record, optional
1011
+ # @param cse [Hash, Nilclass] Case to record, optional
1012
+ #
1013
+ def record(ent, act, idx, cse)
1014
+
1015
+ ####################
1016
+ # Sanity checks
1017
+
1018
+ # form & content
1019
+ if idx || cse
1020
+ Validate.validate(ent, 'New Entry'.freeze, Items::ItemEntryNew)
1021
+ else
1022
+ Validate.validate(ent, 'Editable Entry'.freeze, Items::ItemEntryEdit)
1023
+ end
1024
+ Validate.validate(act, 'action'.freeze, Items::ItemActionEdit) if act
1025
+ Validate.validate(idx, 'index'.freeze, Items::ItemIndexEdit) if idx
1026
+ Validate.validate(cse, 'case'.freeze, Items::ItemCaseEdit) if cse
1027
+
1028
+ # edit index OR case, not both
1029
+ if idx && cse
1030
+ raise(Error::Value, 'May not edit both case and index at once'.freeze)
1031
+ end
1032
+
1033
+ # no changing the action
1034
+ if act && ent['action'] && act['action'] && act['action'] != ent['action']
1035
+ raise(Error::Conflict, 'May not change entry\'s action'.freeze)
1036
+ end
1037
+
1038
+ # access users/roles/groups are valid
1039
+ if cse
1040
+ cse['access'].each do |acc|
1041
+ acc['grant'].each do |gnt|
1042
+ urg = @users.read(gnt)
1043
+ if !urg
1044
+ raise(Error::NotFound, 'User/role/group %s not found'.freeze % gnt)
1045
+ end
1046
+ end
1047
+ end
1048
+ end
1049
+
1050
+ # tasking users/roles are valid
1051
+ if act
1052
+ act['tasks'].each_index do |ix|
1053
+ next if ix == 0
1054
+ tsk = act['tasks'][ix]
1055
+ ur = @users.read(tsk['assigned'])
1056
+ if !ur
1057
+ raise(Error::NotFound, 'User/role %s not found'.freeze %
1058
+ tsk['assigned'])
1059
+ end
1060
+ type = ur['type']
1061
+ if type != 'user' && type != 'role'
1062
+ raise(Error::Values, 'Not a user or role: %s'.freeze %
1063
+ tsk['assigned'])
1064
+ end
1065
+ end
1066
+ end
1067
+
1068
+
1069
+ ####################
1070
+ # Prep
1071
+ cid = ent['caseid']
1072
+
1073
+ # entry
1074
+ ent['icfs'] = 1
1075
+ ent['tags'] ||= [ ]
1076
+ ent['user'] = @user
1077
+ files, fhash = _pre_files(ent)
1078
+
1079
+ # action
1080
+ if act
1081
+ ent['tags'] << ICFS::TagAction
1082
+ act['icfs'] = 1
1083
+ act['caseid'] = cid
1084
+ act['tasks'].each do |tk|
1085
+ tk['tags'] ||= [ ICFS::TagNone ]
1086
+ end
1087
+ end
1088
+
1089
+ # index
1090
+ if idx
1091
+ ent['tags'] << ICFS::TagIndex
1092
+ idx['icfs'] = 1
1093
+ idx['caseid'] = cid
1094
+ idx['tags'] ||= [ ICFS::TagNone ]
1095
+ end
1096
+
1097
+ # case
1098
+ if cse
1099
+ ent['tags'] << ICFS::TagCase
1100
+ cse['icfs'] = 1
1101
+ cse['caseid'] = cid
1102
+ cse['tags'] ||= [ ICFS::TagNone ]
1103
+ end
1104
+
1105
+ # log
1106
+ log = {
1107
+ 'icfs' => 1,
1108
+ 'caseid' => cid,
1109
+ 'user' => @user,
1110
+ }
1111
+ log['files_hash'] = fhash if fhash
1112
+
1113
+ # no tags
1114
+ ent['tags'] = [ ICFS::TagNone ] if ent['tags'].empty?
1115
+
1116
+ # current
1117
+ nxt = {
1118
+ 'icfs' => 1,
1119
+ 'caseid' => cid,
1120
+ }
1121
+
1122
+
1123
+ ####################
1124
+ # Write
1125
+
1126
+ # take lock
1127
+ @cache.lock_take(cid)
1128
+ begin
1129
+ now = Time.now.to_i
1130
+
1131
+ ####################
1132
+ # get prior items & numbers
1133
+
1134
+ # current
1135
+ json = @cache.current_read(cid)
1136
+ cur = Validate.parse(json, 'current'.freeze, Items::ItemCurrent)
1137
+
1138
+ # entry
1139
+ if ent['entry']
1140
+ enum = ent['entry']
1141
+ json = @cache.entry_read(cid, enum)
1142
+ ent_pri = Validate.parse(json, 'entry'.freeze, Items::ItemEntry)
1143
+ nxt['entry'] = cur['entry']
1144
+ else
1145
+ enum = cur['entry'] + 1
1146
+ nxt['entry'] = enum
1147
+ end
1148
+
1149
+ # action
1150
+ if ent_pri && ent_pri['action']
1151
+ anum = ent_pri['action']
1152
+ elsif act && act['action']
1153
+ anum = act['action']
1154
+ elsif ent['action']
1155
+ anum = ent['action']
1156
+ end
1157
+ if anum
1158
+ json = @cache.action_read(cid, anum)
1159
+ act_pri = Validate.parse(json, 'action'.freeze, Items::ItemAction)
1160
+ nxt['action'] = cur['action']
1161
+ elsif act
1162
+ anum = cur['action'] + 1
1163
+ nxt['action'] = anum
1164
+ else
1165
+ nxt['action'] = cur['action']
1166
+ end
1167
+
1168
+ # index
1169
+ if idx
1170
+ if idx['index']
1171
+ xnum = idx['index']
1172
+ nxt['index'] = cur['index']
1173
+ else
1174
+ xnum = cur['index'] + 1
1175
+ nxt['index'] = xnum
1176
+ end
1177
+ else
1178
+ xnum = nil
1179
+ nxt['index'] = cur['index']
1180
+ end
1181
+
1182
+ # case
1183
+ cse_pri = case_read(cid)
1184
+ al = access_list(cid)
1185
+
1186
+ # log
1187
+ lnum = cur['log'] + 1
1188
+ nxt['log'] = lnum
1189
+
1190
+
1191
+ ####################
1192
+ # Checks
1193
+ perms = Set.new
1194
+
1195
+ # entry
1196
+ perms.merge(ent['perms']) if ent['perms']
1197
+ if ent_pri
1198
+
1199
+ # must have those perms
1200
+ perms.add(ent_pri['perms']) if ent_pri['perms']
1201
+
1202
+ # may not change action
1203
+ if ent_pri['action'] && (ent['action'] != ent_pri['action'])
1204
+ raise(Error::Conflict, 'May not change entry\'s action'.freeze)
1205
+ end
1206
+
1207
+ # may not remove or add action, index, case tags
1208
+ if( (ent_pri['tags'].include?(ICFS::TagAction) !=
1209
+ ent['tags'].include?(ICFS::TagAction) ) ||
1210
+ (ent_pri['tags'].include?(ICFS::TagIndex) !=
1211
+ ent['tags'].include?(ICFS::TagIndex) ) ||
1212
+ (ent_pri['tags'].include?(ICFS::TagCase) !=
1213
+ ent['tags'].include?(ICFS::TagCase) ) )
1214
+ raise(Error::Conflict, 'May not change entry\'s special tags'.freeze)
1215
+ end
1216
+ end
1217
+
1218
+ # action
1219
+ if act
1220
+ pri_tsk = act_pri ? act_pri['tasks'] : []
1221
+ cur_tsk = act['tasks']
1222
+ act_open = cur_tsk[0]['status']
1223
+
1224
+ # not allowed to delete tasks
1225
+ if pri_tsk.size > cur_tsk.size
1226
+ raise(Error::Conflict, 'May not delete tasks'.freeze)
1227
+ end
1228
+
1229
+ # check each task
1230
+ perm_act = al.include?(ICFS::PermAction)
1231
+ tasked = false
1232
+ cur_tsk.each_index do |ix|
1233
+ ct = cur_tsk[ix]
1234
+ pt = pri_tsk[ix]
1235
+
1236
+ # may not delete a tasking
1237
+ if pt && pt['assigned'] != ct['assigned']
1238
+ raise(Error::Conflict, 'May not delete task'.freeze)
1239
+ end
1240
+
1241
+ # new taskings require action to be open
1242
+ if !pt && !act_open
1243
+ raise(Error::Value, 'New tasks require the action be open'.freeze)
1244
+ end
1245
+
1246
+ # may not have a task open if action is closed
1247
+ if ct['status'] && !act_open
1248
+ raise(Error::Value, 'Open tasks on closed action'.freeze)
1249
+ end
1250
+
1251
+ # can set any values for our tasks
1252
+ if @ur.include?(ct['assigned']) || (ix == 0 && perm_act )
1253
+ tasked = true
1254
+ next
1255
+ end
1256
+
1257
+ # must be flagged if new tasking or re-opening
1258
+ if !ct['flag'] && (!pt || (ct['status'] && !pt['status']))
1259
+ raise(Error::Value, 'New or re-opened taskings must flag'.freeze)
1260
+ end
1261
+
1262
+ # no changing other's taskings, no deflagging, and no
1263
+ # closing task without action
1264
+ if pt && (
1265
+ (pt['title'] != ct['title']) || (pt['time'] != ct['time']) ||
1266
+ (pt['tags'] != ct['tags']) || (pt['flag'] && !ct['flag']) ||
1267
+ (pt['status'] && !ct['status'] && !perm_act) )
1268
+ raise(Error::Value, 'May not change other\'s tasks'.freeze)
1269
+ end
1270
+ end
1271
+
1272
+ # new tasks or changes to other's tasks
1273
+ if !act_pri || !tasked
1274
+ perms.add( ICFS::PermAction )
1275
+ end
1276
+
1277
+ end
1278
+
1279
+ # no checks for index
1280
+
1281
+ # case
1282
+ if cse
1283
+ # no changing template
1284
+ unless cse['template'] == cse_pri['template']
1285
+ raise(Error::Conflict, 'May not change template status'.freeze)
1286
+ end
1287
+
1288
+ # manage required
1289
+ perms.add( ICFS::PermManage ) if cse
1290
+ end
1291
+
1292
+ # write unless a case or pre-existing action
1293
+ unless cse || act_pri
1294
+ perms.add( ICFS::PermWrite)
1295
+ end
1296
+
1297
+ # permissions
1298
+ perms_miss = perms - al
1299
+ unless perms_miss.empty?
1300
+ raise(Error::Perms, 'Missing perms: %s'.freeze %
1301
+ perms_miss.to_a.sort.join(', ') )
1302
+ end
1303
+
1304
+
1305
+ ####################
1306
+ # Items
1307
+
1308
+ # entry
1309
+ ent['entry'] = enum
1310
+ ent['log'] = lnum
1311
+ ent['time'] ||= now
1312
+ ent['action'] = anum if act
1313
+ if idx
1314
+ if ent['index']
1315
+ ent['index'] = ent['index'].push(xnum).uniq.sort
1316
+ else
1317
+ ent['index'] = [ xnum ]
1318
+ end
1319
+ end
1320
+ ent['files'].each{|fi| fi['log'] ||= lnum } if ent['files']
1321
+ eitem = Validate.generate(ent, 'entry'.freeze, Items::ItemEntry)
1322
+ log['entry'] = {
1323
+ 'num' => enum,
1324
+ 'hash' => ICFS.hash(eitem)
1325
+ }
1326
+
1327
+ # action
1328
+ if act
1329
+ act['action'] = anum
1330
+ act['log'] = lnum
1331
+ aitem = Validate.generate(act, 'action'.freeze, Items::ItemAction)
1332
+ log['action'] = {
1333
+ 'num' => anum,
1334
+ 'hash' => ICFS.hash(aitem)
1335
+ }
1336
+ end
1337
+
1338
+ # index
1339
+ if idx
1340
+ idx['index'] = xnum
1341
+ idx['log'] = lnum
1342
+ xitem = Validate.generate(idx, 'index'.freeze, Items::ItemIndex)
1343
+ log['index'] = {
1344
+ 'num' => xnum,
1345
+ 'hash' => ICFS.hash(xitem)
1346
+ }
1347
+ end
1348
+
1349
+ # case
1350
+ if cse
1351
+ cse['log'] = lnum
1352
+ citem = Validate.generate(cse, 'case'.freeze, Items::ItemCase)
1353
+ log['case_hash'] = ICFS.hash(citem)
1354
+ end
1355
+
1356
+ # log
1357
+ log['log'] = lnum
1358
+ log['prev'] = cur['hash']
1359
+ log['time'] = now
1360
+ litem = Validate.generate(log, 'log'.freeze, Items::ItemLog)
1361
+ nxt['hash'] = ICFS.hash(litem)
1362
+
1363
+ # next
1364
+ nitem = Validate.generate(nxt, 'current'.freeze, Items::ItemCurrent)
1365
+
1366
+
1367
+ ####################
1368
+ # Write
1369
+
1370
+ # entry
1371
+ @cache.entry_write(cid, enum, eitem)
1372
+ @store.entry_write(cid, enum, lnum, eitem)
1373
+
1374
+ # action
1375
+ if act
1376
+ @cache.action_write(cid, anum, aitem)
1377
+ @store.action_write(cid, anum, lnum, aitem)
1378
+ end
1379
+
1380
+ # index
1381
+ if idx
1382
+ @cache.index_write(cid, xnum, xitem)
1383
+ @store.index_write(cid, xnum, lnum, xitem)
1384
+ end
1385
+
1386
+ # case
1387
+ if cse
1388
+ @cache.case_write(cid, citem)
1389
+ @store.case_write(cid, lnum, citem)
1390
+ end
1391
+
1392
+ # log
1393
+ @cache.log_write(cid, lnum, litem)
1394
+ @store.log_write(cid, lnum, litem)
1395
+
1396
+ # current
1397
+ @cache.current_write(cid, nitem)
1398
+
1399
+ # release the lock
1400
+ ensure
1401
+ @cache.lock_release(cid)
1402
+ end
1403
+
1404
+ # write the files
1405
+ files.each_index{|ix| @store.file_write(cid, enum, lnum, ix+1, files[ix]) }
1406
+
1407
+ end # def record()
1408
+
1409
+
1410
+ ###############################################
1411
+ # Assemble files before taking the lock
1412
+ #
1413
+ def _pre_files(ent)
1414
+
1415
+ files = []
1416
+ if ent.key?('files')
1417
+ fhash = []
1418
+ ent['files'].each do |at|
1419
+ if at.key?('temp')
1420
+ fi = at['temp']
1421
+ at.delete('temp')
1422
+ files << fi
1423
+ at['num'] = files.size
1424
+ fhash << ICFS.hash_temp(fi)
1425
+ end
1426
+ end
1427
+ end
1428
+
1429
+ return [files, fhash]
1430
+ end # def _pre_files()
1431
+ private :_pre_files
1432
+
1433
+
1434
+ end # class ICFS::Api
1435
+
1436
+ end # module ICFS