p4_web_api 2014.2.0.pre2 → 2014.2.0.pre4

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 069a887b3fabfbb41e4014f0351e9ad79e699ee9
4
- data.tar.gz: 6fc582c57a30740878523f1265eade7bd6d24749
3
+ metadata.gz: 7928b3bca740696b9fb622b838ad6dfa0eb06857
4
+ data.tar.gz: abca39306fb28d24b7c986f19d11dcf041973904
5
5
  SHA512:
6
- metadata.gz: a055d9ca2f41db3f61c9f8d3969d8b5ef506759671437d157eec182af6eb8ff18ff0567704203b4c4755b2deff87806cc2867ac98e0f161de93c52245f3c24e2
7
- data.tar.gz: fa442bde271cf8387ac423ae2ba3fc9b8a88d38bd396478998990a8c1c74fb14f37065700acdf0e2276a89562143a25705038a8a85ba95bae5813de9382595da
6
+ metadata.gz: 4f8f49dec8c3fcb072354ae86c4ce6e3f5ea46ece96c4a5052e8a24a7cf43499332374149dd8792302c5152026d2d908f20bac6ce4e1306025342a78c3207c21
7
+ data.tar.gz: b1f84f8cc8e6c1d192b769ca9066d415571349d3f182b49c492605e5c58b3b37511f9e5565dfb8ee584105014d3d1f0f30cdd73a65048b16e84ec4c46bdd6b36
data/bin/p4_web_api CHANGED
@@ -1,4 +1,10 @@
1
1
  #!/usr/bin/env ruby
2
- lib_path = File.expand_path('../../lib/p4_web_api.rb', __FILE__)
3
- require lib_path
2
+
3
+ ENV['RACK_ENV'] = 'production' unless ENV.key?('RACK_ENV')
4
+
5
+ lib_dir = File.expand_path('../../lib', __FILE__)
6
+ $LOAD_PATH.unshift(lib_dir)
7
+
8
+ require 'p4_web_api'
9
+
4
10
  P4WebAPI::App.run!
data/lib/p4_web_api.rb CHANGED
@@ -18,9 +18,10 @@ require 'sinatra/json'
18
18
  require 'sinatra/config_file'
19
19
  require 'rack/parser'
20
20
 
21
- require 'p4_web_api/version'
22
21
  require 'p4_web_api/auth'
22
+ require 'p4_web_api/helpers'
23
23
  require 'p4_web_api/p4_util'
24
+ require 'p4_web_api/version'
24
25
 
25
26
  # The P4WebAPI is our namespace for the web application.
26
27
  #
@@ -95,84 +96,6 @@ module P4WebAPI
95
96
 
96
97
  P4WebAPI::Auth.validate_token_dir(settings.token_path)
97
98
 
98
- helpers do
99
- # A block helper that uses the authentication credentials to create the p4
100
- # connection
101
- def open_p4(&block)
102
- options = {
103
- user: env['AUTH_CREDENTIALS'].first,
104
- password: P4Util.resolve_password(env, settings),
105
- host: P4Util.resolve_host(env, settings),
106
- port: P4Util.resolve_port(env, settings)
107
- }
108
-
109
- charset = P4Util.resolve_charset(env, settings)
110
- options[:charset] = charset if charset
111
-
112
- P4Util.open(options, &block)
113
- end
114
-
115
- # Block helper to open a p4 handle with a temporary client workspace.
116
- # The client workspace will map the series of depot path expressions
117
- # directly into the client workspace.
118
- #
119
- # The depot_paths are generally expected to be complete file path
120
- # expressions, e.g., '//depot/dir1/dir2/file'. They'll be mapped directly
121
- # to the temporary working area: '//client/depot/dir/1/dir2/file'
122
- #
123
- # The block handler here will be called with the arguments (p4, root),
124
- # where root is the temporary client's root directory.
125
- def open_p4_temp_client(depot_paths, &block)
126
- open_p4 do |p4|
127
- name = (0...8).map { (65 + rand(26)).chr }.join
128
- dir = init_temp_workspace_dir(name)
129
- init_temp_client(p4, name, dir, depot_paths)
130
-
131
- ex = nil
132
- begin
133
- block.call(p4, dir)
134
- rescue StandardError => e
135
- ex = e
136
- end
137
-
138
- delete_temp_client(p4, name, dir)
139
-
140
- fail ex unless ex.nil?
141
- end
142
- end
143
- end
144
-
145
- def init_temp_workspace_dir(name)
146
- dir = File.join(settings.workspace_folder, name)
147
- unless Dir.exist?(dir)
148
- FileUtils.mkpath(dir)
149
- FileUtils.chmod(0700, dir)
150
- end
151
- dir
152
- end
153
-
154
- def init_temp_client(p4, name, dir, depot_paths)
155
- spec = p4.fetch_client
156
- spec._root = dir
157
- spec._client = name
158
- spec._description = 'p4_web_api temp client'
159
-
160
- spec._view = depot_paths.map do |path|
161
- stripped = path.gsub(/^[\/]*/, '')
162
- "\"//#{stripped}\" \"//#{name}/#{stripped}\""
163
- end
164
-
165
- p4.save_client(spec)
166
- p4.client = name
167
-
168
- p4.run_sync('//...')
169
- end
170
-
171
- def delete_temp_client(p4, name, dir)
172
- p4.run_client('-d', '-f', name)
173
- FileUtils.rmtree(dir)
174
- end
175
-
176
99
  error do
177
100
  err = env['sinatra.error']
178
101
 
@@ -201,6 +124,16 @@ module P4WebAPI
201
124
  fail err
202
125
  end
203
126
 
127
+ not_found do
128
+ return {
129
+ # This is not an application error, so I'm using error code 0 to mean
130
+ # 'use HTTP status'
131
+ MessageCode: 0,
132
+ MessageSeverity: 3,
133
+ MessageText: 'Resource not found'
134
+ }.to_json
135
+ end
136
+
204
137
  def to_msg(message)
205
138
  {
206
139
  MessageCode: message.msgid,
@@ -209,6 +142,8 @@ module P4WebAPI
209
142
  }
210
143
  end
211
144
 
145
+ helpers Helpers
146
+
212
147
  # Will fetch the system offset based on the server date.
213
148
  #
214
149
  # If allow_env_p4_config is true, we don't cache. Each request could come
@@ -249,597 +184,6 @@ module P4WebAPI
249
184
  self.class.normalizers[spec_type].call(*args)
250
185
  end
251
186
 
252
- # Authentication methods
253
- # See also: https://confluence.perforce.com:8443/display/WS/Authentication+in+Web+Services
254
-
255
- # Creates a sign-in session for the user.
256
- #
257
- # This session returns a token that should be used as the password in basic
258
- # authentication for the user later.
259
- post '/v1/sessions' do
260
- login = params[:login]
261
- password = params[:password] # may be a p4 ticket
262
-
263
- token = nil
264
-
265
- options = {
266
- user: login,
267
- password: password,
268
- host: settings.p4['host'],
269
- port: settings.p4['port']
270
- }
271
- options[:charset] = settings.p4['charset'] if settings.p4.key?('charset')
272
-
273
- P4Util.open(options) do |p4|
274
- token = Auth.create_session(p4, password, settings)
275
- end
276
-
277
- # We signal a 401 when login/passwords are generally invalid. Since this
278
- # is an unauthenticated request, you shouldn't be able to tell if the
279
- # login doesn't exist or the password is incorrect.
280
- halt 401 unless token
281
-
282
- content_type 'text/plain'
283
- return token
284
- end
285
-
286
- # I'm not sure if we should ensure the token being deleted is the token
287
- # being authenticated against. That's not being checked for the time being.
288
- delete '/v1/sessions/:token' do |token|
289
- P4WebAPI::Auth.delete_session(token, settings)
290
- ''
291
- end
292
-
293
- # Generic 'do a perforce command' methods.
294
- #
295
- # These do not ensure true REST-ful style behavior, but are really just
296
- # "hey use HTTP to interact with Perforce".
297
-
298
- get '/v1/run' do
299
-
300
- cmd = params[:cmd]
301
-
302
- args = params.select { |key, _| key.start_with?('arg') }
303
- .map { |_, value| value }
304
-
305
- if settings.run_get_blacklist.include?(cmd)
306
- halt 403, { MessageCode: 15_360,
307
- MessageText: "#{cmd} not allowed in web api",
308
- MessageSeverity: :ERROR }.to_json
309
- end
310
-
311
- results = nil
312
- messages = nil
313
-
314
- open_p4 do |p4|
315
- results = p4.run(cmd, *args)
316
- messages = p4.messages
317
- end
318
-
319
- if messages && messages.length > 0
320
- messages.map { |m| to_msg(m) }.to_json
321
- elsif results
322
- results.to_json
323
- end
324
-
325
- end
326
-
327
- post '/v1/run' do
328
- cmd = params[:cmd]
329
-
330
- args = params.select { |key, _| key.start_with?('arg') }
331
- .map { |_, value| value }
332
-
333
- # Uses the same blacklist as GET which generally makes sense, since we'll
334
- # only really be concerned about client workspace usage on this server.
335
- if settings.run_get_blacklist.include?(cmd)
336
- halt 403, { MessageCode: 15_360,
337
- MessageText: "#{cmd} not allowed in web api",
338
- MessageSeverity: :ERROR }.to_json
339
- end
340
-
341
- messages = nil
342
-
343
- open_p4 do |p4|
344
- p4.input = filter_params(params)
345
- p4.run(cmd, args)
346
- messages = p4.messages
347
- end
348
-
349
- messages.map { |m| to_msg(m) }.to_json if messages
350
- end
351
-
352
- # Convenience method to the 'run' mechanism for typical browsing purposes.
353
- #
354
- # Unlike just standard run commands, this will select either the 'depots' or
355
- # 'files' and 'dirs' commands for a single directory level at a time.
356
-
357
- # Special depots only variant
358
- get '/v1/files' do
359
- results = nil
360
-
361
- open_p4 do |p4|
362
- results = p4.run_depots
363
- end
364
-
365
- normalize_depots(results) if settings.normalize_output
366
-
367
- results.to_json
368
- end
369
-
370
- # General browsing variation.
371
- get '/v1/files/*' do
372
- dirs = params[:splat].select { |x| !x.empty? }
373
-
374
- results = nil
375
-
376
- open_p4 do |p4|
377
- if dirs.empty?
378
- results = p4.run_depots
379
- normalize_depots(results) if settings.normalize_output
380
- else
381
- selector = '//' + dirs.join('/') + '/*'
382
- files_results = p4.run_files(selector)
383
- normalize_files(files_results) if settings.normalize_output
384
- dirs_results = p4.run_dirs(selector)
385
- normalize_dirs(dirs_results) if settings.normalize_output
386
- results = files_results + dirs_results
387
- end
388
- end
389
-
390
- results.to_json
391
- end
392
-
393
- # Convenience method to list changelist metadata with some common filtering
394
- # options.
395
- #
396
- # parameters:
397
- # - `max` = number
398
- # - `status` = pending|submitted|shelved
399
- # - `user` = [login]
400
- # - `files` = pattern
401
- get '/v1/changes' do
402
- max = params['max'] if params.key?('max')
403
- status = params['status'] if params.key?('status')
404
- user = params['user'] if params.key?('user')
405
- files = params['files'] if params.key?('files')
406
-
407
- results = nil
408
-
409
- open_p4 do |p4|
410
- args = ['changes']
411
-
412
- args.push('-m', max) if max
413
- args.push('-s', status) if status
414
- args.push('-u', user) if user
415
- args.push(files) if files
416
-
417
- results = p4.run(*args)
418
- end
419
-
420
- normalize_changes(results) if settings.normalize_output
421
-
422
- results.to_json
423
- end
424
-
425
- # Methods to manipulate the protections table. This blindly assumes you
426
- # indeed have superuser privileges.
427
-
428
- # Just list all protections
429
- get '/v1/protections' do
430
- protects = nil
431
- open_p4 do |p4|
432
- protects = p4.run_protect('-o').first
433
- end
434
-
435
- normalize_protections(results) if settings.normalize_output
436
-
437
- protects.to_json
438
- end
439
-
440
- # Update protections
441
- put '/v1/protections' do
442
- protects = {
443
- 'Protections' => params['Protections']
444
- }
445
-
446
- open_p4 do |p4|
447
- p4.input = protects
448
- p4.run_protect('-i')
449
- end
450
-
451
- ''
452
- end
453
-
454
- # Methods to adjust the triggers table.
455
-
456
- get '/v1/triggers' do
457
- triggers = nil
458
- open_p4 do |p4|
459
- triggers = p4.run_triggers('-o').first
460
- end
461
-
462
- normalize_triggers(results) if settings.normalize_output
463
-
464
- triggers.to_json
465
- end
466
-
467
- # Update protections
468
- put '/v1/triggers' do
469
- triggers = {
470
- 'Triggers' => params['Triggers']
471
- }
472
-
473
- open_p4 do |p4|
474
- p4.input = triggers
475
- p4.run_triggers('-i')
476
- end
477
-
478
- ''
479
- end
480
-
481
- # Generate CRUD methods for each 'spec' type
482
- #
483
-
484
- set(:is_spec) do |_x|
485
- condition do
486
- path_info = env['PATH_INFO']
487
-
488
- matches = %r{^/v1/(?<spec_type>\w+)}.match(path_info)
489
- if matches
490
- spec_type = matches[:spec_type]
491
- return (spec_type == 'branches' ||
492
- spec_type == 'clients' ||
493
- spec_type == 'depots' ||
494
- spec_type == 'groups' ||
495
- spec_type == 'jobs' ||
496
- spec_type == 'labels' ||
497
- # No protects here: the usage is pretty different from the other
498
- # spec mechanisms
499
- spec_type == 'servers'
500
- # No streams, triggers, or users here: it does not work like other
501
- # specs, so these method implementations don't quite work.
502
- )
503
- end
504
- false
505
- end
506
- end
507
-
508
- # Provide a generic collection accessor for each of the specs.
509
- get '/v1/:spec_type', is_spec: true do |spec_type|
510
- results = nil
511
-
512
- open_p4 do |p4|
513
- results = p4.run(spec_type)
514
- end
515
-
516
- send("normalize_#{spec_type}", results) if settings.normalize_output
517
- P4Util.collate_group_results(results) if settings.normalize_output &&
518
- spec_type == 'groups'
519
-
520
- results.to_json
521
- end
522
-
523
- # Provide a generic output accessor for each spec
524
- get '/v1/:spec_type/:id', is_spec: true do |spec_type, id|
525
- results = nil
526
-
527
- open_p4 do |p4|
528
- results = p4.run(P4Util.singular(spec_type), '-o', id)
529
- end
530
-
531
- send("normalize_#{spec_type}", results) if settings.normalize_output
532
-
533
- results[0].to_json
534
- end
535
-
536
- # This is our generic "add" mechanism for each type.
537
- #
538
- # It's assumed that the client understands the requirements of each spec
539
- # type.
540
- post '/v1/:spec_type', is_spec: true do |spec_type|
541
- results = nil
542
-
543
- open_p4 do |p4|
544
- method_name = "save_#{P4Util.singular(spec_type)}".to_sym
545
- results = p4.send(method_name, params)
546
- end
547
-
548
- # In general, the params use the name of the spec as a capitalized
549
- # parameter in the singular form.
550
- if spec_type == 'servers'
551
- id_prop = 'ServerID'
552
- else
553
- id_prop = P4Util.singular(spec_type).capitalize
554
- end
555
- id = nil
556
- if results.is_a?(Array) &&
557
- results.length > 0 &&
558
- /Job .* saved/.match(results[0])
559
- # special "Job" variant to grab the ID out of the results output
560
- id = /Job (.*) saved/.match(results[0])[1]
561
- elsif params.key?(id_prop)
562
- id = params[id_prop]
563
- elsif params.key?(id_prop.to_sym)
564
- id = params[id_prop.to_sym]
565
- end
566
-
567
- if id
568
- redirect "/v1/#{spec_type}/#{id}"
569
- else
570
- halt 400, "Did not locate #{id_prop} in params"
571
- end
572
-
573
- end
574
-
575
- # An 'update' mechanism for each spec type.
576
- put '/v1/:spec_type/:id', is_spec: true do |spec_type, id|
577
- open_p4 do |p4|
578
- singular = P4Util.singular(spec_type)
579
- spec = p4.run(singular, '-o', id)[0]
580
-
581
- spec = spec.merge(filter_params(params))
582
-
583
- method_name = "save_#{singular}".to_sym
584
- p4.send(method_name, spec)
585
- end
586
-
587
- ''
588
- end
589
-
590
- delete '/v1/:spec_type/:id', is_spec: true do |spec_type, id|
591
- open_p4 do |p4|
592
- method_name = "delete_#{P4Util.singular(spec_type)}".to_sym
593
- p4.send(method_name, id)
594
- end
595
-
596
- ''
597
- end
598
-
599
- # List all users registered in the server
600
- get '/v1/users' do
601
- results = nil
602
-
603
- open_p4 do |p4|
604
- results = p4.run_users
605
- end
606
-
607
- normalize_users(results) if settings.normalize_output
608
-
609
- results.to_json
610
- end
611
-
612
- # Create a new user
613
- post '/v1/users' do
614
- open_p4 do |p4|
615
- p4.save_user(params, '-f')
616
- end
617
-
618
- login = params['User']
619
-
620
- redirect "/v1/users/#{login}"
621
- end
622
-
623
- get '/v1/users/:user' do |user|
624
- results = nil
625
-
626
- open_p4 do |p4|
627
- results = p4.run_user('-o', user)
628
- end
629
-
630
- normalize_users(results) if settings.normalize_output
631
-
632
- if results.empty?
633
- halt 404
634
- else
635
- results[0].to_json
636
- end
637
- end
638
-
639
- # 'Update' the user.
640
- put '/v1/users/:user' do |user|
641
- open_p4 do |p4|
642
- results = p4.run_user('-o', user)
643
-
644
- if results.empty?
645
- halt 404
646
- return
647
- end
648
-
649
- spec = results[0]
650
- spec = spec.merge(filter_params(params))
651
-
652
- spec['User'] = user unless spec.key?('User')
653
-
654
- p4.save_user(spec, '-f')
655
- end
656
-
657
- # Avoid 'user jdoe saved' message from going out
658
- ''
659
- end
660
-
661
- # Delete the user.
662
- delete '/v1/users/:user' do |user|
663
- open_p4 do |p4|
664
- p4.run_user('-f', '-d', user)
665
- end
666
-
667
- # If you're deleting yourself (ragequitting via API) remove thy session
668
- if user == env['AUTH_CREDENTIALS'].first
669
- token = env['AUTH_CREDENTIALS'].last
670
- P4WebAPI::Auth.delete_session(token, settings)
671
- end
672
- end
673
-
674
- # Stream manipulation
675
- #
676
- # Streams are a little different from other spec types, in that the singluar
677
- # 'stream' is identified by a depot-style path, as opposed to kind of a
678
- # name. (And since our paths start with two slashes, we ignore those).
679
-
680
- get '/v1/streams' do
681
- streams = nil
682
-
683
- open_p4 do |p4|
684
- streams = p4.run_streams
685
- end
686
-
687
- normalize_streams(streams) if settings.normalize_output
688
-
689
- streams.to_json
690
- end
691
-
692
- # Creates a new stream
693
- post '/v1/streams' do
694
- open_p4 do |p4|
695
- p4.save_stream(filter_params(params))
696
- end
697
-
698
- stream = params['Stream']
699
-
700
- sub_path = stream[2..-1]
701
-
702
- redirect "/v1/streams/#{sub_path}"
703
- end
704
-
705
- get '/v1/streams/*' do
706
- sub_path = params[:splat].join('')
707
-
708
- stream = "//#{sub_path}"
709
-
710
- results = nil
711
-
712
- open_p4 do |p4|
713
- results = p4.run_stream('-o', stream)
714
- end
715
-
716
- normalize_streams(results) if settings.normalize_output
717
-
718
- results[0].to_json
719
- end
720
-
721
- put '/v1/streams/*' do
722
- sub_path = params[:splat].join('')
723
-
724
- stream = "//#{sub_path}"
725
-
726
- open_p4 do |p4|
727
- spec = p4.run_stream('-o', stream)[0]
728
-
729
- spec = spec.merge(filter_params(params))
730
-
731
- p4.save_stream(spec, '-f')
732
- end
733
-
734
- ''
735
- end
736
-
737
- delete '/v1/streams/*' do
738
- sub_path = params[:splat].join('')
739
-
740
- stream = "//#{sub_path}"
741
-
742
- open_p4 do |p4|
743
- p4.run_stream('-d', stream)
744
- end
745
-
746
- ''
747
- end
748
-
749
- # The print method uses the print command underneath to output file content.
750
- #
751
- # Unlike most of the other methods, this does not output application/json.
752
- # If the type contains 'text' we set the Content-Type to 'text/plain',
753
- # otherwise it's 'application/octet-stream'.
754
- get '/v1/print/*' do
755
- path = params[:splat].join('')
756
- path = "//#{path}"
757
-
758
- results = nil
759
- open_p4 do |p4|
760
- results = p4.run_print(path)
761
- end
762
-
763
- file_type = results[0]
764
- content = results[1]
765
-
766
- if file_type['type'] =~ /text/
767
- content_type 'text/plain'
768
- else
769
- content_type 'application/octet-stream'
770
- end
771
-
772
- content
773
- end
774
-
775
- # The upload method adds new file revisions.
776
- #
777
- # This is a multipart method that accepts and array of files, plus,
778
- # a 'mappings' array to indicate the target depot path for each file.
779
- # This should be a JSON encoded array of strings.
780
- #
781
- # An optional 'description' attribute can describe the changes to be made.
782
- post '/v1/upload' do
783
- # if mappings[] and files[] do not have equivalent lengths, the error
784
- # might be kind of lame.
785
- mappings = params[:mappings]
786
- files = []
787
- mappings.each_index { |idx| files << params["file_#{idx}".to_sym] }
788
-
789
- open_p4_temp_client(mappings) do |p4, root|
790
- message = params[:description] || 'Uploaded files'
791
- change_id = init_changelist(p4, message)
792
-
793
- # Information used to determine if the new rev should be an add or edit
794
- files_results = p4.run_files(mappings)
795
-
796
- (0...mappings.length).each do |idx|
797
- type = existing_path?(files_results, mappings[idx]) ? 'edit' : 'add'
798
-
799
- if type == 'edit'
800
- mark_change('edit', p4, change_id, root, mappings[idx])
801
- save_content(root, mappings[idx], files[idx])
802
- else
803
- save_content(root, mappings[idx], files[idx])
804
- mark_change('add', p4, change_id, root, mappings[idx])
805
- end
806
- end
807
-
808
- p4.run_submit('-c', change_id)
809
- end
810
- end
811
-
812
- def init_changelist(p4, description)
813
- change_spec = p4.fetch_change
814
- change_spec._description = description
815
- results = p4.save_change(change_spec)
816
- results[0].gsub(/^Change (\d+) created./, '\1')
817
- end
818
-
819
- def save_content(root, depot_path, file)
820
- local_file = local_path(depot_path, root)
821
- dir = File.dirname(local_file)
822
- FileUtils.mkpath(dir) unless Dir.exist?(dir)
823
-
824
- IO.write(local_file, file[:tempfile].read)
825
- end
826
-
827
- def existing_path?(existing_results, depot_path)
828
- existing_results.any? do |result|
829
- result['depotFile'] == depot_path
830
- end
831
- end
832
-
833
- def mark_change(type, p4, change_id, root, depot_path)
834
- local_file = local_path(depot_path, root)
835
- p4.run(type, '-c', change_id, local_file)
836
- end
837
-
838
- def local_path(depot_path, root)
839
- stripped = depot_path.gsub(/^\/+/, '')
840
- File.join(root, stripped)
841
- end
842
-
843
187
  # Basically a "blacklist" of things we know the frameworks going to add to
844
188
  # the params array we don't want to pass on to the p4 command sets for spec
845
189
  # input
@@ -852,3 +196,16 @@ module P4WebAPI
852
196
  run! if app_file == $PROGRAM_NAME
853
197
  end
854
198
  end
199
+
200
+ # Reopen up the P4WebAPI::App class and add most of our method handling. This
201
+ # is done in lieu of having multiple Sinatra apps, so we can have the same
202
+ # configuration and error handling.
203
+ require 'p4_web_api/app/changes'
204
+ require 'p4_web_api/app/commands'
205
+ require 'p4_web_api/app/files'
206
+ require 'p4_web_api/app/protections'
207
+ require 'p4_web_api/app/sessions'
208
+ require 'p4_web_api/app/specs'
209
+ require 'p4_web_api/app/streams'
210
+ require 'p4_web_api/app/triggers'
211
+ require 'p4_web_api/app/users'