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.
@@ -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,14 +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
- header['AUTHORIZATION'] = 'Basic ' + ["#{option(:api_token)}:x"].pack('m*').gsub(/\s/, '')
215
- if option(:source)
216
- header['X-4me-Source'] = option(:source)
217
- 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/, '')}"
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 =~ /^\// # make sure path starts with /
230
- 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
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
- when :NilClass then ''
250
- when :String then escape ? uri_escape(value) : value
251
- when :TrueClass then 'true'
252
- when :FalseClass then 'false'
253
- when :DateTime then datetime = value.new_offset(0).iso8601; escape ? uri_escape(datetime) : datetime
254
- when :Date then value.strftime("%Y-%m-%d")
255
- 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')
256
276
  # do not convert arrays in put/post requests as squashing arrays is only used in filtering
257
- 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
258
278
  # TODO: temporary for special constructions to update contact details, see Request #1444166
259
- when :Hash then escape ? value.to_s : value
260
- 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
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
- _response = begin
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{ |_http| _http.request(request) }
275
- rescue ::Exception => e
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
- response = Sdk4me::Response.new(request, _response)
279
- if response.valid?
280
- @logger.debug { "Response:\n#{JSON.pretty_generate(response.json)}" }
281
- elsif response.raw.body =~ /^\s*<\?xml/i
282
- @logger.debug { "XML response:\n#{response.raw.body}" }
283
- elsif '303' == response.raw.code.to_s
284
- @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']}" }
285
305
  else
286
- @logger.error { "Request failed: #{response.message}" }
306
+ @logger.error { "#{request.method} request to #{domain}:#{port}#{request.path} failed: #{resp.message}" }
287
307
  end
288
- response
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
- now = Time.now
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
- # respect the max_retry_time with fallback to max 1 hour and 1 minute wait time
307
- max_retry_time = option(:max_retry_time) > 0 ? option(:max_retry_time) : 3660
308
- begin
309
- _response = super(request, domain, port, ssl)
310
- if _response.throttled?
311
- retry_after = _response.retry_after == 0 ? 300 : [_response.retry_after, 2].max
312
- if (Time.now - now + retry_after) < max_retry_time
313
- @logger.warn { "Request throttled, trying again in #{retry_after} seconds: #{_response.message}" }
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
- end while _response.throttled? && !timed_out
320
- _response
340
+ break unless response.throttled? && !timed_out
341
+ end
342
+ response
321
343
  end
322
344
  end
323
- Client.send(:prepend, SendWithRateLimitBlock)
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) > 0
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 = Time.now
354
+ now = nil
332
355
  timed_out = false
333
- begin
334
- _response = super(request, domain, port, ssl)
335
- # throttling is handled separately
336
- if !_response.success? && !_response.throttled?
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: #{_response.message}" }
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
- end while !_response.success? && !_response.throttled? && !timed_out
346
- _response
369
+ break unless response.failure? && !timed_out
370
+ end
371
+ response
347
372
  end
348
373
  end
349
- Client.send(:prepend, SendWithRetries)
374
+ Client.prepend SendWithRetries
350
375
  end
351
376
 
352
377
  # HTTPS with certificate bundle
353
378
  module Net
354
379
  class HTTP
355
- alias_method :original_use_ssl=, :use_ssl=
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
- 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 in :attachments to 4me and return the data with the uploaded attachment info
12
- def upload_attachments!(path, data)
13
- raise_exceptions = !!data.delete(:attachments_exception)
14
- attachments = [data.delete(:attachments)].flatten.compact
15
- return if attachments.empty?
16
-
17
- # retrieve the upload configuration for this record from 4me
18
- storage = @client.get(path =~ /\d+$/ ? path : "#{path}/new", {attachment_upload_token: true}, @client.send(:expand_header))[:storage_upload]
19
- report_error("Attachments not allowed for #{path}", raise_exceptions) and return unless storage
11
+ # Upload attachments and replace the data inline with the uploaded
12
+ # attachments info.
13
+ #
14
+ # To upload field attachments:
15
+ # * data[:note_attachments] = ['/tmp/test.doc', '/tmp/test.log']
16
+ #
17
+ # To upload inline images:
18
+ # * data[:note] containing text referring to inline images in
19
+ # data[:note_attachments] by their array index, with the index being
20
+ # zero-based. Text can only refer to inline images in its own
21
+ # attachments collection. For example:
22
+ #
23
+ # data = {
24
+ # note: "Hello [note_attachments: 0] and [note_attachments: 1]",
25
+ # note_attachments: ['/tmp/jip.png', '/tmp/janneke.png'],
26
+ # ...
27
+ # }
28
+ #
29
+ # After calling this method the data that will be posted to update the
30
+ # 4me record would look similar to:
31
+ #
32
+ # data = {
33
+ # note: "Hello ![](storage/abc/adjhajdhjaadf.png) and ![](storage/abc/fskdhakjfkjdssdf.png])",
34
+ # note_attachments: [
35
+ # { key: 'storage/abc/fskdhakjfkjdssdf.png', filesize: 12345, inline: true },
36
+ # { key: 'storage/abc/fskdhakjfkjdssdf.png'], filesize: 98765, inline: true }
37
+ # ],
38
+ # ...
39
+ # }
40
+ def upload_attachments!(data)
41
+ # Field attachments
42
+ field_attachments = []
43
+ data.each do |field, value|
44
+ next unless field.to_s.end_with?('_attachments')
45
+ next unless value.is_a?(Enumerable) && value.any?
46
+
47
+ value.map! { |attachment| upload_attachment(attachment) }.compact!
48
+ field_attachments << field if value.any?
49
+ end
20
50
 
21
- # upload each attachment and store the {key, filesize} has in the note_attachments parameter
22
- data[attachments_field(path)] = attachments.map {|attachment| upload_attachment(storage, attachment, raise_exceptions) }.compact.to_json
51
+ # Rich text inline attachments
52
+ field_attachments.each do |field_attachment|
53
+ field = field_attachment.to_s.sub(/_attachments$/, '')
54
+ value = data[field.to_sym] || data[field]
55
+ next unless value.is_a?(String)
56
+
57
+ value.gsub!(/\[#{field_attachment}:\s?(\d+)\]/) do |match|
58
+ idx = Regexp.last_match(1).to_i
59
+ attachment = data[field_attachment][idx]
60
+ if attachment
61
+ attachment[:inline] = true
62
+ "![](#{attachment[:key]})" # magic markdown for inline attachments
63
+ else
64
+ match
65
+ end
66
+ end
67
+ end
23
68
  end
24
69
 
25
70
  private
26
71
 
27
- def attachments_field(path)
28
- case path
29
- when /cis/, /contracts/, /flsas/, /service_instances/, /slas/
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 report_error(message, raise_exceptions)
39
- if raise_exceptions
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
- # upload a single attachment and return the data for the note_attachments
47
- # returns nil and provides an error in case the attachment upload failed
48
- def upload_attachment(storage, attachment, raise_exceptions)
49
- begin
50
- # attachment is already a file or we need to open the file from disk
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
- # there are two different upload methods: AWS S3 and 4me local storage
57
- key_template = "#{storage[:upload_path]}#{FILENAME_TEMPLATE}"
58
- key = key_template.gsub(FILENAME_TEMPLATE, File.basename(attachment.path))
59
- upload_method = storage[:provider] == AWS_PROVIDER ? :aws_upload : :upload_to_4me
60
- send(upload_method, storage, key_template, key, attachment)
61
-
62
- # return the values for the note_attachments param
63
- {key: key, filesize: File.size(attachment.path)}
64
- rescue ::Exception => e
65
- report_error("Attachment upload failed: #{e.message}", raise_exceptions)
66
- nil
87
+ provider = storage[:provider]
88
+ raise 'No provider found' unless provider
89
+
90
+ # attachment is already a file or we need to open the file from disk
91
+ unless attachment.respond_to?(:path) && attachment.respond_to?(:read)
92
+ raise "file does not exist: #{attachment}" unless File.exist?(attachment)
93
+
94
+ attachment = File.open(attachment, 'rb')
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
- def aws_upload(aws, key_template, key, attachment)
71
- # upload the file to AWS
72
- response = send_file(aws[:upload_uri], {
73
- :'x-amz-server-side-encryption' => 'AES256',
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
- # inform 4me of the successful upload
88
- response = @client.get(aws[:success_url].split('/').last, {key: key}, @client.send(:expand_header))
89
- raise "4me confirmation #{aws[:success_url].split('/').last} for #{key} failed: #{response.message}" unless response.valid?
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
- # upload the file directly to 4me
93
- def upload_to_4me(storage, key_template, key, attachment)
94
- uri = storage[:upload_uri] =~ /\/v1/ ? storage[:upload_uri] : storage[:upload_uri].gsub('/attachments', '/v1/attachments')
95
- response = send_file(uri, {file: attachment, key: key_template}, @client.send(:expand_header))
96
- 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?
97
128
  end
98
129
 
99
130
  def send_file(uri, params, basic_auth_header = {})
100
- 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)
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