4me-sdk 1.2.0 → 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.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
  #
@@ -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
- [:host, :api_version].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
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.new("Missing required configuration option access_token")
73
+ raise ::Sdk4me::Exception, 'Missing required configuration option access_token'
70
74
  else
71
- @logger.info('Use of api_token is deprecated, consider switching to access_token instead.')
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.new(response.message) unless response.valid?
97
+ raise ::Sdk4me::Exception, response.message unless response.valid?
98
+
94
99
  # yield the resources
95
- response.json.each{ |resource| yield resource }
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 put(path, data = {}, header = {})
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
- alias_method :patch, :put
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.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
+
139
145
  token = response[:token]
140
- while true
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.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?
146
152
  end
147
153
  # wait 30 seconds while the response is OK and import is still busy
148
- break unless ['queued', 'processing'].include?(response[:state])
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.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
+
178
186
  token = response[:token]
179
- while true
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.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?
185
193
  end
186
194
  # wait 30 seconds while the response is OK and export is still busy
187
- break unless ['queued', 'processing'].include?(response[:state])
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,41 +202,37 @@ module Sdk4me
193
202
  response
194
203
  end
195
204
 
196
- def logger
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 = {}, header = {})
204
- Sdk4me::Attachments.new(self).upload_attachments!(path, data)
205
- 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)
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
211
218
 
212
- URI_ESCAPE_PATTERN = Regexp.new("[^#{URI::PATTERN::UNRESERVED}]")
213
219
  def uri_escape(value)
214
- URI.escape(value, URI_ESCAPE_PATTERN).gsub('.', '%2E')
220
+ URI.encode_www_form_component(value).gsub('+', '%20').gsub('.', '%2E')
215
221
  end
216
222
 
217
223
  # Expand the given header with the default header
218
- def expand_header(header = {})
219
- header = DEFAULT_HEADER.merge(header)
224
+ def expand_header(headers = {})
225
+ header = DEFAULT_HEADER.dup
220
226
  header['X-4me-Account'] = option(:account) if option(:account)
221
227
  if option(:access_token).present?
222
- header['AUTHORIZATION'] = 'Bearer ' + option(:access_token)
228
+ header['AUTHORIZATION'] = "Bearer #{option(:access_token)}"
223
229
  else
224
230
  token_and_password = option(:api_token).include?(':') ? option(:api_token) : "#{option(:api_token)}:x"
225
- header['AUTHORIZATION'] = 'Basic ' + [token_and_password].pack('m*').gsub(/\s/, '')
226
- end
227
- if option(:source)
228
- header['X-4me-Source'] = option(:source)
229
- header['HTTP_USER_AGENT'] = option(:source)
231
+ header['AUTHORIZATION'] = "Basic #{[token_and_password].pack('m*').gsub(/\s/, '')}"
230
232
  end
233
+ header['X-4me-Source'] = option(:source) if option(:source)
234
+ header['User-Agent'] = option(:user_agent) if option(:user_agent)
235
+ header.merge!(headers)
231
236
  header
232
237
  end
233
238
 
@@ -238,8 +243,8 @@ module Sdk4me
238
243
  # fields: ['id', 'created_at', 'sourceID']
239
244
  def expand_path(path, params = {})
240
245
  path = path.dup
241
- path = "/#{path}" unless path =~ /^\// # make sure path starts with /
242
- path = "/#{option(:api_version)}#{path}" unless path =~ /^\/v[\d.]+\// # preprend api version
246
+ path = "/#{path}" unless path =~ %r{^/} # make sure path starts with /
247
+ path = "/#{option(:api_version)}#{path}" unless path =~ %r{^/v[\d.]+/} # preprend api version
243
248
  params.each do |key, value|
244
249
  path << (path['?'] ? '&' : '?')
245
250
  path << expand_param(key, value)
@@ -258,18 +263,20 @@ module Sdk4me
258
263
  # Parameter value typecasting
259
264
  def typecast(value, escape = true)
260
265
  case value.class.name.to_sym
261
- when :NilClass then ''
262
- when :String then escape ? uri_escape(value) : value
263
- when :TrueClass then 'true'
264
- when :FalseClass then 'false'
265
- when :DateTime then datetime = value.new_offset(0).iso8601; escape ? uri_escape(datetime) : datetime
266
- when :Date then value.strftime("%Y-%m-%d")
267
- when :Time then value.strftime("%H:%M")
266
+ when :NilClass then ''
267
+ when :String then escape ? uri_escape(value) : value
268
+ when :TrueClass then 'true'
269
+ when :FalseClass then 'false'
270
+ when :DateTime
271
+ datetime = value.new_offset(0).iso8601
272
+ escape ? uri_escape(datetime) : datetime
273
+ when :Date then value.strftime('%Y-%m-%d')
274
+ when :Time then value.strftime('%H:%M')
268
275
  # do not convert arrays in put/post requests as squashing arrays is only used in filtering
269
- when :Array then escape ? value.map{ |v| typecast(v, escape) }.join(',') : value
276
+ when :Array then escape ? value.map { |v| typecast(v, escape) }.join(',') : value
270
277
  # TODO: temporary for special constructions to update contact details, see Request #1444166
271
- when :Hash then escape ? value.to_s : value
272
- else escape ? value.to_json : value.to_s
278
+ when :Hash then escape ? value.to_s : value
279
+ else escape ? value.to_json : value.to_s
273
280
  end
274
281
  end
275
282
 
@@ -277,27 +284,27 @@ module Sdk4me
277
284
  # Guaranteed to return a Response, thought it may be +empty?+
278
285
  def _send(request, domain = @domain, port = @port, ssl = @ssl)
279
286
  @logger.debug { "Sending #{request.method} request to #{domain}:#{port}#{request.path}" }
280
- _response = begin
287
+ response = begin
281
288
  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
289
  http = http_with_proxy.new(domain, port)
283
290
  http.read_timeout = option(:read_timeout)
284
291
  http.use_ssl = ssl
285
292
  http.verify_mode = OpenSSL::SSL::VERIFY_NONE if @ssl_verify_none
286
- http.start{ |_http| _http.request(request) }
287
- rescue ::Exception => e
293
+ http.start { |transport| transport.request(request) }
294
+ rescue StandardError => e
288
295
  Struct.new(:body, :message, :code, :header).new(nil, "No Response from Server - #{e.message} for '#{domain}:#{port}#{request.path}'", 500, {})
289
296
  end
290
- response = Sdk4me::Response.new(request, _response)
291
- if response.valid?
292
- @logger.debug { "Response:\n#{JSON.pretty_generate(response.json)}" }
293
- elsif response.raw.body =~ /^\s*<\?xml/i
294
- @logger.debug { "XML response:\n#{response.raw.body}" }
295
- elsif '303' == response.raw.code.to_s
296
- @logger.debug { "Redirect: #{response.raw.header['Location']}" }
297
+ resp = Sdk4me::Response.new(request, response)
298
+ if resp.valid?
299
+ @logger.debug { "Response:\n#{JSON.pretty_generate(resp.json)}" }
300
+ elsif resp.raw.body =~ /^\s*<\?xml/i
301
+ @logger.debug { "XML response:\n#{resp.raw.body}" }
302
+ elsif resp.raw.code.to_s == '303'
303
+ @logger.debug { "Redirect: #{resp.raw.header['Location']}" }
297
304
  else
298
- @logger.error { "#{request.method} request to #{domain}:#{port}#{request.path} failed: #{response.message}" }
305
+ @logger.error { "#{request.method} request to #{domain}:#{port}#{request.path} failed: #{resp.message}" }
299
306
  end
300
- response
307
+ resp
301
308
  end
302
309
 
303
310
  # parse the given URI to [domain, port, ssl, path]
@@ -306,65 +313,70 @@ module Sdk4me
306
313
  ssl = uri.scheme == 'https'
307
314
  [ssl, uri.host, uri.port, uri.path]
308
315
  end
309
-
310
316
  end
311
317
 
312
318
  module SendWithRateLimitBlock
313
319
  # Wraps the _send method with retries when the server does not respond, see +initialize+ option +:rate_limit_block+
314
320
  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) > 0
321
+ return super(request, domain, port, ssl) unless option(:block_at_rate_limit) && option(:max_throttle_time).positive?
322
+
316
323
  now = nil
317
324
  timed_out = false
318
- begin
319
- _response = super(request, domain, port, ssl)
325
+ response = nil
326
+ loop do
327
+ response = super(request, domain, port, ssl)
320
328
  now ||= Time.now
321
- if _response.throttled?
329
+ if response.throttled?
322
330
  # if no Retry-After is not provided, the 4me server is very busy, wait 5 minutes
323
- retry_after = _response.retry_after == 0 ? 300 : [_response.retry_after, 2].max
331
+ retry_after = response.retry_after.zero? ? 300 : [response.retry_after, 2].max
324
332
  if (Time.now - now + retry_after) < option(:max_throttle_time)
325
- @logger.warn { "Request throttled, trying again in #{retry_after} seconds: #{_response.message}" }
333
+ @logger.warn { "Request throttled, trying again in #{retry_after} seconds: #{response.message}" }
326
334
  sleep(retry_after)
327
335
  else
328
336
  timed_out = true
329
337
  end
330
338
  end
331
- end while _response.throttled? && !timed_out
332
- _response
339
+ break unless response.throttled? && !timed_out
340
+ end
341
+ response
333
342
  end
334
343
  end
335
- Client.send(:prepend, SendWithRateLimitBlock)
344
+ Client.prepend SendWithRateLimitBlock
336
345
 
337
346
  module SendWithRetries
338
347
  # Wraps the _send method with retries when the server does not respond, see +initialize+ option +:retries+
339
348
  def _send(request, domain = @domain, port = @port, ssl = @ssl)
340
- return super(request, domain, port, ssl) unless option(:max_retry_time) > 0
349
+ return super(request, domain, port, ssl) unless option(:max_retry_time).positive?
350
+
341
351
  retries = 0
342
352
  sleep_time = 1
343
353
  now = nil
344
354
  timed_out = false
345
- begin
346
- _response = super(request, domain, port, ssl)
355
+ response = nil
356
+ loop do
357
+ response = super(request, domain, port, ssl)
347
358
  now ||= Time.now
348
- if _response.failure?
359
+ if response.failure?
349
360
  sleep_time *= 2
350
361
  if (Time.now - now + sleep_time) < option(:max_retry_time)
351
- @logger.warn { "Request failed, retry ##{retries += 1} in #{sleep_time} seconds: #{_response.message}" }
362
+ @logger.warn { "Request failed, retry ##{retries += 1} in #{sleep_time} seconds: #{response.message}" }
352
363
  sleep(sleep_time)
353
364
  else
354
365
  timed_out = true
355
366
  end
356
367
  end
357
- end while _response.failure? && !timed_out
358
- _response
368
+ break unless response.failure? && !timed_out
369
+ end
370
+ response
359
371
  end
360
372
  end
361
- Client.send(:prepend, SendWithRetries)
373
+ Client.prepend SendWithRetries
362
374
  end
363
375
 
364
376
  # HTTPS with certificate bundle
365
377
  module Net
366
378
  class HTTP
367
- alias_method :original_use_ssl=, :use_ssl=
379
+ alias original_use_ssl= use_ssl=
368
380
 
369
381
  def use_ssl=(flag)
370
382
  self.ca_file = File.expand_path(Sdk4me.configuration.current[:ca_file], __FILE__) if flag
@@ -373,4 +385,3 @@ module Net
373
385
  end
374
386
  end
375
387
  end
376
-
@@ -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/adjhajdhjaadf.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