4me-sdk 1.2.0 → 2.0.0.pre.rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,18 +202,16 @@ 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
@@ -215,19 +222,18 @@ module Sdk4me
215
222
  end
216
223
 
217
224
  # Expand the given header with the default header
218
- def expand_header(header = {})
219
- header = DEFAULT_HEADER.merge(header)
225
+ def expand_header(headers = {})
226
+ header = DEFAULT_HEADER.dup
220
227
  header['X-4me-Account'] = option(:account) if option(:account)
221
228
  if option(:access_token).present?
222
- header['AUTHORIZATION'] = 'Bearer ' + option(:access_token)
229
+ header['AUTHORIZATION'] = "Bearer #{option(:access_token)}"
223
230
  else
224
231
  token_and_password = option(:api_token).include?(':') ? option(:api_token) : "#{option(:api_token)}:x"
225
- header['AUTHORIZATION'] = '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)
232
+ header['AUTHORIZATION'] = "Basic #{[token_and_password].pack('m*').gsub(/\s/, '')}"
230
233
  end
234
+ header['X-4me-Source'] = option(:source) if option(:source)
235
+ header['User-Agent'] = option(:user_agent) if option(:user_agent)
236
+ header.merge!(headers)
231
237
  header
232
238
  end
233
239
 
@@ -238,8 +244,8 @@ module Sdk4me
238
244
  # fields: ['id', 'created_at', 'sourceID']
239
245
  def expand_path(path, params = {})
240
246
  path = path.dup
241
- path = "/#{path}" unless path =~ /^\// # make sure path starts with /
242
- 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
243
249
  params.each do |key, value|
244
250
  path << (path['?'] ? '&' : '?')
245
251
  path << expand_param(key, value)
@@ -258,18 +264,20 @@ module Sdk4me
258
264
  # Parameter value typecasting
259
265
  def typecast(value, escape = true)
260
266
  case value.class.name.to_sym
261
- 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")
267
+ when :NilClass then ''
268
+ when :String then escape ? uri_escape(value) : value
269
+ when :TrueClass then 'true'
270
+ when :FalseClass then 'false'
271
+ when :DateTime
272
+ datetime = value.new_offset(0).iso8601
273
+ escape ? uri_escape(datetime) : datetime
274
+ when :Date then value.strftime('%Y-%m-%d')
275
+ when :Time then value.strftime('%H:%M')
268
276
  # do not convert arrays in put/post requests as squashing arrays is only used in filtering
269
- 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
270
278
  # 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
279
+ when :Hash then escape ? value.to_s : value
280
+ else escape ? value.to_json : value.to_s
273
281
  end
274
282
  end
275
283
 
@@ -277,27 +285,27 @@ module Sdk4me
277
285
  # Guaranteed to return a Response, thought it may be +empty?+
278
286
  def _send(request, domain = @domain, port = @port, ssl = @ssl)
279
287
  @logger.debug { "Sending #{request.method} request to #{domain}:#{port}#{request.path}" }
280
- _response = begin
288
+ response = begin
281
289
  http_with_proxy = option(:proxy_host).blank? ? Net::HTTP : Net::HTTP::Proxy(option(:proxy_host), option(:proxy_port), option(:proxy_user), option(:proxy_password))
282
290
  http = http_with_proxy.new(domain, port)
283
291
  http.read_timeout = option(:read_timeout)
284
292
  http.use_ssl = ssl
285
293
  http.verify_mode = OpenSSL::SSL::VERIFY_NONE if @ssl_verify_none
286
- http.start{ |_http| _http.request(request) }
287
- rescue ::Exception => e
294
+ http.start { |transport| transport.request(request) }
295
+ rescue StandardError => e
288
296
  Struct.new(:body, :message, :code, :header).new(nil, "No Response from Server - #{e.message} for '#{domain}:#{port}#{request.path}'", 500, {})
289
297
  end
290
- 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']}" }
298
+ resp = Sdk4me::Response.new(request, response)
299
+ if resp.valid?
300
+ @logger.debug { "Response:\n#{JSON.pretty_generate(resp.json)}" }
301
+ elsif resp.raw.body =~ /^\s*<\?xml/i
302
+ @logger.debug { "XML response:\n#{resp.raw.body}" }
303
+ elsif resp.raw.code.to_s == '303'
304
+ @logger.debug { "Redirect: #{resp.raw.header['Location']}" }
297
305
  else
298
- @logger.error { "#{request.method} request to #{domain}:#{port}#{request.path} failed: #{response.message}" }
306
+ @logger.error { "#{request.method} request to #{domain}:#{port}#{request.path} failed: #{resp.message}" }
299
307
  end
300
- response
308
+ resp
301
309
  end
302
310
 
303
311
  # parse the given URI to [domain, port, ssl, path]
@@ -306,65 +314,70 @@ module Sdk4me
306
314
  ssl = uri.scheme == 'https'
307
315
  [ssl, uri.host, uri.port, uri.path]
308
316
  end
309
-
310
317
  end
311
318
 
312
319
  module SendWithRateLimitBlock
313
320
  # Wraps the _send method with retries when the server does not respond, see +initialize+ option +:rate_limit_block+
314
321
  def _send(request, domain = @domain, port = @port, ssl = @ssl)
315
- return super(request, domain, port, ssl) unless option(:block_at_rate_limit) && option(:max_throttle_time) > 0
322
+ return super(request, domain, port, ssl) unless option(:block_at_rate_limit) && option(:max_throttle_time).positive?
323
+
316
324
  now = nil
317
325
  timed_out = false
318
- begin
319
- _response = super(request, domain, port, ssl)
326
+ response = nil
327
+ loop do
328
+ response = super(request, domain, port, ssl)
320
329
  now ||= Time.now
321
- if _response.throttled?
330
+ if response.throttled?
322
331
  # if no Retry-After is not provided, the 4me server is very busy, wait 5 minutes
323
- retry_after = _response.retry_after == 0 ? 300 : [_response.retry_after, 2].max
332
+ retry_after = response.retry_after.zero? ? 300 : [response.retry_after, 2].max
324
333
  if (Time.now - now + retry_after) < option(:max_throttle_time)
325
- @logger.warn { "Request throttled, trying again in #{retry_after} seconds: #{_response.message}" }
334
+ @logger.warn { "Request throttled, trying again in #{retry_after} seconds: #{response.message}" }
326
335
  sleep(retry_after)
327
336
  else
328
337
  timed_out = true
329
338
  end
330
339
  end
331
- end while _response.throttled? && !timed_out
332
- _response
340
+ break unless response.throttled? && !timed_out
341
+ end
342
+ response
333
343
  end
334
344
  end
335
- Client.send(:prepend, SendWithRateLimitBlock)
345
+ Client.prepend SendWithRateLimitBlock
336
346
 
337
347
  module SendWithRetries
338
348
  # Wraps the _send method with retries when the server does not respond, see +initialize+ option +:retries+
339
349
  def _send(request, domain = @domain, port = @port, ssl = @ssl)
340
- return super(request, domain, port, ssl) unless option(:max_retry_time) > 0
350
+ return super(request, domain, port, ssl) unless option(:max_retry_time).positive?
351
+
341
352
  retries = 0
342
353
  sleep_time = 1
343
354
  now = nil
344
355
  timed_out = false
345
- begin
346
- _response = super(request, domain, port, ssl)
356
+ response = nil
357
+ loop do
358
+ response = super(request, domain, port, ssl)
347
359
  now ||= Time.now
348
- if _response.failure?
360
+ if response.failure?
349
361
  sleep_time *= 2
350
362
  if (Time.now - now + sleep_time) < option(:max_retry_time)
351
- @logger.warn { "Request failed, retry ##{retries += 1} in #{sleep_time} seconds: #{_response.message}" }
363
+ @logger.warn { "Request failed, retry ##{retries += 1} in #{sleep_time} seconds: #{response.message}" }
352
364
  sleep(sleep_time)
353
365
  else
354
366
  timed_out = true
355
367
  end
356
368
  end
357
- end while _response.failure? && !timed_out
358
- _response
369
+ break unless response.failure? && !timed_out
370
+ end
371
+ response
359
372
  end
360
373
  end
361
- Client.send(:prepend, SendWithRetries)
374
+ Client.prepend SendWithRetries
362
375
  end
363
376
 
364
377
  # HTTPS with certificate bundle
365
378
  module Net
366
379
  class HTTP
367
- alias_method :original_use_ssl=, :use_ssl=
380
+ alias original_use_ssl= use_ssl=
368
381
 
369
382
  def use_ssl=(flag)
370
383
  self.ca_file = File.expand_path(Sdk4me.configuration.current[:ca_file], __FILE__) if flag
@@ -373,4 +386,3 @@ module Net
373
386
  end
374
387
  end
375
388
  end
376
-
@@ -1,152 +1,139 @@
1
1
  module Sdk4me
2
2
  class Attachments
3
+ S3_PROVIDER = 's3'.freeze
4
+ FILENAME_TEMPLATE = '${filename}'.freeze
3
5
 
4
- 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
70
+ private
71
+
72
+ def raise_error(message)
73
+ @client.logger.error { message }
74
+ raise Sdk4me::UploadFailed, message
68
75
  end
69
76
 
70
- def 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
77
+ def storage
78
+ @storage ||= @client.get('/attachments/storage').json.with_indifferent_access
79
79
  end
80
80
 
81
- def report_error(message, raise_exceptions)
82
- if raise_exceptions
83
- raise Sdk4me::UploadFailed.new(message)
84
- else
85
- @client.logger.error{ message }
81
+ # Upload a single attachment and return the data that should be submitted
82
+ # back to 4me. Returns nil and provides an error in case the attachment
83
+ # upload failed.
84
+ def upload_attachment(attachment)
85
+ return nil unless attachment
86
+
87
+ provider = storage[:provider]
88
+ raise 'No provider found' unless provider
89
+
90
+ # attachment is already a file or we need to open the file from disk
91
+ unless attachment.respond_to?(:path) && attachment.respond_to?(:read)
92
+ raise "file does not exist: #{attachment}" unless File.exist?(attachment)
93
+
94
+ attachment = File.open(attachment, 'rb')
86
95
  end
87
- end
88
96
 
89
- # 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
97
+ key_template = storage[provider][:key]
98
+ key = key_template.sub(FILENAME_TEMPLATE, File.basename(attachment.path))
98
99
 
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
100
+ if provider == S3_PROVIDER
101
+ upload_to_s3(key, attachment)
102
+ else
103
+ upload_to_4me_local(key, attachment)
110
104
  end
105
+
106
+ # return the values for the attachments param
107
+ { key: key, filesize: File.size(attachment.path) }
108
+ rescue StandardError => e
109
+ raise_error("Attachment upload failed: #{e.message}")
111
110
  end
112
111
 
113
- 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
- })
125
- # this is a bit of a hack, but Amazon S3 returns only XML :(
126
- xml = response.raw.body || ''
127
- error = xml[/<Error>.*<Message>(.*)<\/Message>.*<\/Error>/, 1]
128
- raise "AWS upload to #{aws[:upload_uri]} for #{key} failed: #{error}" if error
112
+ # Upload the file to AWS S3 storage
113
+ def upload_to_s3(key, attachment)
114
+ uri = storage[:upload_uri]
115
+ response = send_file(uri, storage[:s3].merge({ file: attachment }))
129
116
 
130
- # 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?
117
+ # this is a bit of a hack, but Amazon S3 returns only XML :(
118
+ xml = response.body || ''
119
+ error = xml[%r{<Error>.*<Message>(.*)</Message>.*</Error>}, 1]
120
+ raise "AWS S3 upload to #{uri} for #{key} failed: #{error}" if error
133
121
  end
134
122
 
135
- # 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?
123
+ # Upload the file directly to 4me local storage
124
+ def upload_to_4me_local(key, attachment)
125
+ uri = storage[:upload_uri]
126
+ response = send_file(uri, storage[:local].merge({ file: attachment }), @client.send(:expand_header))
127
+ raise "4me upload to #{uri} for #{key} failed: #{response.message}" unless response.valid?
140
128
  end
141
129
 
142
130
  def send_file(uri, params, basic_auth_header = {})
143
- params = {:'Content-Type' => MIME::Types.type_for(params[:key])[0] || MIME::Types["application/octet-stream"][0]}.merge(params)
131
+ params = { 'Content-Type': MIME::Types.type_for(params[:key])[0] || MIME::Types['application/octet-stream'][0] }.merge(params)
144
132
  data, header = Sdk4me::Multipart::Post.prepare_query(params)
145
133
  ssl, domain, port, path = @client.send(:ssl_domain_port_path, uri)
146
134
  request = Net::HTTP::Post.new(path, basic_auth_header.merge(header))
147
135
  request.body = data
148
136
  @client.send(:_send, request, domain, port, ssl)
149
137
  end
150
-
151
138
  end
152
139
  end