icfs 0.1.0

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