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.
- checksums.yaml +4 -4
- data/bin/p4_web_api +8 -2
- data/lib/p4_web_api.rb +27 -670
- data/lib/p4_web_api/app/changes.rb +73 -0
- data/lib/p4_web_api/app/commands.rb +58 -0
- data/lib/p4_web_api/app/files.rb +166 -0
- data/lib/p4_web_api/app/protections.rb +33 -0
- data/lib/p4_web_api/app/sessions.rb +45 -0
- data/lib/p4_web_api/app/specs.rb +114 -0
- data/lib/p4_web_api/app/streams.rb +76 -0
- data/lib/p4_web_api/app/triggers.rb +31 -0
- data/lib/p4_web_api/app/users.rb +78 -0
- data/lib/p4_web_api/auth.rb +3 -3
- data/lib/p4_web_api/change_helper.rb +149 -0
- data/lib/p4_web_api/helpers.rb +84 -0
- data/lib/p4_web_api/p4_util.rb +90 -1
- data/lib/p4_web_api/version.rb +1 -1
- metadata +13 -2
@@ -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
|