4me-sdk 1.1.7 → 2.0.0.pre.rc.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/4me-sdk.gemspec +12 -13
- data/Gemfile.lock +58 -39
- data/LICENSE +1 -1
- data/README.md +53 -36
- data/lib/sdk4me.rb +7 -5
- data/lib/sdk4me/ca-bundle.crt +1327 -1802
- data/lib/sdk4me/client.rb +110 -86
- data/lib/sdk4me/client/attachments.rb +109 -75
- 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 +307 -143
- data/spec/lib/sdk4me/certificate_spec.rb +17 -4
- data/spec/lib/sdk4me/client_spec.rb +490 -475
- data/spec/lib/sdk4me/response_spec.rb +249 -232
- 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 +38 -24
data/lib/sdk4me/client.rb
CHANGED
@@ -13,21 +13,24 @@ 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
|
#
|
28
31
|
# Shared configuration for all 4me SDK Clients:
|
29
32
|
# Sdk4me.configure do |config|
|
30
|
-
# config.
|
33
|
+
# config.access_token = 'd41f5868feb65fc87fa2311a473a8766ea38bc40'
|
31
34
|
# config.account = 'my-sandbox'
|
32
35
|
# ...
|
33
36
|
# end
|
@@ -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'
|
42
|
-
# -
|
43
|
+
# - host: The 4me REST API host, default: 'https://api.4me.com'
|
44
|
+
# - api_version: The 4me REST API version, default: 'v1'
|
45
|
+
# - access_token: *required* The 4me access token
|
43
46
|
# - account: Specify a different (trusted) account to work with
|
44
|
-
# @see
|
47
|
+
# @see https://developer.4me.com/v1/#multiple-accounts
|
45
48
|
# - source: The Source used when creating new records
|
46
|
-
# @see
|
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
|
@@ -51,7 +55,7 @@ module Sdk4me
|
|
51
55
|
# one retry will always be performed unless you set the value to -1
|
52
56
|
# - read_timeout: HTTP GET read timeout in seconds (default = 25)
|
53
57
|
# - block_at_rate_limit: Set to +true+ to block the request until the rate limit is lifted, default: +false+
|
54
|
-
# @see
|
58
|
+
# @see https://developer.4me.com/v1/#rate-limiting
|
55
59
|
#
|
56
60
|
# - proxy_host: Define in case HTTP traffic needs to go through a proxy
|
57
61
|
# - proxy_port: Port of the proxy, defaults to 8080
|
@@ -59,12 +63,19 @@ 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
|
69
|
+
@logger = @options[:logger]
|
65
70
|
@ssl, @domain, @port = ssl_domain_port_path(option(:host))
|
71
|
+
unless option(:access_token).present?
|
72
|
+
if option(:api_token).blank?
|
73
|
+
raise ::Sdk4me::Exception, 'Missing required configuration option access_token'
|
74
|
+
else
|
75
|
+
@logger.info('DEPRECATED: Use of api_token is deprecated, switch to using access_token instead. -- https://developer.4me.com/v1/#authentication')
|
76
|
+
end
|
77
|
+
end
|
66
78
|
@ssl_verify_none = options[:ssl_verify_none]
|
67
|
-
@logger = @options[:logger]
|
68
79
|
end
|
69
80
|
|
70
81
|
# Retrieve an option
|
@@ -77,15 +88,16 @@ module Sdk4me
|
|
77
88
|
# Returns total nr of resources yielded (for logging)
|
78
89
|
def each(path, params = {}, header = {}, &block)
|
79
90
|
# retrieve the resources using the max page size (least nr of API calls)
|
80
|
-
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))
|
81
92
|
size = 0
|
82
93
|
while next_path
|
83
94
|
# retrieve the records (with retry and optionally wait for rate-limit)
|
84
95
|
response = get(next_path, {}, header)
|
85
96
|
# raise exception in case the response is invalid
|
86
|
-
raise ::Sdk4me::Exception
|
97
|
+
raise ::Sdk4me::Exception, response.message unless response.valid?
|
98
|
+
|
87
99
|
# yield the resources
|
88
|
-
response.json.each
|
100
|
+
response.json.each(&block)
|
89
101
|
size += response.json.size
|
90
102
|
# go to the next page
|
91
103
|
next_path = response.pagination_relative_link(:next)
|
@@ -104,14 +116,14 @@ module Sdk4me
|
|
104
116
|
end
|
105
117
|
|
106
118
|
# send HTTPS PATCH request and return instance of Sdk4me::Response
|
107
|
-
def
|
108
|
-
_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)))
|
109
121
|
end
|
110
|
-
|
122
|
+
alias put patch
|
111
123
|
|
112
124
|
# send HTTPS POST request and return instance of Sdk4me::Response
|
113
125
|
def post(path, data = {}, header = {})
|
114
|
-
_send(json_request(Net::HTTP::Post, path, data, header))
|
126
|
+
_send(json_request(Net::HTTP::Post, path, data, expand_header(header)))
|
115
127
|
end
|
116
128
|
|
117
129
|
# upload a CSV file to import
|
@@ -128,17 +140,19 @@ module Sdk4me
|
|
128
140
|
@logger.info { "Import file '#{csv.path}' successfully uploaded with token '#{response[:token]}'." } if response.valid?
|
129
141
|
|
130
142
|
if block_until_completed
|
131
|
-
raise ::Sdk4me::UploadFailed
|
143
|
+
raise ::Sdk4me::UploadFailed, "Failed to queue #{type} import. #{response.message}" unless response.valid?
|
144
|
+
|
132
145
|
token = response[:token]
|
133
|
-
|
146
|
+
loop do
|
134
147
|
response = get("/import/#{token}")
|
135
148
|
unless response.valid?
|
136
149
|
sleep(5)
|
137
150
|
response = get("/import/#{token}") # single retry to recover from a network error
|
138
|
-
raise ::Sdk4me::Exception
|
151
|
+
raise ::Sdk4me::Exception, "Unable to monitor progress for #{type} import. #{response.message}" unless response.valid?
|
139
152
|
end
|
140
153
|
# wait 30 seconds while the response is OK and import is still busy
|
141
|
-
break unless [
|
154
|
+
break unless %w[queued processing].include?(response[:state])
|
155
|
+
|
142
156
|
@logger.debug { "Import of '#{csv.path}' is #{response[:state]}. Checking again in 30 seconds." }
|
143
157
|
sleep(30)
|
144
158
|
end
|
@@ -154,7 +168,7 @@ module Sdk4me
|
|
154
168
|
# @param locale: Required for translations export
|
155
169
|
# @raise Sdk4me::Exception in case the export progress could not be monitored
|
156
170
|
def export(types, from = nil, block_until_completed = false, locale = nil)
|
157
|
-
data = {type: [types].flatten.join(',')}
|
171
|
+
data = { type: [types].flatten.join(',') }
|
158
172
|
data[:from] = from unless from.blank?
|
159
173
|
data[:locale] = locale unless locale.blank?
|
160
174
|
response = post('/export', data)
|
@@ -167,17 +181,19 @@ module Sdk4me
|
|
167
181
|
end
|
168
182
|
|
169
183
|
if block_until_completed
|
170
|
-
raise ::Sdk4me::UploadFailed
|
184
|
+
raise ::Sdk4me::UploadFailed, "Failed to queue '#{data[:type]}' export. #{response.message}" unless response.valid?
|
185
|
+
|
171
186
|
token = response[:token]
|
172
|
-
|
187
|
+
loop do
|
173
188
|
response = get("/export/#{token}")
|
174
189
|
unless response.valid?
|
175
190
|
sleep(5)
|
176
191
|
response = get("/export/#{token}") # single retry to recover from a network error
|
177
|
-
raise ::Sdk4me::Exception
|
192
|
+
raise ::Sdk4me::Exception, "Unable to monitor progress for '#{data[:type]}' export. #{response.message}" unless response.valid?
|
178
193
|
end
|
179
194
|
# wait 30 seconds while the response is OK and export is still busy
|
180
|
-
break unless [
|
195
|
+
break unless %w[queued processing].include?(response[:state])
|
196
|
+
|
181
197
|
@logger.debug { "Export of '#{data[:type]}' is #{response[:state]}. Checking again in 30 seconds." }
|
182
198
|
sleep(30)
|
183
199
|
end
|
@@ -186,18 +202,16 @@ module Sdk4me
|
|
186
202
|
response
|
187
203
|
end
|
188
204
|
|
189
|
-
|
190
|
-
@logger
|
191
|
-
end
|
205
|
+
attr_reader :logger
|
192
206
|
|
193
207
|
private
|
194
208
|
|
195
209
|
# create a request (place data in body if the request becomes too large)
|
196
|
-
def json_request(request_class, path, data
|
197
|
-
Sdk4me::Attachments.new(self).upload_attachments!(
|
198
|
-
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)
|
199
213
|
body = {}
|
200
|
-
data.each{ |k,v| body[k.to_s] = typecast(v, false) }
|
214
|
+
data.each { |k, v| body[k.to_s] = typecast(v, false) }
|
201
215
|
request.body = body.to_json
|
202
216
|
request
|
203
217
|
end
|
@@ -208,14 +222,18 @@ module Sdk4me
|
|
208
222
|
end
|
209
223
|
|
210
224
|
# Expand the given header with the default header
|
211
|
-
def expand_header(
|
212
|
-
header = DEFAULT_HEADER.
|
225
|
+
def expand_header(headers = {})
|
226
|
+
header = DEFAULT_HEADER.dup
|
213
227
|
header['X-4me-Account'] = option(:account) if option(:account)
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
228
|
+
if option(:access_token).present?
|
229
|
+
header['AUTHORIZATION'] = "Bearer #{option(:access_token)}"
|
230
|
+
else
|
231
|
+
token_and_password = option(:api_token).include?(':') ? option(:api_token) : "#{option(:api_token)}:x"
|
232
|
+
header['AUTHORIZATION'] = "Basic #{[token_and_password].pack('m*').gsub(/\s/, '')}"
|
218
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)
|
219
237
|
header
|
220
238
|
end
|
221
239
|
|
@@ -226,8 +244,8 @@ module Sdk4me
|
|
226
244
|
# fields: ['id', 'created_at', 'sourceID']
|
227
245
|
def expand_path(path, params = {})
|
228
246
|
path = path.dup
|
229
|
-
path = "/#{path}" unless path =~
|
230
|
-
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
|
231
249
|
params.each do |key, value|
|
232
250
|
path << (path['?'] ? '&' : '?')
|
233
251
|
path << expand_param(key, value)
|
@@ -246,18 +264,20 @@ module Sdk4me
|
|
246
264
|
# Parameter value typecasting
|
247
265
|
def typecast(value, escape = true)
|
248
266
|
case value.class.name.to_sym
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
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')
|
256
276
|
# do not convert arrays in put/post requests as squashing arrays is only used in filtering
|
257
|
-
|
277
|
+
when :Array then escape ? value.map { |v| typecast(v, escape) }.join(',') : value
|
258
278
|
# TODO: temporary for special constructions to update contact details, see Request #1444166
|
259
|
-
|
260
|
-
|
279
|
+
when :Hash then escape ? value.to_s : value
|
280
|
+
else escape ? value.to_json : value.to_s
|
261
281
|
end
|
262
282
|
end
|
263
283
|
|
@@ -265,27 +285,27 @@ module Sdk4me
|
|
265
285
|
# Guaranteed to return a Response, thought it may be +empty?+
|
266
286
|
def _send(request, domain = @domain, port = @port, ssl = @ssl)
|
267
287
|
@logger.debug { "Sending #{request.method} request to #{domain}:#{port}#{request.path}" }
|
268
|
-
|
288
|
+
response = begin
|
269
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))
|
270
290
|
http = http_with_proxy.new(domain, port)
|
271
291
|
http.read_timeout = option(:read_timeout)
|
272
292
|
http.use_ssl = ssl
|
273
293
|
http.verify_mode = OpenSSL::SSL::VERIFY_NONE if @ssl_verify_none
|
274
|
-
http.start{ |
|
275
|
-
rescue
|
294
|
+
http.start { |transport| transport.request(request) }
|
295
|
+
rescue StandardError => e
|
276
296
|
Struct.new(:body, :message, :code, :header).new(nil, "No Response from Server - #{e.message} for '#{domain}:#{port}#{request.path}'", 500, {})
|
277
297
|
end
|
278
|
-
|
279
|
-
if
|
280
|
-
@logger.debug { "Response:\n#{JSON.pretty_generate(
|
281
|
-
elsif
|
282
|
-
@logger.debug { "XML response:\n#{
|
283
|
-
elsif
|
284
|
-
@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']}" }
|
285
305
|
else
|
286
|
-
@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}" }
|
287
307
|
end
|
288
|
-
|
308
|
+
resp
|
289
309
|
end
|
290
310
|
|
291
311
|
# parse the given URI to [domain, port, ssl, path]
|
@@ -294,65 +314,70 @@ module Sdk4me
|
|
294
314
|
ssl = uri.scheme == 'https'
|
295
315
|
[ssl, uri.host, uri.port, uri.path]
|
296
316
|
end
|
297
|
-
|
298
317
|
end
|
299
318
|
|
300
319
|
module SendWithRateLimitBlock
|
301
320
|
# Wraps the _send method with retries when the server does not respond, see +initialize+ option +:rate_limit_block+
|
302
321
|
def _send(request, domain = @domain, port = @port, ssl = @ssl)
|
303
|
-
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
|
+
|
304
324
|
now = nil
|
305
325
|
timed_out = false
|
306
|
-
|
307
|
-
|
326
|
+
response = nil
|
327
|
+
loop do
|
328
|
+
response = super(request, domain, port, ssl)
|
308
329
|
now ||= Time.now
|
309
|
-
if
|
330
|
+
if response.throttled?
|
310
331
|
# if no Retry-After is not provided, the 4me server is very busy, wait 5 minutes
|
311
|
-
retry_after =
|
332
|
+
retry_after = response.retry_after.zero? ? 300 : [response.retry_after, 2].max
|
312
333
|
if (Time.now - now + retry_after) < option(:max_throttle_time)
|
313
|
-
@logger.warn { "Request throttled, trying again in #{retry_after} seconds: #{
|
334
|
+
@logger.warn { "Request throttled, trying again in #{retry_after} seconds: #{response.message}" }
|
314
335
|
sleep(retry_after)
|
315
336
|
else
|
316
337
|
timed_out = true
|
317
338
|
end
|
318
339
|
end
|
319
|
-
|
320
|
-
|
340
|
+
break unless response.throttled? && !timed_out
|
341
|
+
end
|
342
|
+
response
|
321
343
|
end
|
322
344
|
end
|
323
|
-
Client.
|
345
|
+
Client.prepend SendWithRateLimitBlock
|
324
346
|
|
325
347
|
module SendWithRetries
|
326
348
|
# Wraps the _send method with retries when the server does not respond, see +initialize+ option +:retries+
|
327
349
|
def _send(request, domain = @domain, port = @port, ssl = @ssl)
|
328
|
-
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
|
+
|
329
352
|
retries = 0
|
330
353
|
sleep_time = 1
|
331
354
|
now = nil
|
332
355
|
timed_out = false
|
333
|
-
|
334
|
-
|
356
|
+
response = nil
|
357
|
+
loop do
|
358
|
+
response = super(request, domain, port, ssl)
|
335
359
|
now ||= Time.now
|
336
|
-
if
|
360
|
+
if response.failure?
|
337
361
|
sleep_time *= 2
|
338
362
|
if (Time.now - now + sleep_time) < option(:max_retry_time)
|
339
|
-
@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}" }
|
340
364
|
sleep(sleep_time)
|
341
365
|
else
|
342
366
|
timed_out = true
|
343
367
|
end
|
344
368
|
end
|
345
|
-
|
346
|
-
|
369
|
+
break unless response.failure? && !timed_out
|
370
|
+
end
|
371
|
+
response
|
347
372
|
end
|
348
373
|
end
|
349
|
-
Client.
|
374
|
+
Client.prepend SendWithRetries
|
350
375
|
end
|
351
376
|
|
352
377
|
# HTTPS with certificate bundle
|
353
378
|
module Net
|
354
379
|
class HTTP
|
355
|
-
|
380
|
+
alias original_use_ssl= use_ssl=
|
356
381
|
|
357
382
|
def use_ssl=(flag)
|
358
383
|
self.ca_file = File.expand_path(Sdk4me.configuration.current[:ca_file], __FILE__) if flag
|
@@ -361,4 +386,3 @@ module Net
|
|
361
386
|
end
|
362
387
|
end
|
363
388
|
end
|
364
|
-
|
@@ -1,109 +1,143 @@
|
|
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
|
-
|
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 ![](storage/abc/adjhajdhjaadf.png) and ![](storage/abc/fskdhakjfkjdssdf.png])",
|
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?
|
49
|
+
end
|
20
50
|
|
21
|
-
#
|
22
|
-
|
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
|
+
"![](#{attachment[:key]})" # magic markdown for inline attachments
|
63
|
+
else
|
64
|
+
match
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
23
68
|
end
|
24
69
|
|
25
70
|
private
|
26
71
|
|
27
|
-
def
|
28
|
-
|
29
|
-
|
30
|
-
:remarks_attachments
|
31
|
-
when /service_offerings/
|
32
|
-
:summary_attachments
|
33
|
-
else
|
34
|
-
:note_attachments
|
35
|
-
end
|
72
|
+
def raise_error(message)
|
73
|
+
@client.logger.error { message }
|
74
|
+
raise Sdk4me::UploadFailed, message
|
36
75
|
end
|
37
76
|
|
38
|
-
def
|
39
|
-
|
40
|
-
raise Sdk4me::UploadFailed.new(message)
|
41
|
-
else
|
42
|
-
@client.logger.error{ message }
|
43
|
-
end
|
77
|
+
def storage
|
78
|
+
@storage ||= @client.get('/attachments/storage').json.with_indifferent_access
|
44
79
|
end
|
45
80
|
|
46
|
-
#
|
47
|
-
#
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
unless attachment.respond_to?(:path) && attachment.respond_to?(:read)
|
52
|
-
raise "file does not exist: #{attachment}" unless File.exists?(attachment)
|
53
|
-
attachment = File.open(attachment, 'rb')
|
54
|
-
end
|
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
|
55
86
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
rescue ::Exception => e
|
65
|
-
report_error("Attachment upload failed: #{e.message}", raise_exceptions)
|
66
|
-
nil
|
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')
|
67
95
|
end
|
96
|
+
|
97
|
+
key_template = storage[provider][:key]
|
98
|
+
key = key_template.sub(FILENAME_TEMPLATE, File.basename(attachment.path))
|
99
|
+
|
100
|
+
key = if provider == S3_PROVIDER
|
101
|
+
upload_to_s3(key, attachment)
|
102
|
+
else
|
103
|
+
upload_to_4me_local(key, attachment)
|
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}")
|
68
110
|
end
|
69
111
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
AWSAccessKeyId: aws[:access_key],
|
76
|
-
acl: 'private',
|
77
|
-
signature: aws[:signature],
|
78
|
-
success_action_status: 201,
|
79
|
-
policy: aws[:policy],
|
80
|
-
file: attachment # file must be last
|
81
|
-
})
|
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 }))
|
116
|
+
|
82
117
|
# this is a bit of a hack, but Amazon S3 returns only XML :(
|
83
|
-
xml = response.
|
84
|
-
error = xml[
|
85
|
-
raise "AWS upload to #{
|
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
|
86
121
|
|
87
|
-
|
88
|
-
response = @client.get(aws[:success_url].split('/').last, {key: key}, @client.send(:expand_header))
|
89
|
-
raise "4me confirmation #{aws[:success_url].split('/').last} for #{key} failed: #{response.message}" unless response.valid?
|
122
|
+
xml[%r{<Key>(.*)</Key>}, 1]
|
90
123
|
end
|
91
124
|
|
92
|
-
#
|
93
|
-
def
|
94
|
-
uri = storage[:upload_uri]
|
95
|
-
response = send_file(uri, {file: attachment
|
96
|
-
raise "4me upload to #{
|
125
|
+
# Upload the file directly to 4me local storage
|
126
|
+
def upload_to_4me_local(key, attachment)
|
127
|
+
uri = storage[:upload_uri]
|
128
|
+
response = send_file(uri, storage[:local].merge({ file: attachment }), @client.send(:expand_header))
|
129
|
+
raise "4me upload to #{uri} for #{key} failed: #{response.message}" unless response.valid?
|
130
|
+
|
131
|
+
JSON.parse(response.body)['key']
|
97
132
|
end
|
98
133
|
|
99
134
|
def send_file(uri, params, basic_auth_header = {})
|
100
|
-
params = {
|
135
|
+
params = { 'Content-Type': MIME::Types.type_for(params[:key])[0] || MIME::Types['application/octet-stream'][0] }.merge(params)
|
101
136
|
data, header = Sdk4me::Multipart::Post.prepare_query(params)
|
102
137
|
ssl, domain, port, path = @client.send(:ssl_domain_port_path, uri)
|
103
138
|
request = Net::HTTP::Post.new(path, basic_auth_header.merge(header))
|
104
139
|
request.body = data
|
105
140
|
@client.send(:_send, request, domain, port, ssl)
|
106
141
|
end
|
107
|
-
|
108
142
|
end
|
109
143
|
end
|