4me-sdk 1.1.5 → 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 +5 -5
- data/4me-sdk.gemspec +12 -13
- data/Gemfile.lock +58 -39
- data/LICENSE +1 -1
- data/README.md +64 -39
- data/lib/sdk4me.rb +10 -8
- data/lib/sdk4me/ca-bundle.crt +1327 -1802
- data/lib/sdk4me/client.rb +116 -92
- data/lib/sdk4me/client/attachments.rb +106 -76
- data/lib/sdk4me/client/multipart.rb +16 -18
- data/lib/sdk4me/client/response.rb +22 -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 -233
- data/spec/lib/sdk4me_spec.rb +9 -9
- 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 { "
|
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)
|
304
|
-
|
322
|
+
return super(request, domain, port, ssl) unless option(:block_at_rate_limit) && option(:max_throttle_time).positive?
|
323
|
+
|
324
|
+
now = nil
|
305
325
|
timed_out = false
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
if
|
311
|
-
|
312
|
-
|
313
|
-
|
326
|
+
response = nil
|
327
|
+
loop do
|
328
|
+
response = super(request, domain, port, ssl)
|
329
|
+
now ||= Time.now
|
330
|
+
if response.throttled?
|
331
|
+
# if no Retry-After is not provided, the 4me server is very busy, wait 5 minutes
|
332
|
+
retry_after = response.retry_after.zero? ? 300 : [response.retry_after, 2].max
|
333
|
+
if (Time.now - now + retry_after) < option(:max_throttle_time)
|
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
|
-
now =
|
354
|
+
now = nil
|
332
355
|
timed_out = false
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
356
|
+
response = nil
|
357
|
+
loop do
|
358
|
+
response = super(request, domain, port, ssl)
|
359
|
+
now ||= Time.now
|
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,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
|
-
|
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?
|
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
|
+
"" # 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
|
-
|
65
|
-
|
66
|
-
|
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')
|
95
|
+
end
|
96
|
+
|
97
|
+
key_template = storage[provider][:key]
|
98
|
+
key = key_template.sub(FILENAME_TEMPLATE, File.basename(attachment.path))
|
99
|
+
|
100
|
+
if provider == S3_PROVIDER
|
101
|
+
upload_to_s3(key, attachment)
|
102
|
+
else
|
103
|
+
upload_to_4me_local(key, attachment)
|
67
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
|
-
key: key_template,
|
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
|
-
})
|
82
|
-
# this is a bit of a hack, but Amazon S3 returns only XML :(
|
83
|
-
xml = response.raw.body || ''
|
84
|
-
error = xml[/<Error>.*<Message>(.*)<\/Message>.*<\/Error>/, 1]
|
85
|
-
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 }))
|
86
116
|
|
87
|
-
#
|
88
|
-
|
89
|
-
|
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
|
90
121
|
end
|
91
122
|
|
92
|
-
#
|
93
|
-
def
|
94
|
-
uri = storage[:upload_uri]
|
95
|
-
response = send_file(uri, {file: attachment
|
96
|
-
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?
|
97
128
|
end
|
98
129
|
|
99
130
|
def send_file(uri, params, basic_auth_header = {})
|
100
|
-
params = {
|
131
|
+
params = { 'Content-Type': MIME::Types.type_for(params[:key])[0] || MIME::Types['application/octet-stream'][0] }.merge(params)
|
101
132
|
data, header = Sdk4me::Multipart::Post.prepare_query(params)
|
102
133
|
ssl, domain, port, path = @client.send(:ssl_domain_port_path, uri)
|
103
134
|
request = Net::HTTP::Post.new(path, basic_auth_header.merge(header))
|
104
135
|
request.body = data
|
105
136
|
@client.send(:_send, request, domain, port, ssl)
|
106
137
|
end
|
107
|
-
|
108
138
|
end
|
109
139
|
end
|