p4_web_api 2014.2.0.pre2 → 2014.2.0.pre4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,73 @@
1
+ require 'p4_web_api/change_helper'
2
+
3
+ module P4WebAPI
4
+ # Add methods related to changelist resources.
5
+ class App < Sinatra::Base
6
+ # Convenience method to list changelist metadata with some common filtering
7
+ # options.
8
+ #
9
+ # parameters:
10
+ # - `max` = number
11
+ # - `status` = pending|submitted|shelved
12
+ # - `user` = perforce login
13
+ # - `files` = pattern
14
+ get '/v1/changes' do
15
+ max = params['max'] if params.key?('max')
16
+ status = params['status'] if params.key?('status')
17
+ user = params['user'] if params.key?('user')
18
+ files = params['files'] if params.key?('files')
19
+
20
+ results = nil
21
+
22
+ open_p4 do |p4|
23
+ args = ['changes']
24
+
25
+ args.push('-m', max) if max
26
+ args.push('-s', status) if status
27
+ args.push('-u', user) if user
28
+ args.push(files) if files
29
+
30
+ results = p4.run(*args)
31
+ end
32
+
33
+ normalize_changes(results) if settings.normalize_output
34
+
35
+ results.to_json
36
+ end
37
+
38
+ # Create a new changelist with edits to multiple files.
39
+ post '/v1/changes' do
40
+ description = params['Description'] || 'Edited files'
41
+ files = params['Files']
42
+
43
+ depot_paths = files.map { |f| f['DepotFile'] }
44
+
45
+ open_p4_temp_client(depot_paths) do |p4, client_root, client_name|
46
+ helper = ChangeHelper.new(p4: p4,
47
+ client_root: client_root,
48
+ client_name: client_name,
49
+ description: description,
50
+ files: files)
51
+ helper.call
52
+ end
53
+ end
54
+
55
+ # Uses describe to produce the list of opened files regardless of client.
56
+ #
57
+ # There is a little bit of an open question regarding performance. The
58
+ # p4ruby usage here does *not* generate diffs of changes, which the base
59
+ # command line seems to want to do. If the output accumulates a lot of RAM
60
+ # for large diffs, we could be in trouble.
61
+ get '/v1/changes/:change' do |change|
62
+ results = nil
63
+
64
+ open_p4 do |p4|
65
+ results = p4.run_describe('-S', change)
66
+ end
67
+
68
+ normalize_describe(results) if settings.normalize_output
69
+
70
+ results.to_json
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,58 @@
1
+ require 'sinatra/base'
2
+
3
+ module P4WebAPI
4
+ # Methods for executing generic Perforce commands.
5
+ #
6
+ # These do not actually map to any particular 'resource', but are just a
7
+ # bucket for random things you can try out.
8
+ #
9
+ # thj: I would prefer to remove these methods, and replace them with
10
+ # 'resources' we would define for people.
11
+ class App < Sinatra::Base
12
+ get '/v1/commands/:cmd' do |cmd|
13
+ args = params.select { |k, _| k.start_with?('arg') }.map { |_, v| v }
14
+
15
+ if settings.run_get_blacklist.include?(cmd)
16
+ halt 403, { MessageCode: 15_360,
17
+ MessageText: "#{cmd} not allowed in web api",
18
+ MessageSeverity: :ERROR }.to_json
19
+ end
20
+
21
+ results = nil
22
+ messages = nil
23
+
24
+ open_p4 do |p4|
25
+ results = p4.run(cmd, *args)
26
+ messages = p4.messages
27
+ end
28
+
29
+ if messages && messages.length > 0
30
+ messages.map { |m| to_msg(m) }.to_json
31
+ elsif results
32
+ results.to_json
33
+ end
34
+ end
35
+
36
+ post '/v1/commands/:cmd' do |cmd|
37
+ args = params.select { |key, _| key.start_with?('arg') }.map { |_, x| x }
38
+
39
+ # Uses the same blacklist as GET which generally makes sense, since we'll
40
+ # only really be concerned about client workspace usage on this server.
41
+ if settings.run_get_blacklist.include?(cmd)
42
+ halt 403, { MessageCode: 15_360,
43
+ MessageText: "#{cmd} not allowed in web api",
44
+ MessageSeverity: :ERROR }.to_json
45
+ end
46
+
47
+ messages = nil
48
+
49
+ open_p4 do |p4|
50
+ p4.input = filter_params(params)
51
+ p4.run(cmd, args)
52
+ messages = p4.messages
53
+ end
54
+
55
+ messages.map { |m| to_msg(m) }.to_json if messages
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,166 @@
1
+ require 'base64'
2
+ require 'p4_web_api/change_helper'
3
+ require 'p4_web_api/p4_util'
4
+ require 'sinatra/base'
5
+
6
+ module P4WebAPI
7
+ # Add 'file' methods
8
+ #
9
+ # 'files' are actually a combination of path metadata. There are three
10
+ # major kinds of file resources: depots, dirs, and files. The true file
11
+ # resources can be a combination of details, along with the file content.
12
+ class App < Sinatra::Base
13
+ # Special depots only variant to match no path
14
+ get '/v1/files' do
15
+ results = nil
16
+
17
+ open_p4 do |p4|
18
+ results = p4.run_depots
19
+ end
20
+
21
+ normalize_depots(results) if settings.normalize_output
22
+
23
+ results.to_json
24
+ end
25
+
26
+ # General browsing variation.
27
+ #
28
+ # Since we want this to be able to fetch file content in the case you
29
+ # specify a file, versus a directory listing, we actually execute the
30
+ # 'p4 files' command on the file. If we get a single result back, we then
31
+ # add the base64'd content. If the file does not exist, we treat it like
32
+ # a directory request. Thus, you'll never really get a 404, just an empty
33
+ # array.
34
+ get '/v1/files/*' do
35
+ dirs = params[:splat].select { |x| !x.empty? }
36
+
37
+ results = nil
38
+
39
+ open_p4 do |p4|
40
+ if dirs.empty?
41
+ results = p4.run_depots
42
+ normalize_depots(results) if settings.normalize_output
43
+ else
44
+ file_selector = '//' + dirs.join('/')
45
+ files_results = nil
46
+ p4.at_exception_level(P4::RAISE_NONE) do
47
+ files_results = p4.run_files(file_selector)
48
+ end
49
+ files_results = [] unless files_results
50
+
51
+ if files_results.length == 1 &&
52
+ files_results.first.key?('depotFile') &&
53
+ files_results.first['depotFile'] == file_selector
54
+ # Treat request like a single file GET
55
+ normalize_files(files_results) if settings.normalize_output
56
+ results = files_results[0]
57
+
58
+ print_results = p4.run_print(file_selector)
59
+
60
+ results['Content'] = Base64.encode64(print_results[1])
61
+
62
+ else
63
+
64
+ # Treat request like a directory list
65
+ selector = '//' + dirs.join('/') + '/*'
66
+
67
+ files_results = p4.run_files('-e', selector)
68
+ normalize_files(files_results) if settings.normalize_output
69
+
70
+ dirs_results = p4.run_dirs(selector)
71
+ normalize_dirs(dirs_results) if settings.normalize_output
72
+
73
+ results = files_results + dirs_results
74
+ end
75
+ end
76
+ end
77
+
78
+ results.to_json
79
+ end
80
+
81
+ # File upload mechanism
82
+ #
83
+ # This allows for multi-file patching based on particular directory level.
84
+ # Because sinatra doesn't really support JSON array bodies, this must be
85
+ # specified via a 'Files' parameter. If that parameter exists, we consider
86
+ # this to be a directory upload.
87
+ #
88
+ # If this is a directory upload, we expect the following parameters on each
89
+ # array object:
90
+ #
91
+ # - 'Content' - The base64 content
92
+ # - 'DepotFile' - the *relative* path from the main splat
93
+ #
94
+ # Otherwise, we mostly just care about the 'Content'
95
+ # fields for single file uploads.
96
+ #
97
+ # In both cases, a 'Description' field can be used to indicate a release
98
+ # message.
99
+ patch '/v1/files/*' do
100
+ path_parts = params[:splat].select { |x| !x.empty? }
101
+ description = params['Description'] || 'Uploaded files'
102
+ is_dir = params.key?('Files')
103
+
104
+ files = nil
105
+ if is_dir
106
+ # TODO: 'clean' the directory path, avoiding refs like '...'
107
+ dir_root = "//#{path_parts.join('/')}"
108
+
109
+ files = params['Files'].map do |f|
110
+ {
111
+ 'DepotFile' => "#{dir_root}/#{f['DepotFile']}",
112
+ 'Content' => f['Content']
113
+ }
114
+ end
115
+ else
116
+ # TODO: 'sanitize' this file path
117
+ files = [
118
+ {
119
+ 'DepotFile' => "//#{path_parts.join('/')}",
120
+ 'Content' => params[:Content]
121
+ }
122
+ ]
123
+ end
124
+ files.each { |f| f['Action'] = 'upload' }
125
+
126
+ depot_paths = files.map { |f| f['DepotFile'] }
127
+
128
+ open_p4_temp_client(depot_paths) do |p4, client_root, client_name|
129
+ helper = ChangeHelper.new(p4: p4,
130
+ client_root: client_root,
131
+ client_name: client_name,
132
+ description: description,
133
+ files: files)
134
+ helper.call
135
+ end
136
+
137
+ ''
138
+ end
139
+
140
+ # Delete a single file.
141
+ delete '/v1/files/*' do
142
+ description = params['Description'] || 'Deleting file'
143
+ path_parts = params[:splat].select { |x| !x.empty? }
144
+
145
+ # TODO: 'sanitize' this file path?
146
+ file_path = "//#{path_parts.join('/')}"
147
+
148
+ open_p4_temp_client([file_path]) do |p4|
149
+ change_id = P4Util.init_changelist(p4, description)
150
+ begin
151
+ p4.run_delete('-c', change_id, file_path)
152
+ p4.run_submit('-c', change_id)
153
+ rescue StandardError => ex
154
+ p4.at_exeception_level(P4::RAISE_NONE) do
155
+ p4.run_change('-d', '-f', change_id)
156
+ if P4Util.error?(p4)
157
+ puts "possible issues deleting change #{change_id}: " \
158
+ "#{p4.messages}"
159
+ end
160
+ end
161
+ raise ex
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,33 @@
1
+ module P4WebAPI
2
+ # Methods to manipulate protections table.
3
+ #
4
+ # The 'protections' resource in our system is the complete list, so you don't
5
+ # fetch or manipulate any single 'protection'. It's all or nothing.
6
+ class App < Sinatra::Base
7
+ # Just list all protections
8
+ get '/v1/protections' do
9
+ protects = nil
10
+ open_p4 do |p4|
11
+ protects = p4.run_protect('-o').first
12
+ end
13
+
14
+ normalize_protections(results) if settings.normalize_output
15
+
16
+ protects.to_json
17
+ end
18
+
19
+ # Update protections
20
+ put '/v1/protections' do
21
+ protects = {
22
+ 'Protections' => params['Protections']
23
+ }
24
+
25
+ open_p4 do |p4|
26
+ p4.input = protects
27
+ p4.run_protect('-i')
28
+ end
29
+
30
+ ''
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,45 @@
1
+ require 'p4_web_api/auth'
2
+
3
+ module P4WebAPI
4
+ # Authentication methods
5
+ # See also: https://confluence.perforce.com:8443/display/WS/Authentication+in+Web+Services
6
+ class App < Sinatra::Base
7
+ # Creates a sign-in session for the user.
8
+ #
9
+ # This session returns a token that should be used as the password in basic
10
+ # authentication for the user later.
11
+ post '/v1/sessions' do
12
+ user = params[:user]
13
+ password = params[:password] # may be a p4 ticket
14
+
15
+ token = nil
16
+
17
+ options = {
18
+ user: user,
19
+ password: password,
20
+ host: settings.p4['host'],
21
+ port: settings.p4['port']
22
+ }
23
+ options[:charset] = settings.p4['charset'] if settings.p4.key?('charset')
24
+
25
+ P4Util.open(options) do |p4|
26
+ token = Auth.create_session(p4, password, settings)
27
+ end
28
+
29
+ # We signal a 401 when login/passwords are generally invalid. Since this
30
+ # is an unauthenticated request, you shouldn't be able to tell if the
31
+ # login doesn't exist or the password is incorrect.
32
+ halt 401 unless token
33
+
34
+ content_type 'text/plain'
35
+ return token
36
+ end
37
+
38
+ # I'm not sure if we should ensure the token being deleted is the token
39
+ # being authenticated against. That's not being checked for the time being.
40
+ delete '/v1/sessions/:token' do |token|
41
+ P4WebAPI::Auth.delete_session(token, settings)
42
+ ''
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,114 @@
1
+ module P4WebAPI
2
+ # Generic CRUD behavior for most of our spec types.
3
+ class App < Sinatra::Base
4
+ set(:is_spec) do |_x|
5
+ condition do
6
+ path_info = env['PATH_INFO']
7
+
8
+ matches = %r{^/v1/(?<spec_type>\w+)}.match(path_info)
9
+ if matches
10
+ spec_type = matches[:spec_type]
11
+ return (spec_type == 'branches' ||
12
+ spec_type == 'clients' ||
13
+ spec_type == 'depots' ||
14
+ spec_type == 'groups' ||
15
+ spec_type == 'jobs' ||
16
+ spec_type == 'labels' ||
17
+ spec_type == 'servers'
18
+ )
19
+ end
20
+ false
21
+ end
22
+ end
23
+
24
+ # Provide a generic collection accessor for each of the specs.
25
+ get '/v1/:spec_type', is_spec: true do |spec_type|
26
+ results = nil
27
+
28
+ open_p4 do |p4|
29
+ results = p4.run(spec_type)
30
+ end
31
+
32
+ send("normalize_#{spec_type}", results) if settings.normalize_output
33
+ P4Util.collate_group_results(results) if settings.normalize_output &&
34
+ spec_type == 'groups'
35
+
36
+ results.to_json
37
+ end
38
+
39
+ # Provide a generic output accessor for each spec
40
+ get '/v1/:spec_type/:id', is_spec: true do |spec_type, id|
41
+ results = nil
42
+
43
+ open_p4 do |p4|
44
+ results = p4.run(P4Util.singular(spec_type), '-o', id)
45
+ end
46
+
47
+ send("normalize_#{spec_type}", results) if settings.normalize_output
48
+
49
+ results[0].to_json
50
+ end
51
+
52
+ # This is our generic "add" mechanism for each type.
53
+ #
54
+ # It's assumed that the client understands the requirements of each spec
55
+ # type.
56
+ post '/v1/:spec_type', is_spec: true do |spec_type|
57
+ results = nil
58
+
59
+ open_p4 do |p4|
60
+ method_name = "save_#{P4Util.singular(spec_type)}".to_sym
61
+ results = p4.send(method_name, params)
62
+ end
63
+
64
+ # In general, the params use the name of the spec as a capitalized
65
+ # parameter in the singular form.
66
+ if spec_type == 'servers'
67
+ id_prop = 'ServerID'
68
+ else
69
+ id_prop = P4Util.singular(spec_type).capitalize
70
+ end
71
+ id = nil
72
+ if results.is_a?(Array) &&
73
+ results.length > 0 &&
74
+ /Job .* saved/.match(results[0])
75
+ # special "Job" variant to grab the ID out of the results output
76
+ id = /Job (.*) saved/.match(results[0])[1]
77
+ elsif params.key?(id_prop)
78
+ id = params[id_prop]
79
+ elsif params.key?(id_prop.to_sym)
80
+ id = params[id_prop.to_sym]
81
+ end
82
+
83
+ if id
84
+ redirect "/v1/#{spec_type}/#{id}"
85
+ else
86
+ halt 400, "Did not locate #{id_prop} in params"
87
+ end
88
+ end
89
+
90
+ # An 'update' mechanism for each spec type.
91
+ patch '/v1/:spec_type/:id', is_spec: true do |spec_type, id|
92
+ open_p4 do |p4|
93
+ singular = P4Util.singular(spec_type)
94
+ spec = p4.run(singular, '-o', id)[0]
95
+
96
+ spec = spec.merge(filter_params(params))
97
+
98
+ method_name = "save_#{singular}".to_sym
99
+ p4.send(method_name, spec)
100
+ end
101
+
102
+ ''
103
+ end
104
+
105
+ delete '/v1/:spec_type/:id', is_spec: true do |spec_type, id|
106
+ open_p4 do |p4|
107
+ method_name = "delete_#{P4Util.singular(spec_type)}".to_sym
108
+ p4.send(method_name, id)
109
+ end
110
+
111
+ ''
112
+ end
113
+ end
114
+ end