4me-sdk 1.1.8 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.rb'
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.rb'
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 = {'Content-Type' => 'application/json'}
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.api_token = 'd41f5868feb65fc87fa2311a473a8766ea38bc40'
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
- # - api_token: *required* The 4me API token
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 http://developer.4me.com/v1/#multiple-accounts
47
+ # @see https://developer.4me.com/v1/#multiple-accounts
45
48
  # - source: The Source used when creating new records
46
- # @see http://developer.4me.com/v1/general/source/
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 http://developer.4me.com/v1/#rate-limiting
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
- [:host, :api_version, :api_token].each do |required_option|
63
- raise ::Sdk4me::Exception.new("Missing required configuration option #{required_option}") if option(required_option).blank?
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.new(response.message) unless response.valid?
97
+ raise ::Sdk4me::Exception, response.message unless response.valid?
98
+
87
99
  # yield the resources
88
- response.json.each{ |resource| yield resource }
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 put(path, data = {}, header = {})
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
- alias_method :patch, :put
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.new("Failed to queue #{type} import. #{response.message}") unless response.valid?
143
+ raise ::Sdk4me::UploadFailed, "Failed to queue #{type} import. #{response.message}" unless response.valid?
144
+
132
145
  token = response[:token]
133
- while true
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.new("Unable to monitor progress for #{type} import. #{response.message}") unless response.valid?
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 ['queued', 'processing'].include?(response[:state])
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.new("Failed to queue '#{data[:type]}' export. #{response.message}") unless response.valid?
184
+ raise ::Sdk4me::UploadFailed, "Failed to queue '#{data[:type]}' export. #{response.message}" unless response.valid?
185
+
171
186
  token = response[:token]
172
- while true
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.new("Unable to monitor progress for '#{data[:type]}' export. #{response.message}") unless response.valid?
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 ['queued', 'processing'].include?(response[:state])
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
- def logger
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 = {}, header = {})
197
- Sdk4me::Attachments.new(self).upload_attachments!(path, data)
198
- request = request_class.new(expand_path(path), expand_header(header))
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,15 +222,18 @@ module Sdk4me
208
222
  end
209
223
 
210
224
  # Expand the given header with the default header
211
- def expand_header(header = {})
212
- header = DEFAULT_HEADER.merge(header)
225
+ def expand_header(headers = {})
226
+ header = DEFAULT_HEADER.dup
213
227
  header['X-4me-Account'] = option(:account) if option(:account)
214
- token_and_password = option(:api_token).include?(':') ? option(:api_token) : "#{option(:api_token)}:x"
215
- header['AUTHORIZATION'] = 'Basic ' + [token_and_password].pack('m*').gsub(/\s/, '')
216
- if option(:source)
217
- header['X-4me-Source'] = option(:source)
218
- header['HTTP_USER_AGENT'] = option(:source)
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/, '')}"
219
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)
220
237
  header
221
238
  end
222
239
 
@@ -227,8 +244,8 @@ module Sdk4me
227
244
  # fields: ['id', 'created_at', 'sourceID']
228
245
  def expand_path(path, params = {})
229
246
  path = path.dup
230
- path = "/#{path}" unless path =~ /^\// # make sure path starts with /
231
- path = "/#{option(:api_version)}#{path}" unless path =~ /^\/v[\d.]+\// # preprend api version
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
232
249
  params.each do |key, value|
233
250
  path << (path['?'] ? '&' : '?')
234
251
  path << expand_param(key, value)
@@ -247,18 +264,20 @@ module Sdk4me
247
264
  # Parameter value typecasting
248
265
  def typecast(value, escape = true)
249
266
  case value.class.name.to_sym
250
- when :NilClass then ''
251
- when :String then escape ? uri_escape(value) : value
252
- when :TrueClass then 'true'
253
- when :FalseClass then 'false'
254
- when :DateTime then datetime = value.new_offset(0).iso8601; escape ? uri_escape(datetime) : datetime
255
- when :Date then value.strftime("%Y-%m-%d")
256
- when :Time then value.strftime("%H:%M")
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')
257
276
  # do not convert arrays in put/post requests as squashing arrays is only used in filtering
258
- when :Array then escape ? value.map{ |v| typecast(v, escape) }.join(',') : value
277
+ when :Array then escape ? value.map { |v| typecast(v, escape) }.join(',') : value
259
278
  # TODO: temporary for special constructions to update contact details, see Request #1444166
260
- when :Hash then escape ? value.to_s : value
261
- else escape ? value.to_json : value.to_s
279
+ when :Hash then escape ? value.to_s : value
280
+ else escape ? value.to_json : value.to_s
262
281
  end
263
282
  end
264
283
 
@@ -266,27 +285,27 @@ module Sdk4me
266
285
  # Guaranteed to return a Response, thought it may be +empty?+
267
286
  def _send(request, domain = @domain, port = @port, ssl = @ssl)
268
287
  @logger.debug { "Sending #{request.method} request to #{domain}:#{port}#{request.path}" }
269
- _response = begin
288
+ response = begin
270
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))
271
290
  http = http_with_proxy.new(domain, port)
272
291
  http.read_timeout = option(:read_timeout)
273
292
  http.use_ssl = ssl
274
293
  http.verify_mode = OpenSSL::SSL::VERIFY_NONE if @ssl_verify_none
275
- http.start{ |_http| _http.request(request) }
276
- rescue ::Exception => e
294
+ http.start { |transport| transport.request(request) }
295
+ rescue StandardError => e
277
296
  Struct.new(:body, :message, :code, :header).new(nil, "No Response from Server - #{e.message} for '#{domain}:#{port}#{request.path}'", 500, {})
278
297
  end
279
- response = Sdk4me::Response.new(request, _response)
280
- if response.valid?
281
- @logger.debug { "Response:\n#{JSON.pretty_generate(response.json)}" }
282
- elsif response.raw.body =~ /^\s*<\?xml/i
283
- @logger.debug { "XML response:\n#{response.raw.body}" }
284
- elsif '303' == response.raw.code.to_s
285
- @logger.debug { "Redirect: #{response.raw.header['Location']}" }
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']}" }
286
305
  else
287
- @logger.error { "#{request.method} request to #{domain}:#{port}#{request.path} failed: #{response.message}" }
306
+ @logger.error { "#{request.method} request to #{domain}:#{port}#{request.path} failed: #{resp.message}" }
288
307
  end
289
- response
308
+ resp
290
309
  end
291
310
 
292
311
  # parse the given URI to [domain, port, ssl, path]
@@ -295,65 +314,70 @@ module Sdk4me
295
314
  ssl = uri.scheme == 'https'
296
315
  [ssl, uri.host, uri.port, uri.path]
297
316
  end
298
-
299
317
  end
300
318
 
301
319
  module SendWithRateLimitBlock
302
320
  # Wraps the _send method with retries when the server does not respond, see +initialize+ option +:rate_limit_block+
303
321
  def _send(request, domain = @domain, port = @port, ssl = @ssl)
304
- return super(request, domain, port, ssl) unless option(:block_at_rate_limit) && option(:max_throttle_time) > 0
322
+ return super(request, domain, port, ssl) unless option(:block_at_rate_limit) && option(:max_throttle_time).positive?
323
+
305
324
  now = nil
306
325
  timed_out = false
307
- begin
308
- _response = super(request, domain, port, ssl)
326
+ response = nil
327
+ loop do
328
+ response = super(request, domain, port, ssl)
309
329
  now ||= Time.now
310
- if _response.throttled?
330
+ if response.throttled?
311
331
  # if no Retry-After is not provided, the 4me server is very busy, wait 5 minutes
312
- retry_after = _response.retry_after == 0 ? 300 : [_response.retry_after, 2].max
332
+ retry_after = response.retry_after.zero? ? 300 : [response.retry_after, 2].max
313
333
  if (Time.now - now + retry_after) < option(:max_throttle_time)
314
- @logger.warn { "Request throttled, trying again in #{retry_after} seconds: #{_response.message}" }
334
+ @logger.warn { "Request throttled, trying again in #{retry_after} seconds: #{response.message}" }
315
335
  sleep(retry_after)
316
336
  else
317
337
  timed_out = true
318
338
  end
319
339
  end
320
- end while _response.throttled? && !timed_out
321
- _response
340
+ break unless response.throttled? && !timed_out
341
+ end
342
+ response
322
343
  end
323
344
  end
324
- Client.send(:prepend, SendWithRateLimitBlock)
345
+ Client.prepend SendWithRateLimitBlock
325
346
 
326
347
  module SendWithRetries
327
348
  # Wraps the _send method with retries when the server does not respond, see +initialize+ option +:retries+
328
349
  def _send(request, domain = @domain, port = @port, ssl = @ssl)
329
- return super(request, domain, port, ssl) unless option(:max_retry_time) > 0
350
+ return super(request, domain, port, ssl) unless option(:max_retry_time).positive?
351
+
330
352
  retries = 0
331
353
  sleep_time = 1
332
354
  now = nil
333
355
  timed_out = false
334
- begin
335
- _response = super(request, domain, port, ssl)
356
+ response = nil
357
+ loop do
358
+ response = super(request, domain, port, ssl)
336
359
  now ||= Time.now
337
- if _response.failure?
360
+ if response.failure?
338
361
  sleep_time *= 2
339
362
  if (Time.now - now + sleep_time) < option(:max_retry_time)
340
- @logger.warn { "Request failed, retry ##{retries += 1} in #{sleep_time} seconds: #{_response.message}" }
363
+ @logger.warn { "Request failed, retry ##{retries += 1} in #{sleep_time} seconds: #{response.message}" }
341
364
  sleep(sleep_time)
342
365
  else
343
366
  timed_out = true
344
367
  end
345
368
  end
346
- end while _response.failure? && !timed_out
347
- _response
369
+ break unless response.failure? && !timed_out
370
+ end
371
+ response
348
372
  end
349
373
  end
350
- Client.send(:prepend, SendWithRetries)
374
+ Client.prepend SendWithRetries
351
375
  end
352
376
 
353
377
  # HTTPS with certificate bundle
354
378
  module Net
355
379
  class HTTP
356
- alias_method :original_use_ssl=, :use_ssl=
380
+ alias original_use_ssl= use_ssl=
357
381
 
358
382
  def use_ssl=(flag)
359
383
  self.ca_file = File.expand_path(Sdk4me.configuration.current[:ca_file], __FILE__) if flag
@@ -362,4 +386,3 @@ module Net
362
386
  end
363
387
  end
364
388
  end
365
-
@@ -1,152 +1,143 @@
1
1
  module Sdk4me
2
2
  class Attachments
3
+ S3_PROVIDER = 's3'.freeze
4
+ FILENAME_TEMPLATE = '${filename}'.freeze
3
5
 
4
- AWS_PROVIDER = 'aws'
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
- # upload the attachments and return the data with the uploaded attachment info
12
- # Two flavours available
13
- # * data[:attachments]
14
- # * data[:note] containing text with '[attachment:/tmp/images/green_fuzz.jpg]'
15
- def upload_attachments!(path, data)
16
- upload_options = {
17
- raise_exceptions: !!data.delete(:attachments_exception),
18
- attachments_field: attachments_field(path),
19
- }
20
- uploaded_attachments = upload_normal_attachments!(path, data, upload_options)
21
- uploaded_attachments += upload_inline_attachments!(path, data, upload_options)
22
- # jsonify the attachments, if any were uploaded
23
- data[upload_options[:attachments_field]] = uploaded_attachments.compact.to_json if uploaded_attachments.compact.any?
24
- end
25
-
26
- private
27
-
28
- # upload the attachments in :attachments to 4me and return the data with the uploaded attachment info
29
- def upload_normal_attachments!(path, data, upload_options)
30
- attachments = [data.delete(:attachments)].flatten.compact
31
- return [] if attachments.empty?
32
-
33
- upload_options[:storage] ||= storage(path, upload_options[:raise_exceptions])
34
- return [] unless upload_options[:storage]
35
-
36
- attachments.map do |attachment|
37
- upload_attachment(upload_options[:storage], attachment, upload_options[:raise_exceptions])
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?
38
49
  end
39
- end
40
50
 
41
- INLINE_ATTACHMENT_REGEXP = /\[attachment:([^\]]+)\]/.freeze
42
- # upload any '[attachment:/tmp/images/green_fuzz.jpg]' in :note text field to 4me as inline attachment and add the s3 key to the text
43
- def upload_inline_attachments!(path, data, upload_options)
44
- text_field = upload_options[:attachments_field].to_s.gsub('_attachments', '').to_sym
45
- return [] unless (data[text_field] || '') =~ INLINE_ATTACHMENT_REGEXP
46
-
47
- upload_options[:storage] ||= storage(path, upload_options[:raise_exceptions])
48
- return [] unless upload_options[:storage]
49
-
50
- attachments = []
51
- data[text_field] = data[text_field].gsub(INLINE_ATTACHMENT_REGEXP) do |full_match|
52
- attachment_details = upload_attachment(upload_options[:storage], $~[1], upload_options[:raise_exceptions])
53
- if attachment_details
54
- attachments << attachment_details.merge(inline: true)
55
- "![](#{attachment_details[:key]})" # magic markdown for inline attachments
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
+ "![](#{attachment[:key]})" # 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
- def storage(path, raise_exceptions)
64
- # retrieve the upload configuration for this record from 4me
65
- storage = @client.get(path =~ /\d+$/ ? path : "#{path}/new", {attachment_upload_token: true}, @client.send(:expand_header))[:storage_upload]
66
- report_error("Attachments not allowed for #{path}", raise_exceptions) unless storage
67
- storage
68
- end
70
+ private
69
71
 
70
- def attachments_field(path)
71
- case path
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
72
+ def raise_error(message)
73
+ @client.logger.error { message }
74
+ raise Sdk4me::UploadFailed, message
79
75
  end
80
76
 
81
- def report_error(message, raise_exceptions)
82
- if raise_exceptions
83
- raise Sdk4me::UploadFailed.new(message)
84
- else
85
- @client.logger.error{ message }
86
- end
77
+ def storage
78
+ @storage ||= @client.get('/attachments/storage').json.with_indifferent_access
87
79
  end
88
80
 
89
- # upload a single attachment and return the data for the note_attachments
90
- # returns nil and provides an error in case the attachment upload failed
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
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
98
89
 
99
- # there are two different upload methods: AWS S3 and 4me local storage
100
- key_template = "#{storage[:upload_path]}#{FILENAME_TEMPLATE}"
101
- key = key_template.gsub(FILENAME_TEMPLATE, File.basename(attachment.path))
102
- upload_method = storage[:provider] == AWS_PROVIDER ? :aws_upload : :upload_to_4me
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
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')
110
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}")
111
110
  end
112
111
 
113
- def aws_upload(aws, key_template, key, attachment)
114
- # upload the file to AWS
115
- response = send_file(aws[:upload_uri], {
116
- :'x-amz-server-side-encryption' => 'AES256',
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
- })
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
+
125
117
  # 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
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
129
121
 
130
- # inform 4me of the successful upload
131
- response = @client.get(aws[:success_url].split('/').last, {key: key}, @client.send(:expand_header))
132
- raise "4me confirmation #{aws[:success_url].split('/').last} for #{key} failed: #{response.message}" unless response.valid?
122
+ xml[%r{<Key>(.*)</Key>}, 1]
133
123
  end
134
124
 
135
- # upload the file directly to 4me
136
- def upload_to_4me(storage, key_template, key, attachment)
137
- uri = storage[:upload_uri] =~ /\/v1/ ? storage[:upload_uri] : storage[:upload_uri].gsub('/attachments', '/v1/attachments')
138
- response = send_file(uri, {file: attachment, key: key_template}, @client.send(:expand_header))
139
- raise "4me upload to #{storage[:upload_uri]} for #{key} failed: #{response.message}" unless response.valid?
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']
140
132
  end
141
133
 
142
134
  def send_file(uri, params, basic_auth_header = {})
143
- params = {:'Content-Type' => MIME::Types.type_for(params[:key])[0] || MIME::Types["application/octet-stream"][0]}.merge(params)
135
+ params = { 'Content-Type': MIME::Types.type_for(params[:key])[0] || MIME::Types['application/octet-stream'][0] }.merge(params)
144
136
  data, header = Sdk4me::Multipart::Post.prepare_query(params)
145
137
  ssl, domain, port, path = @client.send(:ssl_domain_port_path, uri)
146
138
  request = Net::HTTP::Post.new(path, basic_auth_header.merge(header))
147
139
  request.body = data
148
140
  @client.send(:_send, request, domain, port, ssl)
149
141
  end
150
-
151
142
  end
152
143
  end