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.
@@ -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