4me-sdk 1.1.6 → 2.0.0.pre.rc.2

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,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) && option(:max_throttle_time) > 0
322
+ return super(request, domain, port, ssl) unless option(:block_at_rate_limit) && option(:max_throttle_time).positive?
323
+
304
324
  now = nil
305
325
  timed_out = false
306
- begin
307
- _response = super(request, domain, port, ssl)
326
+ response = nil
327
+ loop do
328
+ response = super(request, domain, port, ssl)
308
329
  now ||= Time.now
309
- if _response.throttled?
330
+ if response.throttled?
310
331
  # if no Retry-After is not provided, the 4me server is very busy, wait 5 minutes
311
- 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
312
333
  if (Time.now - now + retry_after) < option(:max_throttle_time)
313
- @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}" }
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
354
  now = nil
332
355
  timed_out = false
333
- begin
334
- _response = super(request, domain, port, ssl)
356
+ response = nil
357
+ loop do
358
+ response = super(request, domain, port, ssl)
335
359
  now ||= Time.now
336
- if _response.failure?
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.failure? && !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,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 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')
67
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}")
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
- })
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
+
82
117
  # 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
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
86
121
 
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?
122
+ xml[%r{<Key>(.*)</Key>}, 1]
90
123
  end
91
124
 
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?
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
+ key
97
132
  end
98
133
 
99
134
  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)
135
+ params = { 'Content-Type': MIME::Types.type_for(params[:key])[0] || MIME::Types['application/octet-stream'][0] }.merge(params)
101
136
  data, header = Sdk4me::Multipart::Post.prepare_query(params)
102
137
  ssl, domain, port, path = @client.send(:ssl_domain_port_path, uri)
103
138
  request = Net::HTTP::Post.new(path, basic_auth_header.merge(header))
104
139
  request.body = data
105
140
  @client.send(:_send, request, domain, port, ssl)
106
141
  end
107
-
108
142
  end
109
143
  end