4me-sdk 1.2.0 → 2.0.0.pre.rc.1
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 +4 -4
- data/4me-sdk.gemspec +12 -13
- data/Gemfile.lock +51 -30
- data/LICENSE +1 -1
- data/README.md +31 -23
- data/lib/sdk4me.rb +6 -5
- data/lib/sdk4me/ca-bundle.crt +1327 -1802
- data/lib/sdk4me/client.rb +96 -84
- data/lib/sdk4me/client/attachments.rb +103 -116
- data/lib/sdk4me/client/multipart.rb +16 -18
- data/lib/sdk4me/client/response.rb +17 -19
- data/lib/sdk4me/client/version.rb +1 -1
- data/spec/lib/sdk4me/attachments_spec.rb +282 -203
- data/spec/lib/sdk4me/certificate_spec.rb +3 -4
- data/spec/lib/sdk4me/client_spec.rb +165 -157
- data/spec/lib/sdk4me/response_spec.rb +25 -29
- data/spec/lib/sdk4me_spec.rb +7 -7
- data/spec/spec_helper.rb +5 -8
- data/spec/support/matchers/never_raise.rb +16 -21
- data/spec/support/util.rb +2 -2
- metadata +35 -20
data/lib/sdk4me/client.rb
CHANGED
@@ -13,15 +13,18 @@ require 'sdk4me/client/multipart'
|
|
13
13
|
require 'sdk4me/client/attachments'
|
14
14
|
|
15
15
|
# cherry-pick some core extensions from active support
|
16
|
-
require 'active_support/core_ext/module/aliasing
|
16
|
+
require 'active_support/core_ext/module/aliasing'
|
17
17
|
require 'active_support/core_ext/object/blank'
|
18
|
-
require 'active_support/core_ext/object/try
|
18
|
+
require 'active_support/core_ext/object/try'
|
19
19
|
require 'active_support/core_ext/hash/indifferent_access'
|
20
20
|
|
21
21
|
module Sdk4me
|
22
22
|
class Client
|
23
23
|
MAX_PAGE_SIZE = 100
|
24
|
-
DEFAULT_HEADER = {
|
24
|
+
DEFAULT_HEADER = {
|
25
|
+
'Content-Type' => 'application/json',
|
26
|
+
'User-Agent' => "4me-sdk-ruby/#{Sdk4me::Client::VERSION}"
|
27
|
+
}.freeze
|
25
28
|
|
26
29
|
# Create a new 4me SDK Client
|
27
30
|
#
|
@@ -37,13 +40,14 @@ module Sdk4me
|
|
37
40
|
#
|
38
41
|
# All options available:
|
39
42
|
# - logger: The Ruby Logger instance, default: Logger.new(STDOUT)
|
40
|
-
# - host: The 4me API host, default: 'https://api.4me.com'
|
41
|
-
# - api_version: The 4me API version, default: 'v1'
|
43
|
+
# - host: The 4me REST API host, default: 'https://api.4me.com'
|
44
|
+
# - api_version: The 4me REST API version, default: 'v1'
|
42
45
|
# - access_token: *required* The 4me access token
|
43
46
|
# - account: Specify a different (trusted) account to work with
|
44
47
|
# @see https://developer.4me.com/v1/#multiple-accounts
|
45
48
|
# - source: The Source used when creating new records
|
46
49
|
# @see https://developer.4me.com/v1/general/source/
|
50
|
+
# - user_agent: The User-Agent header of each request
|
47
51
|
#
|
48
52
|
# - max_retry_time: maximum nr of seconds to wait for server to respond (default = 5400 = 1.5 hours)
|
49
53
|
# the sleep time between retries starts at 2 seconds and doubles after each retry
|
@@ -59,16 +63,16 @@ module Sdk4me
|
|
59
63
|
# - proxy_password: Proxy password
|
60
64
|
def initialize(options = {})
|
61
65
|
@options = Sdk4me.configuration.current.merge(options)
|
62
|
-
[
|
63
|
-
raise ::Sdk4me::Exception
|
66
|
+
%i[host api_version].each do |required_option|
|
67
|
+
raise ::Sdk4me::Exception, "Missing required configuration option #{required_option}" if option(required_option).blank?
|
64
68
|
end
|
65
69
|
@logger = @options[:logger]
|
66
70
|
@ssl, @domain, @port = ssl_domain_port_path(option(:host))
|
67
71
|
unless option(:access_token).present?
|
68
72
|
if option(:api_token).blank?
|
69
|
-
raise ::Sdk4me::Exception
|
73
|
+
raise ::Sdk4me::Exception, 'Missing required configuration option access_token'
|
70
74
|
else
|
71
|
-
@logger.info('Use of api_token is deprecated,
|
75
|
+
@logger.info('DEPRECATED: Use of api_token is deprecated, switch to using access_token instead. -- https://developer.4me.com/v1/#authentication')
|
72
76
|
end
|
73
77
|
end
|
74
78
|
@ssl_verify_none = options[:ssl_verify_none]
|
@@ -84,15 +88,16 @@ module Sdk4me
|
|
84
88
|
# Returns total nr of resources yielded (for logging)
|
85
89
|
def each(path, params = {}, header = {}, &block)
|
86
90
|
# retrieve the resources using the max page size (least nr of API calls)
|
87
|
-
next_path = expand_path(path, {per_page: MAX_PAGE_SIZE, page: 1}.merge(params))
|
91
|
+
next_path = expand_path(path, { per_page: MAX_PAGE_SIZE, page: 1 }.merge(params))
|
88
92
|
size = 0
|
89
93
|
while next_path
|
90
94
|
# retrieve the records (with retry and optionally wait for rate-limit)
|
91
95
|
response = get(next_path, {}, header)
|
92
96
|
# raise exception in case the response is invalid
|
93
|
-
raise ::Sdk4me::Exception
|
97
|
+
raise ::Sdk4me::Exception, response.message unless response.valid?
|
98
|
+
|
94
99
|
# yield the resources
|
95
|
-
response.json.each
|
100
|
+
response.json.each(&block)
|
96
101
|
size += response.json.size
|
97
102
|
# go to the next page
|
98
103
|
next_path = response.pagination_relative_link(:next)
|
@@ -111,14 +116,14 @@ module Sdk4me
|
|
111
116
|
end
|
112
117
|
|
113
118
|
# send HTTPS PATCH request and return instance of Sdk4me::Response
|
114
|
-
def
|
115
|
-
_send(json_request(Net::HTTP::Patch, path, data, header))
|
119
|
+
def patch(path, data = {}, header = {})
|
120
|
+
_send(json_request(Net::HTTP::Patch, path, data, expand_header(header)))
|
116
121
|
end
|
117
|
-
|
122
|
+
alias put patch
|
118
123
|
|
119
124
|
# send HTTPS POST request and return instance of Sdk4me::Response
|
120
125
|
def post(path, data = {}, header = {})
|
121
|
-
_send(json_request(Net::HTTP::Post, path, data, header))
|
126
|
+
_send(json_request(Net::HTTP::Post, path, data, expand_header(header)))
|
122
127
|
end
|
123
128
|
|
124
129
|
# upload a CSV file to import
|
@@ -135,17 +140,19 @@ module Sdk4me
|
|
135
140
|
@logger.info { "Import file '#{csv.path}' successfully uploaded with token '#{response[:token]}'." } if response.valid?
|
136
141
|
|
137
142
|
if block_until_completed
|
138
|
-
raise ::Sdk4me::UploadFailed
|
143
|
+
raise ::Sdk4me::UploadFailed, "Failed to queue #{type} import. #{response.message}" unless response.valid?
|
144
|
+
|
139
145
|
token = response[:token]
|
140
|
-
|
146
|
+
loop do
|
141
147
|
response = get("/import/#{token}")
|
142
148
|
unless response.valid?
|
143
149
|
sleep(5)
|
144
150
|
response = get("/import/#{token}") # single retry to recover from a network error
|
145
|
-
raise ::Sdk4me::Exception
|
151
|
+
raise ::Sdk4me::Exception, "Unable to monitor progress for #{type} import. #{response.message}" unless response.valid?
|
146
152
|
end
|
147
153
|
# wait 30 seconds while the response is OK and import is still busy
|
148
|
-
break unless [
|
154
|
+
break unless %w[queued processing].include?(response[:state])
|
155
|
+
|
149
156
|
@logger.debug { "Import of '#{csv.path}' is #{response[:state]}. Checking again in 30 seconds." }
|
150
157
|
sleep(30)
|
151
158
|
end
|
@@ -161,7 +168,7 @@ module Sdk4me
|
|
161
168
|
# @param locale: Required for translations export
|
162
169
|
# @raise Sdk4me::Exception in case the export progress could not be monitored
|
163
170
|
def export(types, from = nil, block_until_completed = false, locale = nil)
|
164
|
-
data = {type: [types].flatten.join(',')}
|
171
|
+
data = { type: [types].flatten.join(',') }
|
165
172
|
data[:from] = from unless from.blank?
|
166
173
|
data[:locale] = locale unless locale.blank?
|
167
174
|
response = post('/export', data)
|
@@ -174,17 +181,19 @@ module Sdk4me
|
|
174
181
|
end
|
175
182
|
|
176
183
|
if block_until_completed
|
177
|
-
raise ::Sdk4me::UploadFailed
|
184
|
+
raise ::Sdk4me::UploadFailed, "Failed to queue '#{data[:type]}' export. #{response.message}" unless response.valid?
|
185
|
+
|
178
186
|
token = response[:token]
|
179
|
-
|
187
|
+
loop do
|
180
188
|
response = get("/export/#{token}")
|
181
189
|
unless response.valid?
|
182
190
|
sleep(5)
|
183
191
|
response = get("/export/#{token}") # single retry to recover from a network error
|
184
|
-
raise ::Sdk4me::Exception
|
192
|
+
raise ::Sdk4me::Exception, "Unable to monitor progress for '#{data[:type]}' export. #{response.message}" unless response.valid?
|
185
193
|
end
|
186
194
|
# wait 30 seconds while the response is OK and export is still busy
|
187
|
-
break unless [
|
195
|
+
break unless %w[queued processing].include?(response[:state])
|
196
|
+
|
188
197
|
@logger.debug { "Export of '#{data[:type]}' is #{response[:state]}. Checking again in 30 seconds." }
|
189
198
|
sleep(30)
|
190
199
|
end
|
@@ -193,18 +202,16 @@ module Sdk4me
|
|
193
202
|
response
|
194
203
|
end
|
195
204
|
|
196
|
-
|
197
|
-
@logger
|
198
|
-
end
|
205
|
+
attr_reader :logger
|
199
206
|
|
200
207
|
private
|
201
208
|
|
202
209
|
# create a request (place data in body if the request becomes too large)
|
203
|
-
def json_request(request_class, path, data
|
204
|
-
Sdk4me::Attachments.new(self).upload_attachments!(
|
205
|
-
request = request_class.new(expand_path(path),
|
210
|
+
def json_request(request_class, path, data, header)
|
211
|
+
Sdk4me::Attachments.new(self, path).upload_attachments!(data)
|
212
|
+
request = request_class.new(expand_path(path), header)
|
206
213
|
body = {}
|
207
|
-
data.each{ |k,v| body[k.to_s] = typecast(v, false) }
|
214
|
+
data.each { |k, v| body[k.to_s] = typecast(v, false) }
|
208
215
|
request.body = body.to_json
|
209
216
|
request
|
210
217
|
end
|
@@ -215,19 +222,18 @@ module Sdk4me
|
|
215
222
|
end
|
216
223
|
|
217
224
|
# Expand the given header with the default header
|
218
|
-
def expand_header(
|
219
|
-
header = DEFAULT_HEADER.
|
225
|
+
def expand_header(headers = {})
|
226
|
+
header = DEFAULT_HEADER.dup
|
220
227
|
header['X-4me-Account'] = option(:account) if option(:account)
|
221
228
|
if option(:access_token).present?
|
222
|
-
header['AUTHORIZATION'] =
|
229
|
+
header['AUTHORIZATION'] = "Bearer #{option(:access_token)}"
|
223
230
|
else
|
224
231
|
token_and_password = option(:api_token).include?(':') ? option(:api_token) : "#{option(:api_token)}:x"
|
225
|
-
header['AUTHORIZATION'] =
|
226
|
-
end
|
227
|
-
if option(:source)
|
228
|
-
header['X-4me-Source'] = option(:source)
|
229
|
-
header['HTTP_USER_AGENT'] = option(:source)
|
232
|
+
header['AUTHORIZATION'] = "Basic #{[token_and_password].pack('m*').gsub(/\s/, '')}"
|
230
233
|
end
|
234
|
+
header['X-4me-Source'] = option(:source) if option(:source)
|
235
|
+
header['User-Agent'] = option(:user_agent) if option(:user_agent)
|
236
|
+
header.merge!(headers)
|
231
237
|
header
|
232
238
|
end
|
233
239
|
|
@@ -238,8 +244,8 @@ module Sdk4me
|
|
238
244
|
# fields: ['id', 'created_at', 'sourceID']
|
239
245
|
def expand_path(path, params = {})
|
240
246
|
path = path.dup
|
241
|
-
path = "/#{path}" unless path =~
|
242
|
-
path = "/#{option(:api_version)}#{path}" unless path =~
|
247
|
+
path = "/#{path}" unless path =~ %r{^/} # make sure path starts with /
|
248
|
+
path = "/#{option(:api_version)}#{path}" unless path =~ %r{^/v[\d.]+/} # preprend api version
|
243
249
|
params.each do |key, value|
|
244
250
|
path << (path['?'] ? '&' : '?')
|
245
251
|
path << expand_param(key, value)
|
@@ -258,18 +264,20 @@ module Sdk4me
|
|
258
264
|
# Parameter value typecasting
|
259
265
|
def typecast(value, escape = true)
|
260
266
|
case value.class.name.to_sym
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
267
|
+
when :NilClass then ''
|
268
|
+
when :String then escape ? uri_escape(value) : value
|
269
|
+
when :TrueClass then 'true'
|
270
|
+
when :FalseClass then 'false'
|
271
|
+
when :DateTime
|
272
|
+
datetime = value.new_offset(0).iso8601
|
273
|
+
escape ? uri_escape(datetime) : datetime
|
274
|
+
when :Date then value.strftime('%Y-%m-%d')
|
275
|
+
when :Time then value.strftime('%H:%M')
|
268
276
|
# do not convert arrays in put/post requests as squashing arrays is only used in filtering
|
269
|
-
|
277
|
+
when :Array then escape ? value.map { |v| typecast(v, escape) }.join(',') : value
|
270
278
|
# TODO: temporary for special constructions to update contact details, see Request #1444166
|
271
|
-
|
272
|
-
|
279
|
+
when :Hash then escape ? value.to_s : value
|
280
|
+
else escape ? value.to_json : value.to_s
|
273
281
|
end
|
274
282
|
end
|
275
283
|
|
@@ -277,27 +285,27 @@ module Sdk4me
|
|
277
285
|
# Guaranteed to return a Response, thought it may be +empty?+
|
278
286
|
def _send(request, domain = @domain, port = @port, ssl = @ssl)
|
279
287
|
@logger.debug { "Sending #{request.method} request to #{domain}:#{port}#{request.path}" }
|
280
|
-
|
288
|
+
response = begin
|
281
289
|
http_with_proxy = option(:proxy_host).blank? ? Net::HTTP : Net::HTTP::Proxy(option(:proxy_host), option(:proxy_port), option(:proxy_user), option(:proxy_password))
|
282
290
|
http = http_with_proxy.new(domain, port)
|
283
291
|
http.read_timeout = option(:read_timeout)
|
284
292
|
http.use_ssl = ssl
|
285
293
|
http.verify_mode = OpenSSL::SSL::VERIFY_NONE if @ssl_verify_none
|
286
|
-
http.start{ |
|
287
|
-
rescue
|
294
|
+
http.start { |transport| transport.request(request) }
|
295
|
+
rescue StandardError => e
|
288
296
|
Struct.new(:body, :message, :code, :header).new(nil, "No Response from Server - #{e.message} for '#{domain}:#{port}#{request.path}'", 500, {})
|
289
297
|
end
|
290
|
-
|
291
|
-
if
|
292
|
-
@logger.debug { "Response:\n#{JSON.pretty_generate(
|
293
|
-
elsif
|
294
|
-
@logger.debug { "XML response:\n#{
|
295
|
-
elsif
|
296
|
-
@logger.debug { "Redirect: #{
|
298
|
+
resp = Sdk4me::Response.new(request, response)
|
299
|
+
if resp.valid?
|
300
|
+
@logger.debug { "Response:\n#{JSON.pretty_generate(resp.json)}" }
|
301
|
+
elsif resp.raw.body =~ /^\s*<\?xml/i
|
302
|
+
@logger.debug { "XML response:\n#{resp.raw.body}" }
|
303
|
+
elsif resp.raw.code.to_s == '303'
|
304
|
+
@logger.debug { "Redirect: #{resp.raw.header['Location']}" }
|
297
305
|
else
|
298
|
-
@logger.error { "#{request.method} request to #{domain}:#{port}#{request.path} failed: #{
|
306
|
+
@logger.error { "#{request.method} request to #{domain}:#{port}#{request.path} failed: #{resp.message}" }
|
299
307
|
end
|
300
|
-
|
308
|
+
resp
|
301
309
|
end
|
302
310
|
|
303
311
|
# parse the given URI to [domain, port, ssl, path]
|
@@ -306,65 +314,70 @@ module Sdk4me
|
|
306
314
|
ssl = uri.scheme == 'https'
|
307
315
|
[ssl, uri.host, uri.port, uri.path]
|
308
316
|
end
|
309
|
-
|
310
317
|
end
|
311
318
|
|
312
319
|
module SendWithRateLimitBlock
|
313
320
|
# Wraps the _send method with retries when the server does not respond, see +initialize+ option +:rate_limit_block+
|
314
321
|
def _send(request, domain = @domain, port = @port, ssl = @ssl)
|
315
|
-
return super(request, domain, port, ssl) unless option(:block_at_rate_limit) && option(:max_throttle_time)
|
322
|
+
return super(request, domain, port, ssl) unless option(:block_at_rate_limit) && option(:max_throttle_time).positive?
|
323
|
+
|
316
324
|
now = nil
|
317
325
|
timed_out = false
|
318
|
-
|
319
|
-
|
326
|
+
response = nil
|
327
|
+
loop do
|
328
|
+
response = super(request, domain, port, ssl)
|
320
329
|
now ||= Time.now
|
321
|
-
if
|
330
|
+
if response.throttled?
|
322
331
|
# if no Retry-After is not provided, the 4me server is very busy, wait 5 minutes
|
323
|
-
retry_after =
|
332
|
+
retry_after = response.retry_after.zero? ? 300 : [response.retry_after, 2].max
|
324
333
|
if (Time.now - now + retry_after) < option(:max_throttle_time)
|
325
|
-
@logger.warn { "Request throttled, trying again in #{retry_after} seconds: #{
|
334
|
+
@logger.warn { "Request throttled, trying again in #{retry_after} seconds: #{response.message}" }
|
326
335
|
sleep(retry_after)
|
327
336
|
else
|
328
337
|
timed_out = true
|
329
338
|
end
|
330
339
|
end
|
331
|
-
|
332
|
-
|
340
|
+
break unless response.throttled? && !timed_out
|
341
|
+
end
|
342
|
+
response
|
333
343
|
end
|
334
344
|
end
|
335
|
-
Client.
|
345
|
+
Client.prepend SendWithRateLimitBlock
|
336
346
|
|
337
347
|
module SendWithRetries
|
338
348
|
# Wraps the _send method with retries when the server does not respond, see +initialize+ option +:retries+
|
339
349
|
def _send(request, domain = @domain, port = @port, ssl = @ssl)
|
340
|
-
return super(request, domain, port, ssl) unless option(:max_retry_time)
|
350
|
+
return super(request, domain, port, ssl) unless option(:max_retry_time).positive?
|
351
|
+
|
341
352
|
retries = 0
|
342
353
|
sleep_time = 1
|
343
354
|
now = nil
|
344
355
|
timed_out = false
|
345
|
-
|
346
|
-
|
356
|
+
response = nil
|
357
|
+
loop do
|
358
|
+
response = super(request, domain, port, ssl)
|
347
359
|
now ||= Time.now
|
348
|
-
if
|
360
|
+
if response.failure?
|
349
361
|
sleep_time *= 2
|
350
362
|
if (Time.now - now + sleep_time) < option(:max_retry_time)
|
351
|
-
@logger.warn { "Request failed, retry ##{retries += 1} in #{sleep_time} seconds: #{
|
363
|
+
@logger.warn { "Request failed, retry ##{retries += 1} in #{sleep_time} seconds: #{response.message}" }
|
352
364
|
sleep(sleep_time)
|
353
365
|
else
|
354
366
|
timed_out = true
|
355
367
|
end
|
356
368
|
end
|
357
|
-
|
358
|
-
|
369
|
+
break unless response.failure? && !timed_out
|
370
|
+
end
|
371
|
+
response
|
359
372
|
end
|
360
373
|
end
|
361
|
-
Client.
|
374
|
+
Client.prepend SendWithRetries
|
362
375
|
end
|
363
376
|
|
364
377
|
# HTTPS with certificate bundle
|
365
378
|
module Net
|
366
379
|
class HTTP
|
367
|
-
|
380
|
+
alias original_use_ssl= use_ssl=
|
368
381
|
|
369
382
|
def use_ssl=(flag)
|
370
383
|
self.ca_file = File.expand_path(Sdk4me.configuration.current[:ca_file], __FILE__) if flag
|
@@ -373,4 +386,3 @@ module Net
|
|
373
386
|
end
|
374
387
|
end
|
375
388
|
end
|
376
|
-
|
@@ -1,152 +1,139 @@
|
|
1
1
|
module Sdk4me
|
2
2
|
class Attachments
|
3
|
+
S3_PROVIDER = 's3'.freeze
|
4
|
+
FILENAME_TEMPLATE = '${filename}'.freeze
|
3
5
|
|
4
|
-
|
5
|
-
FILENAME_TEMPLATE = '${filename}'
|
6
|
-
|
7
|
-
def initialize(client)
|
6
|
+
def initialize(client, path)
|
8
7
|
@client = client
|
8
|
+
@path = path
|
9
9
|
end
|
10
10
|
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
data
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
#
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
11
|
+
# Upload attachments and replace the data inline with the uploaded
|
12
|
+
# attachments info.
|
13
|
+
#
|
14
|
+
# To upload field attachments:
|
15
|
+
# * data[:note_attachments] = ['/tmp/test.doc', '/tmp/test.log']
|
16
|
+
#
|
17
|
+
# To upload inline images:
|
18
|
+
# * data[:note] containing text referring to inline images in
|
19
|
+
# data[:note_attachments] by their array index, with the index being
|
20
|
+
# zero-based. Text can only refer to inline images in its own
|
21
|
+
# attachments collection. For example:
|
22
|
+
#
|
23
|
+
# data = {
|
24
|
+
# note: "Hello [note_attachments: 0] and [note_attachments: 1]",
|
25
|
+
# note_attachments: ['/tmp/jip.png', '/tmp/janneke.png'],
|
26
|
+
# ...
|
27
|
+
# }
|
28
|
+
#
|
29
|
+
# After calling this method the data that will be posted to update the
|
30
|
+
# 4me record would look similar to:
|
31
|
+
#
|
32
|
+
# data = {
|
33
|
+
# note: "Hello  and ",
|
34
|
+
# note_attachments: [
|
35
|
+
# { key: 'storage/abc/fskdhakjfkjdssdf.png', filesize: 12345, inline: true },
|
36
|
+
# { key: 'storage/abc/fskdhakjfkjdssdf.png'], filesize: 98765, inline: true }
|
37
|
+
# ],
|
38
|
+
# ...
|
39
|
+
# }
|
40
|
+
def upload_attachments!(data)
|
41
|
+
# Field attachments
|
42
|
+
field_attachments = []
|
43
|
+
data.each do |field, value|
|
44
|
+
next unless field.to_s.end_with?('_attachments')
|
45
|
+
next unless value.is_a?(Enumerable) && value.any?
|
46
|
+
|
47
|
+
value.map! { |attachment| upload_attachment(attachment) }.compact!
|
48
|
+
field_attachments << field if value.any?
|
38
49
|
end
|
39
|
-
end
|
40
50
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
else
|
57
|
-
full_match
|
51
|
+
# Rich text inline attachments
|
52
|
+
field_attachments.each do |field_attachment|
|
53
|
+
field = field_attachment.to_s.sub(/_attachments$/, '')
|
54
|
+
value = data[field.to_sym] || data[field]
|
55
|
+
next unless value.is_a?(String)
|
56
|
+
|
57
|
+
value.gsub!(/\[#{field_attachment}:\s?(\d+)\]/) do |match|
|
58
|
+
idx = Regexp.last_match(1).to_i
|
59
|
+
attachment = data[field_attachment][idx]
|
60
|
+
if attachment
|
61
|
+
attachment[:inline] = true
|
62
|
+
"" # magic markdown for inline attachments
|
63
|
+
else
|
64
|
+
match
|
65
|
+
end
|
58
66
|
end
|
59
67
|
end
|
60
|
-
attachments
|
61
68
|
end
|
62
69
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
70
|
+
private
|
71
|
+
|
72
|
+
def raise_error(message)
|
73
|
+
@client.logger.error { message }
|
74
|
+
raise Sdk4me::UploadFailed, message
|
68
75
|
end
|
69
76
|
|
70
|
-
def
|
71
|
-
|
72
|
-
when /cis/, /contracts/, /flsas/, /service_instances/, /slas/
|
73
|
-
:remarks_attachments
|
74
|
-
when /service_offerings/
|
75
|
-
:summary_attachments
|
76
|
-
else
|
77
|
-
:note_attachments
|
78
|
-
end
|
77
|
+
def storage
|
78
|
+
@storage ||= @client.get('/attachments/storage').json.with_indifferent_access
|
79
79
|
end
|
80
80
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
81
|
+
# Upload a single attachment and return the data that should be submitted
|
82
|
+
# back to 4me. Returns nil and provides an error in case the attachment
|
83
|
+
# upload failed.
|
84
|
+
def upload_attachment(attachment)
|
85
|
+
return nil unless attachment
|
86
|
+
|
87
|
+
provider = storage[:provider]
|
88
|
+
raise 'No provider found' unless provider
|
89
|
+
|
90
|
+
# attachment is already a file or we need to open the file from disk
|
91
|
+
unless attachment.respond_to?(:path) && attachment.respond_to?(:read)
|
92
|
+
raise "file does not exist: #{attachment}" unless File.exist?(attachment)
|
93
|
+
|
94
|
+
attachment = File.open(attachment, 'rb')
|
86
95
|
end
|
87
|
-
end
|
88
96
|
|
89
|
-
|
90
|
-
|
91
|
-
def upload_attachment(storage, attachment, raise_exceptions)
|
92
|
-
begin
|
93
|
-
# attachment is already a file or we need to open the file from disk
|
94
|
-
unless attachment.respond_to?(:path) && attachment.respond_to?(:read)
|
95
|
-
raise "file does not exist: #{attachment}" unless File.exists?(attachment)
|
96
|
-
attachment = File.open(attachment, 'rb')
|
97
|
-
end
|
97
|
+
key_template = storage[provider][:key]
|
98
|
+
key = key_template.sub(FILENAME_TEMPLATE, File.basename(attachment.path))
|
98
99
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
send(upload_method, storage, key_template, key, attachment)
|
104
|
-
|
105
|
-
# return the values for the note_attachments param
|
106
|
-
{key: key, filesize: File.size(attachment.path)}
|
107
|
-
rescue ::Exception => e
|
108
|
-
report_error("Attachment upload failed: #{e.message}", raise_exceptions)
|
109
|
-
nil
|
100
|
+
if provider == S3_PROVIDER
|
101
|
+
upload_to_s3(key, attachment)
|
102
|
+
else
|
103
|
+
upload_to_4me_local(key, attachment)
|
110
104
|
end
|
105
|
+
|
106
|
+
# return the values for the attachments param
|
107
|
+
{ key: key, filesize: File.size(attachment.path) }
|
108
|
+
rescue StandardError => e
|
109
|
+
raise_error("Attachment upload failed: #{e.message}")
|
111
110
|
end
|
112
111
|
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
key: key_template,
|
118
|
-
AWSAccessKeyId: aws[:access_key],
|
119
|
-
acl: 'private',
|
120
|
-
signature: aws[:signature],
|
121
|
-
success_action_status: 201,
|
122
|
-
policy: aws[:policy],
|
123
|
-
file: attachment # file must be last
|
124
|
-
})
|
125
|
-
# this is a bit of a hack, but Amazon S3 returns only XML :(
|
126
|
-
xml = response.raw.body || ''
|
127
|
-
error = xml[/<Error>.*<Message>(.*)<\/Message>.*<\/Error>/, 1]
|
128
|
-
raise "AWS upload to #{aws[:upload_uri]} for #{key} failed: #{error}" if error
|
112
|
+
# Upload the file to AWS S3 storage
|
113
|
+
def upload_to_s3(key, attachment)
|
114
|
+
uri = storage[:upload_uri]
|
115
|
+
response = send_file(uri, storage[:s3].merge({ file: attachment }))
|
129
116
|
|
130
|
-
#
|
131
|
-
|
132
|
-
|
117
|
+
# this is a bit of a hack, but Amazon S3 returns only XML :(
|
118
|
+
xml = response.body || ''
|
119
|
+
error = xml[%r{<Error>.*<Message>(.*)</Message>.*</Error>}, 1]
|
120
|
+
raise "AWS S3 upload to #{uri} for #{key} failed: #{error}" if error
|
133
121
|
end
|
134
122
|
|
135
|
-
#
|
136
|
-
def
|
137
|
-
uri = storage[:upload_uri]
|
138
|
-
response = send_file(uri, {file: attachment
|
139
|
-
raise "4me upload to #{
|
123
|
+
# Upload the file directly to 4me local storage
|
124
|
+
def upload_to_4me_local(key, attachment)
|
125
|
+
uri = storage[:upload_uri]
|
126
|
+
response = send_file(uri, storage[:local].merge({ file: attachment }), @client.send(:expand_header))
|
127
|
+
raise "4me upload to #{uri} for #{key} failed: #{response.message}" unless response.valid?
|
140
128
|
end
|
141
129
|
|
142
130
|
def send_file(uri, params, basic_auth_header = {})
|
143
|
-
params = {
|
131
|
+
params = { 'Content-Type': MIME::Types.type_for(params[:key])[0] || MIME::Types['application/octet-stream'][0] }.merge(params)
|
144
132
|
data, header = Sdk4me::Multipart::Post.prepare_query(params)
|
145
133
|
ssl, domain, port, path = @client.send(:ssl_domain_port_path, uri)
|
146
134
|
request = Net::HTTP::Post.new(path, basic_auth_header.merge(header))
|
147
135
|
request.body = data
|
148
136
|
@client.send(:_send, request, domain, port, ssl)
|
149
137
|
end
|
150
|
-
|
151
138
|
end
|
152
139
|
end
|