sp-job 0.2.3 → 0.3.22

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/bin/configure +40 -0
  4. data/lib/sp-job.rb +21 -2
  5. data/lib/sp/job/back_burner.rb +350 -68
  6. data/lib/sp/job/broker.rb +18 -16
  7. data/lib/sp/job/broker_http_client.rb +80 -20
  8. data/lib/sp/job/broker_oauth2_client.rb +12 -4
  9. data/lib/sp/job/common.rb +876 -62
  10. data/lib/sp/job/configure/configure.rb +640 -0
  11. data/lib/sp/job/curl_http_client.rb +100 -0
  12. data/lib/sp/job/easy_http_client.rb +94 -0
  13. data/lib/sp/job/http_client.rb +51 -0
  14. data/lib/sp/job/job_db_adapter.rb +38 -36
  15. data/lib/sp/job/jsonapi_error.rb +31 -74
  16. data/lib/sp/job/jwt.rb +55 -5
  17. data/lib/sp/job/mail_queue.rb +9 -2
  18. data/lib/sp/job/manticore_http_client.rb +94 -0
  19. data/lib/sp/job/pg_connection.rb +90 -10
  20. data/lib/sp/job/query_params.rb +45 -0
  21. data/lib/sp/job/rfc822.rb +13 -0
  22. data/lib/sp/job/session.rb +239 -0
  23. data/lib/sp/job/unique_file.rb +37 -1
  24. data/lib/sp/job/uploaded_image_converter.rb +27 -19
  25. data/lib/sp/job/worker.rb +51 -1
  26. data/lib/sp/job/worker_thread.rb +22 -7
  27. data/lib/sp/jsonapi.rb +24 -0
  28. data/lib/sp/jsonapi/adapters/base.rb +177 -0
  29. data/lib/sp/jsonapi/adapters/db.rb +26 -0
  30. data/lib/sp/jsonapi/adapters/raw_db.rb +96 -0
  31. data/lib/sp/jsonapi/exceptions.rb +54 -0
  32. data/lib/sp/jsonapi/model/base.rb +31 -0
  33. data/lib/sp/jsonapi/model/concerns/attributes.rb +91 -0
  34. data/lib/sp/jsonapi/model/concerns/model.rb +39 -0
  35. data/lib/sp/jsonapi/model/concerns/persistence.rb +212 -0
  36. data/lib/sp/jsonapi/model/concerns/serialization.rb +57 -0
  37. data/lib/sp/jsonapi/parameters.rb +54 -0
  38. data/lib/sp/jsonapi/service.rb +96 -0
  39. data/lib/tasks/configure.rake +2 -496
  40. data/sp-job.gemspec +3 -2
  41. metadata +24 -2
data/lib/sp/job/broker.rb CHANGED
@@ -33,10 +33,10 @@ module SP
33
33
  #
34
34
  # Helper class that defined an 'Broker' error.
35
35
  #
36
- class Error < ::SP::Job::JSONAPIError
36
+ class Error < ::SP::Job::JSONAPI::Error
37
37
 
38
38
  def initialize (i18n:, code:, internal:)
39
- super(code: code, internal: internal)
39
+ super(status: code, code: code, detail: nil, internal: internal)
40
40
  @i18n = i18n
41
41
  end
42
42
 
@@ -150,7 +150,7 @@ module SP
150
150
  # @param scope
151
151
  # @param old
152
152
  #
153
- def refresh (scope: nil, old: nil)
153
+ def refresh(scope: nil, old: nil, delete: true)
154
154
  at_response = @client.refresh_access_token(
155
155
  a_refresh_token = old[:refresh_token],
156
156
  a_scope = scope
@@ -168,12 +168,14 @@ module SP
168
168
  raise InternalError.new(i18n: nil, internal: nil)
169
169
  end
170
170
  # delete old tokens from redis
171
- @redis.multi do |multi|
172
- if nil != old[:access_token]
173
- multi.del("#{@service_id}:oauth:access_token:#{old[:access_token]}")
174
- end
175
- if nil != old[:refresh_token]
176
- multi.del("#{@service_id}:oauth:refresh_token:#{old[:refresh_token]}")
171
+ if true == delete
172
+ @redis.multi do |multi|
173
+ if nil != old[:access_token]
174
+ multi.del("#{@service_id}:oauth:access_token:#{old[:access_token]}")
175
+ end
176
+ if nil != old[:refresh_token]
177
+ multi.del("#{@service_id}:oauth:refresh_token:#{old[:refresh_token]}")
178
+ end
177
179
  end
178
180
  end
179
181
  # return oauth response
@@ -236,10 +238,10 @@ module SP
236
238
  raise InternalError.new(i18n: nil, internal: nil)
237
239
  end
238
240
 
239
- if refresh.nil?
241
+ if refresh.nil?
240
242
  refresh = @redis.hget("#{@service_id}:oauth:access_token:#{access}",'refresh_token')
241
243
  end
242
-
244
+
243
245
  # delete tokens from redis
244
246
  @redis.multi do |multi|
245
247
  multi.del("#{@service_id}:oauth:access_token:#{access}")
@@ -282,7 +284,7 @@ module SP
282
284
  #
283
285
  # Obtain an 'access' and a 'refresh' token.
284
286
  #
285
- # @param args check the authorize method o OAuth2 class
287
+ # @param args check the authorize method o OAuth2 class
286
288
  # @return hash with response content type and status code
287
289
  #
288
290
  def authorize (args)
@@ -295,7 +297,7 @@ module SP
295
297
  #
296
298
  # Refresh an access token.
297
299
  #
298
- # @param args check the refresh method o OAuth2 class
300
+ # @param args check the refresh method o OAuth2 class
299
301
  # @return hash with response content type and status code
300
302
  #
301
303
  def refresh (args)
@@ -308,7 +310,7 @@ module SP
308
310
  #
309
311
  # Patch a pair of tokens, by generating new ones
310
312
  #
311
- # @param args check the patch methods o OAuth2 class
313
+ # @param args check the patch methods o OAuth2 class
312
314
  # @return hash with response content type and status code
313
315
  #
314
316
  def patch (args)
@@ -321,11 +323,11 @@ module SP
321
323
  #
322
324
  # Remove a pair of tokens from redis.
323
325
  #
324
- # @param args check the dispose method o OAuth2 class
326
+ # @param args check the dispose method o OAuth2 class
325
327
  # @return hash with response content type and status code
326
328
  #
327
329
  def dispose (args)
328
- call do
330
+ call do
329
331
  finalized(response: oauth2.dispose(args))
330
332
  end
331
333
  @output
@@ -23,7 +23,6 @@ module SP
23
23
  module Job
24
24
 
25
25
  class BrokerHTTPClient
26
-
27
26
  ### INNER CLASS(ES) ###
28
27
 
29
28
  public
@@ -36,7 +35,7 @@ module SP
36
35
 
37
36
  ### INSTANCE METHOD(S) ###
38
37
 
39
- def initialize (a_curb_request)
38
+ def initialize(a_curb_request)
40
39
  http_response, *http_headers = a_curb_request.header_str.split(/[\r\n]+/).map(&:strip)
41
40
  @code = a_curb_request.response_code
42
41
  @headers = Response.symbolize_keys(Hash[http_headers.flat_map{ |s| s.scan(/^(\S+): (.+)/) }])
@@ -156,6 +155,57 @@ module SP
156
155
  end
157
156
  end
158
157
 
158
+ #
159
+ # Perfom an HTTP POST to obtain a file request and, if required, renew access token.
160
+ #
161
+ # @param url
162
+ # @param content_type
163
+ # @param body
164
+ # @param auto_renew_token
165
+ #
166
+ def get_file(url:, content_type: nil, body: nil, auto_renew_token: true)
167
+ if true == auto_renew_token || nil == @session.access_token
168
+ response = call_and_try_to_recover do
169
+ if nil != body
170
+ do_http_post(url, body, content_type)
171
+ else
172
+ do_http_get(url, nil)
173
+ end
174
+ end
175
+ else
176
+ if nil != body
177
+ response = do_http_post(url, body, content_type)
178
+ else
179
+ response = do_http_get(url, nil)
180
+ end
181
+ end
182
+ if 200 == response.code
183
+ begin
184
+ if block_given?
185
+ yield(response.code, response.headers[:'Content-Type'], response.body)
186
+ else
187
+ return response
188
+ end
189
+ rescue Exception => e
190
+ raise ::SP::Job::BrokerOAuth2Client::InternalError.new(nil)
191
+ end
192
+ else
193
+ if 401 == response.code
194
+ raise ::SP::Job::BrokerOAuth2Client::UnauthorizedUser.new(nil)
195
+ else
196
+ begin
197
+ if block_given?
198
+ yield(response.code, response.headers[:'Content-Type'], response.body)
199
+ else
200
+ return response
201
+ end
202
+ rescue Exception => e
203
+ raise ::SP::Job::BrokerOAuth2Client::InternalError.new(nil)
204
+ end
205
+ end
206
+ end
207
+ end
208
+
159
209
  #
160
210
  # Perfom an HTTP POST request and, if required, renew access token.
161
211
  #
@@ -217,10 +267,13 @@ module SP
217
267
  # @param a_uri
218
268
  # @param a_content_type
219
269
  #
220
- def do_http_get (a_uri, a_content_type = 'application/vnd.api+json')
270
+ def do_http_get(a_uri, a_content_type = 'application/vnd.api+json')
221
271
  http_request = Curl::Easy.http_get(a_uri) do |curl|
222
- curl.headers['Content-Type'] = a_content_type;
272
+ if nil != a_content_type
273
+ curl.headers['Content-Type'] = a_content_type
274
+ end
223
275
  curl.headers['Authorization'] = "Bearer #{@session.access_token}"
276
+ curl.headers['User-Agent'] = "SP-JOB/BrokerHTTPClient"
224
277
  end
225
278
  Response.new(http_request)
226
279
  end
@@ -232,10 +285,11 @@ module SP
232
285
  # @param a_body
233
286
  # @param a_content_type
234
287
  #
235
- def do_http_post (a_uri, a_body, a_content_type = 'application/vnd.api+json')
288
+ def do_http_post(a_uri, a_body, a_content_type = 'application/vnd.api+json')
236
289
  http_request = Curl::Easy.http_post(a_uri, a_body) do |curl|
237
290
  curl.headers['Content-Type'] = a_content_type;
238
291
  curl.headers['Authorization'] = "Bearer #{@session.access_token}"
292
+ curl.headers['User-Agent'] = "SP-JOB/BrokerHTTPClient"
239
293
  end
240
294
  Response.new(http_request)
241
295
  end
@@ -247,10 +301,11 @@ module SP
247
301
  # @param a_body
248
302
  # @param a_content_type
249
303
  #
250
- def do_http_patch (a_uri, a_body, a_content_type = 'application/vnd.api+json')
304
+ def do_http_patch(a_uri, a_body, a_content_type = 'application/vnd.api+json')
251
305
  http_request = Curl.http(:PATCH, a_uri, a_body) do |curl|
252
306
  curl.headers['Content-Type'] = a_content_type;
253
307
  curl.headers['Authorization'] = "Bearer #{@session.access_token}"
308
+ curl.headers['User-Agent'] = "SP-JOB/BrokerHTTPClient"
254
309
  end
255
310
  Response.new(http_request)
256
311
  end
@@ -262,10 +317,11 @@ module SP
262
317
  # @param a_body
263
318
  # @param a_content_type
264
319
  #
265
- def do_http_delete (a_uri, a_body = nil, a_content_type = 'application/vnd.api+json')
320
+ def do_http_delete(a_uri, a_body = nil, a_content_type = 'application/vnd.api+json')
266
321
  http_request = Curl::Easy.http_delete(a_uri) do |curl|
267
322
  curl.headers['Content-Type'] = a_content_type;
268
323
  curl.headers['Authorization'] = "Bearer #{@session.access_token}"
324
+ curl.headers['User-Agent'] = "SP-JOB/BrokerHTTPClient"
269
325
  end
270
326
  Response.new(http_request)
271
327
  end
@@ -285,20 +341,24 @@ module SP
285
341
  response = yield
286
342
  if 401 == response.code && response.headers.has_key?(:'WWW-Authenticate')
287
343
  # try to refresh access_token
288
- tokens_response = @oauth2_client.refresh_access_token(@session.refresh_token, @session.scope)
289
- if 200 == tokens_response[:http][:status_code] && tokens_response[:oauth2] && ! tokens_response[:oauth2][:error]
290
- # success: keep track of new data
291
- @session.is_new = false
292
- @session.access_token = tokens_response[:oauth2][:access_token]
293
- @session.refresh_token = tokens_response[:oauth2][:refresh_token]
294
- @session.scope = tokens_response[:oauth2][:scope] || @session.scope
295
- @session.expires_in = tokens_response[:oauth2][:expires_in] || -1
296
- # notify owner
297
- if nil != @refreshed_callback
298
- @refreshed_callback.call(@session)
299
- end
300
- else
344
+ if true == @auto_renew_refresh_token && nil == @session.refresh_token
301
345
  fetch_new_tokens()
346
+ else
347
+ tokens_response = @oauth2_client.refresh_access_token(@session.refresh_token, @session.scope)
348
+ if 200 == tokens_response[:http][:status_code] && tokens_response[:oauth2] && ! tokens_response[:oauth2][:error]
349
+ # success: keep track of new data
350
+ @session.is_new = false
351
+ @session.access_token = tokens_response[:oauth2][:access_token]
352
+ @session.refresh_token = tokens_response[:oauth2][:refresh_token]
353
+ @session.scope = tokens_response[:oauth2][:scope] || @session.scope
354
+ @session.expires_in = tokens_response[:oauth2][:expires_in] || -1
355
+ # notify owner
356
+ if nil != @refreshed_callback
357
+ @refreshed_callback.call(@session)
358
+ end
359
+ else
360
+ fetch_new_tokens()
361
+ end
302
362
  end
303
363
  # retry http request
304
364
  response = yield
@@ -141,13 +141,12 @@ module SP
141
141
  #
142
142
  # Unauthorized User
143
143
  #
144
- class UnauthorizedUser< Error
144
+ class UnauthorizedUser < Error
145
145
  def initialize(a_description)
146
146
  super "unauthorized_user", a_description
147
147
  end
148
148
  end
149
149
 
150
-
151
150
  #
152
151
  # Internal error.
153
152
  #
@@ -157,6 +156,15 @@ module SP
157
156
  end
158
157
  end
159
158
 
159
+ #
160
+ # Invalid token.
161
+ #
162
+ class InvalidToken < Error
163
+ def initialize(a_description)
164
+ super "invalid_token", a_description
165
+ end
166
+ end
167
+
160
168
  #
161
169
  #
162
170
  #
@@ -198,7 +206,7 @@ module SP
198
206
  handle = Curl::Easy.new(url)
199
207
  headers.each do |key, value|
200
208
  handle.headers[key] = value
201
- end
209
+ end
202
210
 
203
211
  case http_method
204
212
  when :get
@@ -208,7 +216,7 @@ module SP
208
216
  args = []
209
217
  params.each do |key, value|
210
218
  args << Curl::PostField.content(key, value)
211
- end
219
+ end
212
220
  handle.http_post(args)
213
221
  return Response.new(code: handle.response_code.to_s, body: handle.body_str)
214
222
  when :delete
data/lib/sp/job/common.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # coding: utf-8
1
2
  #
2
3
  # Copyright (c) 2011-2016 Cloudware S.A. All rights reserved.
3
4
  #
@@ -26,11 +27,26 @@ module SP
26
27
 
27
28
  ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
28
29
 
30
+ def prepend_platform_configuration (job)
31
+ begin
32
+ if config && config[:brands] && job && job[:x_brand]
33
+ @platform_configuration = config[:brands][job[:x_brand].to_sym][:"platform-configuration"]
34
+ @color_scheme = config[:brands][job[:x_brand].to_sym][:"color-scheme"]
35
+ end
36
+ rescue Exception => e
37
+ raise 'No Platform Configuration'
38
+ end
39
+ end
40
+
41
+ def exclude_member(member_number)
42
+ $excluded_members.include? member_number
43
+ end
44
+
29
45
  def thread_data
30
46
  $thread_data[Thread.current]
31
47
  end
32
48
 
33
- def http (oauth_client_id:, oauth_client_secret:)
49
+ def http (oauth_client_id:, oauth_client_secret:, oauth_client_host: nil, oauth_client_redirect_uri: nil)
34
50
 
35
51
  $http_oauth_clients ||= {}
36
52
  $http_oauth_clients[oauth_client_id] ||= ::SP::Job::BrokerHTTPClient.new(
@@ -41,11 +57,11 @@ module SP
41
57
  ),
42
58
  a_oauth2_client = ::SP::Job::BrokerOAuth2Client.new(
43
59
  protocol: $config[:api][:oauth][:protocol],
44
- host: $config[:api][:oauth][:host],
60
+ host: oauth_client_host || $config[:api][:oauth][:host],
45
61
  port: $config[:api][:oauth][:port],
46
62
  client_id: oauth_client_id,
47
63
  client_secret: oauth_client_secret,
48
- redirect_uri: $config[:api][:oauth][:redirect_uri],
64
+ redirect_uri: oauth_client_redirect_uri || $config[:api][:oauth][:redirect_uri],
49
65
  scope: $config[:api][:oauth][:scope],
50
66
  options: {}
51
67
  ),
@@ -67,8 +83,30 @@ module SP
67
83
  $pg
68
84
  end
69
85
 
86
+ def user_db
87
+ if $user_db.nil?
88
+ $user_db = $cluster_members[config[:cluster][:user_db]].db
89
+ end
90
+ $user_db
91
+ end
92
+
93
+ def main_bo_db
94
+ if $main_bo_db.nil?
95
+ $main_bo_db = $cluster_members[config[:cluster][:main_bo_db]].db
96
+ end
97
+ $main_bo_db
98
+ end
99
+
70
100
  def redis
71
- $redis
101
+ # callback is not optional
102
+ if $redis_mutex.nil?
103
+ yield($redis)
104
+ else
105
+ # ... to enforce safe usage!
106
+ $redis_mutex.synchronize {
107
+ yield($redis)
108
+ }
109
+ end
72
110
  end
73
111
 
74
112
  def config
@@ -87,14 +125,23 @@ module SP
87
125
  thread_data.jsonapi.adapter
88
126
  end
89
127
 
128
+ def has_jsonapi
129
+ return thread_data.jsonapi != nil
130
+ end
131
+
90
132
  #
91
133
  # You should not use this method ... unless ... you REALLY need to overide the JSON:API
92
134
  # parameters defined by the JOB object
93
135
  #
94
136
  def set_jsonapi_parameters (params)
95
- thread_data.jsonapi.set_jsonapi_parameters(SP::Duh::JSONAPI::ParametersNotPicky.new(params))
137
+ if RUBY_ENGINE == 'jruby' # TODO suck in the base class from SP-DUH
138
+ thread_data.jsonapi.set_jsonapi_parameters(SP::JSONAPI::ParametersNotPicky.new(params))
139
+ else
140
+ thread_data.jsonapi.set_jsonapi_parameters(SP::Duh::JSONAPI::ParametersNotPicky.new(params))
141
+ end
96
142
  end
97
143
 
144
+ #
98
145
  # You should not use this method ... unless ... you REALLY need to overide the JSON:API
99
146
  # parameters defined by the JOB object
100
147
  #
@@ -102,34 +149,42 @@ module SP
102
149
  HashWithIndifferentAccess.new(JSON.parse(thread_data.jsonapi.parameters.to_json))
103
150
  end
104
151
 
152
+ #
153
+ # returns the logger object that job code must use for logging
154
+ #
105
155
  def logger
106
156
  Backburner.configuration.logger
107
157
  end
108
158
 
109
- def id_to_path (id)
110
- "%03d/%03d/%03d/%03d" % [
111
- (id % 1000000000000) / 1000000000,
112
- (id % 1000000000) / 1000000 ,
113
- (id % 1000000) / 1000 ,
114
- (id % 1000)
115
- ]
116
- end
117
-
118
- def get_random_folder
119
- ALPHABET[rand(26)] + ALPHABET[rand(26)]
120
- end
121
-
122
- def send_to_upload_server (src_file:, id:, extension:, entity: 'company')
123
- folder = get_random_folder
159
+ #
160
+ # Uploads a local file to the resting location on the upload server via the internal network
161
+ #
162
+ # Note the upload server could be the same machine, in that case we just copy the file. When the
163
+ # server is a remote machine it must grant ssh access to this machine and have the program unique-file
164
+ # in the path of ssh user
165
+ #
166
+ # Also make sure the job using this method has the following configuration parameers
167
+ # 1. config[:scp_config][:local] true if this machine is also the upload server
168
+ # 2. config[:scp_config][:local] name of upload host with ssh access
169
+ # 3. config[:scp_config][:path] base path of for file uploads server on the local or remote machine
170
+ #
171
+ # @param src_file name of local file to upload
172
+ # @param id entity id user_id or company_id
173
+ # @param extension filename extension with the . use '.pdf' not 'pdf'
174
+ # @param entity can be either 'user' or 'company'
175
+ # @param folder two letter subfolder inside entity folder use '00' for temp files
176
+ #
177
+ def send_to_upload_server (src_file:, id:, extension:, entity: 'company', folder: nil)
178
+ folder ||= get_random_folder
124
179
  remote_path = File.join(entity, id_to_path(id.to_i), folder)
125
- if config[:uploads][:local] == true
126
- destination_file = ::SP::Job::Unique::File.create(File.join(config[:uploads][:path], remote_path), extension)
180
+ if config[:scp_config][:local] == true
181
+ destination_file = ::SP::Job::Unique::File.create(File.join(config[:scp_config][:path], remote_path), extension)
127
182
  FileUtils.cp(src_file, destination_file)
128
183
  else
129
- uploads_server = config[:uploads][:server]
130
- destination_file = %x[ssh #{uploads_server} unique-file -p #{File.join(config[:uploads][:path], remote_path)} -e #{extension}].strip
184
+ uploads_server = config[:scp_config][:server]
185
+ destination_file = %x[ssh #{uploads_server} unique-file -p #{File.join(config[:scp_config][:path], remote_path)} -e #{extension[1..-1]}].strip
131
186
  if $?.exitstatus == 0
132
- %x[scp #{src_file} #{uploads_server}:#{remote_file}]
187
+ %x[scp #{src_file} #{uploads_server}:#{destination_file}]
133
188
  raise_error(message: 'i18n_upload_to_server_failed') if $?.exitstatus != 0
134
189
  else
135
190
  raise_error(message: 'i18n_upload_to_server_failed')
@@ -139,6 +194,226 @@ module SP
139
194
  return entity[0] + folder + destination_file[-(6+extension.length)..-1]
140
195
  end
141
196
 
197
+
198
+ #
199
+ # Retrieve a previously uploaded file.
200
+ #
201
+ # @param file
202
+ # @param tmp_dir
203
+ #
204
+ # @return When tmp_dir is set file URI otherwise file body.
205
+ #
206
+ def get_from_upload_server(file:, tmp_dir:, alt_path: nil)
207
+ path = alt_path.nil? ? config[:tmp_file_server][:path] + '/' : alt_path
208
+ response = HttpClient.get_klass.get(url: "#{config[:tmp_file_server][:protocol]}://#{config[:tmp_file_server][:server]}:#{config[:tmp_file_server][:port]}/#{path}#{file}")
209
+ if 200 != response[:code]
210
+ raise "#{response[:code]}"
211
+ end
212
+ if tmp_dir
213
+ uri = Unique::File.create("/tmp/#{(Date.today + 2).to_s}", 'dl')
214
+ File.open(uri, 'wb') {
215
+ |f| f.write(response[:body])
216
+ }
217
+ uri
218
+ else
219
+ response[:body]
220
+ end
221
+ end
222
+
223
+ #
224
+ # Send a file from the webservers to a permanent location in the file server by http
225
+ #
226
+ # @param file_name
227
+ # @param src_file
228
+ # @param content_type
229
+ # @param access
230
+ # @param company_id
231
+ # @param user_id
232
+ #
233
+ def send_to_file_server(file_name: '', src_file:, content_type:, access:, billing_type:, billing_id:, company_id: nil, user_id: nil)
234
+
235
+ raise 'missing argument user_id/company_id' if user_id.nil? && company_id.nil?
236
+
237
+ url = "#{config[:internal_file_server][:protocol]}://#{config[:internal_file_server][:server]}:#{config[:internal_file_server][:port]}/#{config[:internal_file_server][:path]}"
238
+
239
+ headers = {
240
+ 'Content-Type' => content_type.to_s,
241
+ 'X-CASPER-ACCESS' => access.to_s,
242
+ 'X-CASPER-FILENAME' => "#{file_name.force_encoding('ISO-8859-1')}",
243
+ 'X-CASPER-ARCHIVED-BY' => 'sp-job',
244
+ 'X-CASPER-BILLING-TYPE' => billing_type.to_s,
245
+ 'X-CASPER-BILLING-ID' => billing_id.to_s
246
+ }
247
+
248
+ if !company_id.nil? && user_id.nil?
249
+ headers['X-CASPER-ENTITY-ID'] = "#{company_id.to_s}"
250
+ elsif company_id.nil? && !user_id.nil?
251
+ headers['X-CASPER-USER-ID'] = "#{user_id.to_s}"
252
+ else
253
+ headers['X-CASPER-USER-ID'] = "#{user_id.to_s}"
254
+ end
255
+
256
+ file = File.open(src_file, "rb")
257
+ contents = file.read
258
+ file.close
259
+
260
+ response = HttpClient.get_klass.post(
261
+ url: url,
262
+ headers: headers,
263
+ body: contents,
264
+ expect: {
265
+ code: 200,
266
+ content: {
267
+ type: 'application/vnd.api+json;charset=utf-8'
268
+ }
269
+ }
270
+ )
271
+
272
+ if 200 != response[:code]
273
+ raise "#{response[:code]}"
274
+ end
275
+
276
+ JSON.parse(response[:body])
277
+
278
+ end
279
+
280
+ #
281
+ # Move a file from uploads/tmp to a permanent location in the file server
282
+ #
283
+ # @param tmp_file
284
+ # @param final_file
285
+ # @param content_type
286
+ # @param access
287
+ # @param user_id
288
+ # @param company_id
289
+ #
290
+ def move_to_file_server(tmp_file:, final_file: '', content_type:, access:, billing_type:, billing_id:, user_id: nil, company_id: nil)
291
+
292
+ raise 'missing argument user_id/company_id' if user_id.nil? && company_id.nil?
293
+
294
+ url = "#{config[:internal_file_server][:protocol]}://#{config[:internal_file_server][:server]}:#{config[:internal_file_server][:port]}/#{config[:internal_file_server][:path]}"
295
+
296
+ headers = {
297
+ 'Content-Type' => content_type.to_s,
298
+ 'X-CASPER-ACCESS' => access.to_s,
299
+ 'X-CASPER-MOVES-URI' => tmp_file.to_s,
300
+ 'X-CASPER-FILENAME' => "#{final_file.force_encoding('ISO-8859-1')}",
301
+ 'X-CASPER-ARCHIVED-BY' => 'sp-job',
302
+ 'X-CASPER-BILLING-TYPE' => billing_type.to_s,
303
+ 'X-CASPER-BILLING-ID' => billing_id.to_s
304
+ }
305
+
306
+ if !company_id.nil? && user_id.nil?
307
+ headers['X-CASPER-ENTITY-ID'] = "#{company_id.to_s}"
308
+ elsif company_id.nil? && !user_id.nil?
309
+ headers['X-CASPER-USER-ID'] = "#{user_id.to_s}"
310
+ else
311
+ headers['X-CASPER-USER-ID'] = "#{user_id.to_s}"
312
+ end
313
+
314
+ response = HttpClient.get_klass.post(
315
+ url: url,
316
+ headers: headers,
317
+ body: '',
318
+ expect: {
319
+ code: 200,
320
+ content: {
321
+ type: 'application/vnd.api+json;charset=utf-8'
322
+ }
323
+ }
324
+ )
325
+
326
+ if 200 != response[:code]
327
+ raise "#{response[:code]}"
328
+ end
329
+
330
+ JSON.parse(response[:body])
331
+
332
+ end
333
+
334
+ #
335
+ # Delete a file in the file server
336
+ #
337
+ # @param file_identifier
338
+ # @param user_id
339
+ # @param entity_id
340
+ # @param role_mask
341
+ # @param module_mask
342
+ #
343
+ def delete_from_file_server (file_identifier:, user_id:, entity_id:, role_mask:, module_mask:, billing_type:, billing_id:)
344
+
345
+ raise 'missing file_identifier' if file_identifier.nil?
346
+
347
+ url = "#{config[:internal_file_server][:protocol]}://#{config[:internal_file_server][:server]}:#{config[:internal_file_server][:port]}/#{config[:internal_file_server][:path]}/#{file_identifier}"
348
+
349
+ headers = {
350
+ 'X-CASPER-USER-ID' => user_id.to_s,
351
+ 'X-CASPER-ENTITY-ID' => entity_id.to_s,
352
+ 'X-CASPER-ROLE-MASK' => role_mask.to_s,
353
+ 'X-CASPER-MODULE-MASK' => module_mask.to_s,
354
+ 'USER-AGENT' => 'sp-job',
355
+ 'X-CASPER-BILLING-TYPE' => billing_type.to_s,
356
+ 'X-CASPER-BILLING-ID' => billing_id.to_s
357
+ }
358
+
359
+ response = HttpClient.get_klass.delete(
360
+ url: url,
361
+ headers: headers
362
+ )
363
+
364
+ if 204 != response[:code]
365
+ raise "#{response[:code]}"
366
+ end
367
+ end
368
+
369
+ #
370
+ # Submit jwt
371
+ #
372
+ def submit_jwt (url, jwt)
373
+ response = HttpClient.get_klass.post(
374
+ url: url,
375
+ headers: {
376
+ 'Content-Type' => 'application/text'
377
+ },
378
+ body: jwt,
379
+ expect: {
380
+ code: 200,
381
+ content: {
382
+ type: 'application/json'
383
+ }
384
+ }
385
+ )
386
+ response
387
+ end
388
+
389
+ #
390
+ # Create a role mask given an array of roles
391
+ #
392
+ def get_role_mask (role_names)
393
+ roles = db.exec(%Q[
394
+ SELECT name, mask
395
+ FROM public.roles
396
+ ])
397
+ roles = roles.inject({}){|n_hash, db_hash| n_hash.merge({db_hash['name'] => db_hash['mask']}) }
398
+
399
+ res = roles[role_names[0]].to_i
400
+ role_names.shift
401
+ role_names.each do |name|
402
+ res = (res | roles[name].to_i)
403
+ end
404
+
405
+ "0x#{sprintf("%04x", res)}"
406
+ end
407
+
408
+ #
409
+ # Submit job to beanstalk queue
410
+ #
411
+ # Mandatory (symbolized) keys in args:
412
+ #
413
+ # 1. :job arbritary job data, must be a hash but can contatined nested data
414
+ #
415
+ # Optional keys in args:
416
+ #
142
417
  def submit_job (args)
143
418
  if $redis_mutex.nil?
144
419
  rv = _submit_job(args)
@@ -150,6 +425,10 @@ module SP
150
425
  rv
151
426
  end
152
427
 
428
+ def get_next_job_id
429
+ ($redis.incr "#{$config[:service_id]}:jobs:sequential_id").to_s
430
+ end
431
+
153
432
  def _submit_job (args)
154
433
  job = args[:job]
155
434
  tube = args[:tube] || $args[:program_name]
@@ -157,7 +436,7 @@ module SP
157
436
 
158
437
  validity = args[:validity] || 180
159
438
  ttr = args[:ttr] || 60
160
- job[:id] = ($redis.incr "#{$config[:service_id]}:jobs:sequential_id").to_s
439
+ job[:id] ||= get_next_job_id
161
440
  job[:tube] = tube
162
441
  job[:validity] = validity
163
442
  redis_key = "#{$config[:service_id]}:jobs:#{tube}:#{job[:id]}"
@@ -188,30 +467,47 @@ module SP
188
467
  td.job_id = job[:id]
189
468
  td.publish_key = $config[:service_id] + ':' + (job[:tube] || $args[:program_name]) + ':' + job[:id]
190
469
  td.job_key = $config[:service_id] + ':jobs:' + (job[:tube] || $args[:program_name]) + ':' + job[:id]
191
- if $config[:options] && $config[:options][:jsonapi] == true
192
- raise "Job didn't specify the mandatory field prefix!" if job[:prefix].blank?
193
- td.jsonapi.set_url(job[:prefix])
194
- td.set_jsonapi_parameters(SP::Duh::JSONAPI::ParametersNotPicky.new(job))
470
+ td.job_tube = (job[:tube] || $args[:program_name])
471
+ if has_jsonapi
472
+ set_jsonapi_parameters(job)
195
473
  end
196
474
 
197
475
  # Make sure the job is still allowed to run by checking if the key exists in redis
198
- unless $redis.exists(td.job_key )
199
- logger.warn "Job validity has expired: job ignored".yellow
476
+ exists = redis do |r|
477
+ r.exists(td.job_key)
478
+ end
479
+ unless exists
480
+ # Signal job termination
481
+ td.job_id = nil
482
+ logger.warn 'Job validity has expired: job ignored'.yellow
483
+ return false
484
+ end
485
+
486
+ # Make sure the job was not explicity cancelled
487
+ cancelled = redis do |r|
488
+ r.hget(td.job_key, 'cancelled')
489
+ end
490
+ if cancelled == 'true'
491
+ td.job_id = nil
492
+ logger.warn 'Job was explicity cancelled'.yellow
200
493
  return false
201
494
  end
495
+
202
496
  return true
203
497
  end
204
498
 
205
499
  def update_progress (args)
206
500
  td = thread_data
207
- status = args[:status]
208
- progress = args[:progress]
209
- p_index = args[:index] || 0
501
+ status = args[:status]
502
+ progress = args[:progress]
503
+ p_index = args[:index] || 0
504
+ notification_title = args[:title]
505
+ p_options = args[:options]
210
506
 
211
507
  if args.has_key? :message
212
508
  message_args = Hash.new
213
509
  args.each do |key, value|
214
- next if [:step, :progress, :message, :status, :barrier, :index, :response, :action, :content_type, :status_code, :link].include? key
510
+ next if [:step, :progress, :message, :status, :barrier, :index, :response, :action, :content_type, :status_code, :link, :custom].include? key
215
511
  message_args[key] = value
216
512
  end
217
513
  message = [ args[:message], message_args ]
@@ -234,6 +530,8 @@ module SP
234
530
  td.job_status[:status] = status.nil? ? 'in-progress' : status
235
531
  td.job_status[:link] = args[:link] if args[:link]
236
532
  td.job_status[:status_code] = args[:status_code] if args[:status_code]
533
+ td.job_status[:message] = message unless message.nil?
534
+
237
535
  if args.has_key? :response
238
536
  td.job_status[:response] = args[:response]
239
537
  td.job_status[:content_type] = args[:content_type]
@@ -246,8 +544,9 @@ module SP
246
544
  td.job_notification[:message] = message unless message.nil?
247
545
  td.job_notification[:index] = p_index unless p_index.nil?
248
546
  td.job_notification[:status] = status.nil? ? 'in-progress' : status
249
- td.job_notification[:link] = args[:link] if args[:link]
250
- td.job_notification[:status_code] = args[:status_code] if args[:status_code]
547
+ [:status_code, :custom, :link].each do |key|
548
+ td.job_notification[key] = args[key] if args[key]
549
+ end
251
550
  if args.has_key? :response
252
551
  td.job_notification[:response] = args[:response]
253
552
  td.job_notification[:content_type] = args[:content_type]
@@ -256,6 +555,48 @@ module SP
256
555
 
257
556
  if ['completed', 'error', 'follow-up', 'cancelled'].include?(status) || (Time.now.to_f - td.report_time_stamp) > $min_progress || args[:barrier]
258
557
  update_progress_on_redis
558
+ if td.current_job[:notification]
559
+ notification_icon = p_options && p_options[:icon] || td.current_job[:notification_options] && td.current_job[:notification_options][:icon] || "toc-icons:notification_SIS"
560
+ notification_link = p_options && p_options[:link] || td.current_job[:notification_options] && td.current_job[:notification_options][:link] || ""
561
+ notification_remote = p_options && p_options[:remote] || td.current_job[:notification_options] && td.current_job[:notification_options][:remote] || false
562
+ notification_title = notification_title || p_options && p_options[:title] || td.current_job[:notification_options] && td.current_job[:notification_options][:title] || td.current_job[:notification_title] || "Notification title"
563
+
564
+ message = {
565
+ dismiss: ['completed', 'error', 'follow-up', 'cancelled', 'imported'].include?(status),
566
+ can_be_canceled: !['completed', 'error', 'follow-up', 'cancelled', 'imported'].include?(status),
567
+ status: status || td.job_notification[:status],
568
+ icon: notification_icon,
569
+ link: notification_link,
570
+ title: notification_title,
571
+ updated_at: Time.new,
572
+ per_user: false,
573
+ remote: notification_remote,
574
+ tube: td.job_tube,
575
+ id: [td.job_tube, td.job_id].join(":"),
576
+ identity: td.job_id,
577
+ content: p_options && p_options[:message] || td.job_notification[:message]
578
+ }
579
+
580
+ if td.current_job[:notification_options]
581
+ message.merge!({
582
+ wizard: td.current_job[:notification_options][:wizard],
583
+ wizard_options: td.current_job[:notification_options][:wizard_options]
584
+ }) if td.current_job[:notification_options][:wizard] && td.current_job[:notification_options][:wizard_options]
585
+
586
+ message.merge!({
587
+ expiration_date: td.current_job[:notification_options][:expiration_date]
588
+ }) if td.current_job[:notification_options][:expiration_date]
589
+ end
590
+
591
+ notification_options = {
592
+ service: $config[:service_id],
593
+ entity: 'company',
594
+ entity_id: td.current_job[:entity_id],
595
+ action: :update
596
+ }
597
+
598
+ manage_notification(notification_options, message)
599
+ end
259
600
  end
260
601
  end
261
602
 
@@ -266,11 +607,122 @@ module SP
266
607
  args[:content_type] ||= 'application/json'
267
608
  args[:response] ||= {}
268
609
  args[:status_code] ||= 200
269
- update_progress(args)
610
+ # $raw_response cam either be:
611
+ # - a Boolean (true or false)
612
+ # - an Array of tube names; in this case, the response is raw if the current tube name is one of the Array names
613
+ is_raw_response = ($raw_response.is_a?(Array) ? td.job_tube.in?($raw_response) : $raw_response)
614
+ if is_raw_response && $transient_job || args[:force_raw_response]
615
+ response = '*'
616
+ response << args[:status_code].to_s
617
+ response << ','
618
+ response << args[:content_type].bytesize.to_s
619
+ response << ','
620
+ response << args[:content_type]
621
+ response << ','
622
+ if args[:response].instance_of? String
623
+ args[:response] = args[:response].force_encoding('utf-8')
624
+ response << args[:response].bytesize.to_s
625
+ response << ','
626
+ response << args[:response]
627
+ elsif args[:response].instance_of? StringIO
628
+ if 'application/json' == args[:content_type]
629
+ raw = args[:response].string.force_encoding('utf-8')
630
+ else
631
+ raw = args[:response].string
632
+ end
633
+ response << raw.size.to_s
634
+ response << ','
635
+ response << raw
636
+ else
637
+ if args[:response].is_a?(Hash)
638
+ if args[:response].has_key?(:errors) && args[:response][:errors].is_a?(Array)
639
+ args[:response][:errors].each do | e |
640
+ if e.has_key?(:detail)
641
+ e[:detail] = e[:detail].force_encoding('utf-8')
642
+ end
643
+ end
644
+ end
645
+ end
646
+ json = args[:response].to_json
647
+ response << json.bytesize.to_s
648
+ response << ','
649
+ response << json
650
+ end
651
+ if $redis_mutex.nil?
652
+ $redis.publish td.publish_key, response
653
+ else
654
+ $redis_mutex.synchronize {
655
+ $redis.publish td.publish_key, response
656
+ }
657
+ end
658
+ else
659
+ update_progress(args)
660
+ end
661
+ signal_job_termination(td)
270
662
  td.job_id = nil
271
663
  end
272
664
 
273
- def error_handler(args)
665
+ def on_bury_lock_cleanup(*args)
666
+ #logger.error "Caused an exception (#{args.to_s})"
667
+ end
668
+
669
+ def on_failure_lock_cleanup(e, *args)
670
+ #logger.error "Lock cleanup caused an exception (#{e})"
671
+ end
672
+
673
+ def on_failure_for_all_jobs(e, *args)
674
+ begin
675
+ job = thread_data.current_job
676
+
677
+ _message = if (e.is_a?(::SP::Job::JobAborted))
678
+ eval(e.message)[:args][:message]
679
+ else
680
+ self.pg_server_error(e) || _notification
681
+ end
682
+
683
+ report_error(progress: 100, status: 'error', message: _message, detail: "Error in job with params: #{job} -> #{e}" )
684
+ rescue => e
685
+ logger.error "**** FAILURE ALL JOBS **** #{e}"
686
+ end
687
+
688
+ end
689
+
690
+ def after_perform_lock_cleanup (*args)
691
+ check_gracefull_exit(dolog: true)
692
+ end
693
+
694
+ def check_gracefull_exit (dolog: false)
695
+ if $gracefull_exit
696
+ jobs = 0
697
+ $thread_data.each do |thread, thread_data|
698
+ unless thread_data.job_id.nil?
699
+ jobs += 1
700
+ end
701
+ end
702
+ if jobs == 0
703
+ message = 'SIGUSR2 requested no jobs are running exiting now'
704
+ if dolog
705
+ logger.info message
706
+ else
707
+ puts message
708
+ end
709
+ $beaneater.close
710
+ exit 0
711
+ else
712
+ message = "SIGUSR2 requested but #{jobs} jobs are still running"
713
+ if dolog
714
+ logger.info message
715
+ else
716
+ puts message
717
+ end
718
+ end
719
+ end
720
+ end
721
+
722
+ def error_handler (args)
723
+ if $config[:options][:source] == "broker"
724
+ raise "Implementation error : please use 'raise' instead of report_error or raise_error"
725
+ end
274
726
  td = thread_data
275
727
  args[:status] ||= 'error'
276
728
  args[:action] ||= 'response'
@@ -279,29 +731,31 @@ module SP
279
731
  update_progress(args)
280
732
  logger.error(args)
281
733
  td.exception_reported = true
734
+ signal_job_termination(td)
282
735
  td.job_id = nil
283
736
  end
284
737
 
738
+ #
739
+ # @NOTE: do not use this method if $config[:options][:source] == "broker"
740
+ #
285
741
  def report_error (args)
286
742
  td = thread_data
287
743
  error_handler(args)
288
744
  raise ::SP::Job::JobAborted.new(args: args, job: td.current_job)
289
745
  end
290
746
 
747
+ #
748
+ # @NOTE: do not use this method if $config[:options][:source] == "broker"
749
+ #
291
750
  def raise_error (args)
292
751
  td = thread_data
752
+ if ! args.is_a? Hash
753
+ raise "'args' must be an Hash!"
754
+ end
293
755
  error_handler(args)
294
756
  raise ::SP::Job::JobException.new(args: args, job: td.current_job)
295
757
  end
296
758
 
297
- def send_text_response (response)
298
- td = thread_data
299
- $redis.pipelined do
300
- $redis.publish td.publish_key, response
301
- $redis.hset td.job_key, 'status', response
302
- end
303
- end
304
-
305
759
  def update_progress_on_redis
306
760
  td = thread_data
307
761
  if $redis_mutex.nil?
@@ -328,6 +782,12 @@ module SP
328
782
  td.report_time_stamp = Time.now.to_f
329
783
  end
330
784
 
785
+ def signal_job_termination (td)
786
+ redis do |r|
787
+ r.publish $config[:service_id] + ':job-signal', { channel: td.publish_key, id: td.job_id.to_i, status: 'finished' }.to_json
788
+ end
789
+ end
790
+
331
791
  def expand_mail_body (template)
332
792
  if template.class == Hash
333
793
  template_path = template[:path]
@@ -350,6 +810,148 @@ module SP
350
810
  ERB.new(erb_template).result(erb_binding)
351
811
  end
352
812
 
813
+ def manage_notification_worker(options = {}, notification = {})
814
+
815
+ if options[:redis]
816
+ redis_client_master = options[:redis]
817
+ end
818
+
819
+ raise 'Can do anything without redis connection on redis_client_master' unless redis_client_master
820
+
821
+ queues = redis_client_master.smembers("resque:queues")
822
+ queue_type = notification[:type]
823
+ c_id = notification[:company_id]
824
+ n_id = notification[:resource_id]
825
+
826
+ queue_len = redis_client_master.llen("resque:queue:#{queue_type}")
827
+ redis_client_master.lrange("resque:queue:#{queue_type}", 0, queue_len)
828
+ match_string = "[#{c_id},#{n_id}]"
829
+ rem_command = redis_client_master.lrange("resque:queue:#{queue_type}", 0, queue_len).find do |queue_find|
830
+ queue_find.include?(match_string)
831
+ end
832
+
833
+ # ap ["notification", notification, "queues", queues, queue_type, c_id, n_id, "rem_command", rem_command]
834
+
835
+ if rem_command
836
+ puts "queue_type => #{queue_type} :::: rem_command => #{rem_command}".red
837
+ redis_client_master.lrem("resque:queue:#{queue_type}", "-2", rem_command)
838
+ end
839
+
840
+ end
841
+
842
+
843
+ def manage_notification(options = {}, notification = {})
844
+
845
+ options = {
846
+ service: "toconline",
847
+ type: 'notifications',
848
+ action: :new
849
+ }.merge(options)
850
+
851
+ if options[:redis]
852
+ redis_client = options[:redis]
853
+ else
854
+ redis_client = $redis
855
+ end
856
+
857
+ redis_key = {
858
+ key: [options[:service], options[:type], options[:entity], options[:entity_id]].join(":"),
859
+ public_key: [options[:service], options[:entity], options[:entity_id]].join(":")
860
+ }
861
+
862
+ if options[:action] == :new
863
+
864
+ response_object = notification
865
+
866
+ job_type = notification[:tube] && notification[:tube].gsub("-hd", "") #remove the -hd pattern to merge on the original tube ex: saft-importer-hd -> saft-importer
867
+
868
+ job_exists = redis_client.sscan(redis_key[:key], 0, { match: "*\"tube\":\"#{job_type}*\"*" }) if job_type
869
+
870
+ unless job_exists
871
+ job_exists = redis_client.sscan(redis_key[:key], 0, { match: "*\"icon\":\"#{notification[:icon]}\"*\"resource_job_queue_name\":\"#{notification[:resource_job_queue_name]}*" })
872
+ end
873
+
874
+ if job_exists[1] && job_exists[1].any?
875
+ job_exists[1].map do |key|
876
+ redis_client.srem redis_key[:key], "#{key}"
877
+ temp_response_object = { id: JSON.parse(key)["id"], destroy: true }
878
+ redis_client.publish redis_key[:public_key], "#{temp_response_object.to_json}"
879
+ end
880
+ # ap ["theres a similar job notification => REDIS PUBLISH DESTROY", redis_key[:public_key], temp_response_object.to_json]
881
+
882
+ # ap ["create the new one after destroy similar"]
883
+ redis_client.sadd redis_key[:key], "#{notification.to_json}"
884
+ redis_client.publish redis_key[:public_key], "#{response_object.to_json}"
885
+ else
886
+ redis_client.sadd redis_key[:key], "#{notification.to_json}"
887
+ redis_client.publish redis_key[:public_key], "#{response_object.to_json}"
888
+ end
889
+
890
+ elsif options[:action] == :update
891
+
892
+ match_member = redis_client.sscan(redis_key[:key], 0, { match: "*#{notification[:id]}\"*" })
893
+
894
+ if match_member && match_member[1][0]
895
+
896
+ notification.merge!({id: notification[:id]}) if notification[:id]
897
+ response_object = notification
898
+
899
+ redis_client.srem redis_key[:key], "#{match_member[1][0]}"
900
+ notification.delete(:identity)
901
+ redis_client.sadd redis_key[:key], "#{notification.to_json}"
902
+
903
+ redis_client.publish redis_key[:public_key], "#{response_object.to_json}"
904
+ # ap ["REDIS PUBLISH UPDATE", redis_key[:public_key], response_object.to_json]
905
+
906
+ else
907
+ # puts 'nothing to update [[better insert]]'
908
+ manage_notification(
909
+ options.merge({action: :new}),
910
+ notification
911
+ )
912
+ end
913
+ else
914
+
915
+ match_member = redis_client.sscan(redis_key[:key], 0, { match: "*#{notification[:identity]}\"*" })
916
+ if match_member && match_member[1].any?
917
+ response_object = { id: notification[:identity], destroy: true } if notification[:identity]
918
+ redis_client.srem redis_key[:key], "#{match_member[1][0]}"
919
+ redis_client.publish redis_key[:public_key], "#{response_object.to_json}"
920
+ else
921
+ puts 'nothing to destroy'
922
+ end
923
+
924
+ end
925
+
926
+ end
927
+
928
+ def email_addresses_valid? (email_addresses)
929
+ begin
930
+ raise ::ArgumentError.new 'A lista de emails tem de estar preenchida' if email_addresses.nil? || email_addresses.empty?
931
+
932
+ email_addresses = email_addresses.split(',').map(&:strip)
933
+ valid_email_addresses = email_addresses.select { |email| !email.match(RFC822::EMAIL).nil? }
934
+ diff_email_addresses = email_addresses - valid_email_addresses
935
+
936
+ raise ::Exception.new "Os seguintes endereços de email não são válidos: #{diff_email_addresses.join(', ')}" if diff_email_addresses.length > 0
937
+
938
+ return { valid: true }
939
+ rescue ::Exception => e
940
+ return {
941
+ valid: false,
942
+ error: {
943
+ type: e.class,
944
+ message: e.message
945
+ },
946
+ invalid_emails: diff_email_addresses
947
+ }
948
+ end
949
+ end
950
+
951
+ def sanitize_email_addresses! (email_addresses)
952
+ email_addresses.split(',').map { |email| email.strip.downcase }.join(', ')
953
+ end
954
+
353
955
  def send_email (args)
354
956
 
355
957
  if args.has_key?(:body) && args[:body] != nil
@@ -358,13 +960,19 @@ module SP
358
960
  email_body = expand_mail_body args[:template]
359
961
  end
360
962
 
963
+ ___internal=nil
964
+ if args.has_key?(:___internal) && nil != args[:___internal]
965
+ ___internal = args[:___internal]
966
+ end
967
+
361
968
  submit_job(
362
- tube: 'mail-queue',
969
+ tube: args[:'mail-queue-tube'] || 'mail-queue',
363
970
  job: {
364
971
  to: args[:to],
365
972
  subject: args[:subject],
366
973
  reply_to: args[:reply_to],
367
- body: email_body
974
+ body: email_body,
975
+ ___internal: ___internal
368
976
  }
369
977
  )
370
978
  end
@@ -380,9 +988,38 @@ module SP
380
988
  document = Roadie::Document.new email_body
381
989
  email_body = document.transform
382
990
 
991
+ to_email = config[:override_mail_recipient] if config[:override_mail_recipient]
992
+ to_email ||= args[:to]
993
+
994
+ response_errors = {}
995
+
996
+ to_emails_validation = email_addresses_valid?(to_email)
997
+ unless to_emails_validation[:valid]
998
+ response_errors[:mailto] = to_emails_validation[:error][:message]
999
+ response_errors[:mailto_invalid] = to_emails_validation[:invalid_emails]
1000
+ end
1001
+
1002
+ # Do not send email to Cc if there is an override_mail_recipient
1003
+ cc_email = nil
1004
+ cc_email ||= args[:cc] if !args[:cc].nil? && config[:override_mail_recipient].nil?
1005
+
1006
+ if !cc_email.nil?
1007
+ cc_emails_validation = email_addresses_valid?(cc_email)
1008
+ unless cc_emails_validation[:valid]
1009
+ response_errors[:mailcc] = cc_emails_validation[:error][:message]
1010
+ response_errors[:mailcc_invalid] = cc_emails_validation[:invalid_emails]
1011
+ end
1012
+ end
1013
+
1014
+ if response_errors.length != 0
1015
+ report_error(message: 'invalidEmails', status_code: 400, response: response_errors)
1016
+ return
1017
+ end
1018
+
383
1019
  m = Mail.new do
384
1020
  from $config[:mail][:from]
385
- to args[:to]
1021
+ to to_email
1022
+ cc cc_email unless cc_email.nil?
386
1023
  subject args[:subject]
387
1024
  reply_to (args[:reply_to] || $config[:mail][:from])
388
1025
 
@@ -394,15 +1031,34 @@ module SP
394
1031
 
395
1032
  if args.has_key?(:attachments) && args[:attachments] != nil
396
1033
  args[:attachments].each do |attach|
397
- attach_uri = URI.escape("#{attach[:protocol]}://#{attach[:host]}:#{attach[:port]}/#{attach[:path]}/#{attach[:file]}")
398
- attach_http_call = Curl::Easy.http_get(attach_uri)
1034
+ uri = "#{attach[:protocol]}://#{attach[:host]}:#{attach[:port]}/#{attach[:path]}"
1035
+ if false == args[:session].nil? && false == args[:session][:role_mask].nil?
1036
+ uri += "/#{attach[:id]}"
1037
+ attach_http_call = Curl::Easy.http_get(URI.escape(uri)) do |http|
1038
+ http.headers['X-CASPER-USER-ID'] = args[:session][:user_id]
1039
+ http.headers['X-CASPER-ENTITY-ID'] = args[:session][:entity_id]
1040
+ http.headers['X-CASPER-ROLE-MASK'] = args[:session][:role_mask]
1041
+ http.headers['X-CASPER-MODULE-MASK'] = args[:session][:module_mask]
1042
+ http.headers['User-Agent'] = "curb/mail-queue"
1043
+ end
1044
+ else
1045
+ uri += "/#{attach[:file]}" if attach.has_key?(:file) && !attach[:file].nil?
1046
+ attach_http_call = Curl::Easy.http_get(URI.escape(uri))
1047
+ end
399
1048
  if attach_http_call.response_code == 200
400
1049
  attributes = {}
401
- attach_http_call.header_str.scan(/(\w+)="([^"]*)"/).each do |group|
402
- attributes[group[0].to_sym] = group[1]
1050
+ if attach.has_key?(:filename)
1051
+ attributes[:filename] = attach[:filename]
1052
+ attributes[:mime_type] = attach[:mime_type]
1053
+ else
1054
+ attach_http_call.header_str.scan(/(\w+)="([^"]*)"/).each do |group|
1055
+ attributes[group[0].to_sym] = group[1]
1056
+ end
1057
+ attributes[:mime_type] = attach_http_call.content_type
403
1058
  end
404
-
405
- m.attachments[attributes[:filename].force_encoding('UTF-8')] = { mime_type: attach_http_call.content_type, content: attach_http_call.body_str }
1059
+ m.attachments[attributes[:filename].force_encoding('UTF-8').gsub('±', ' ')] = { mime_type: attributes[:mime_type], content: attach_http_call.body_str }
1060
+ else
1061
+ raise "synchronous_send_email: #{attach_http_call.response_code} - #{uri}"
406
1062
  end
407
1063
  end
408
1064
  end
@@ -410,17 +1066,175 @@ module SP
410
1066
  m.deliver!
411
1067
  end
412
1068
 
413
- def pg_server_error(e)
1069
+ def pg_server_error (e)
414
1070
  raise e if e.is_a?(::SP::Job::JobCancelled)
415
1071
  base_exception = e
416
- begin
1072
+ while base_exception.respond_to?(:cause) && !base_exception.cause.blank?
417
1073
  base_exception = base_exception.cause
418
- end while base_exception.respond_to?(:cause) && !base_exception.cause.blank?
1074
+ end
1075
+
1076
+ return base_exception.is_a?(PG::ServerError) ? base_exception.result.error_field(PG::PG_DIAG_MESSAGE_PRIMARY) : e.message
1077
+ end
1078
+
1079
+ def file_identifier_to_url(id, filename)
1080
+ url = ''
1081
+ if filename[0] == 'c'
1082
+ url += "company"
1083
+ elsif filename[0] == 'u'
1084
+ url += "user"
1085
+ else
1086
+ raise 'Unrecognizible file type'
1087
+ end
1088
+
1089
+ url += '/'
1090
+ url += id_to_path(id)
1091
+ url += '/'
1092
+ url += filename[1..2]
1093
+ url += '/'
1094
+ url += filename[3..-1]
1095
+ url
1096
+ end
1097
+
1098
+ #
1099
+ # Converts and id to a four level folder hierarchy
1100
+ #
1101
+ # @param id entity id must be an integer
1102
+ # @return the path
1103
+ #
1104
+ def id_to_path (id)
1105
+ "%03d/%03d/%03d/%03d" % [
1106
+ (id % 1000000000000) / 1000000000,
1107
+ (id % 1000000000) / 1000000 ,
1108
+ (id % 1000000) / 1000 ,
1109
+ (id % 1000)
1110
+ ]
1111
+ end
1112
+
1113
+ def get_percentage (total: 1, count: 0) ; (total > 0 ? (count * 100 / total) : count).to_i ; end
419
1114
 
420
- return base_exception.is_a?(PG::ServerError) ? e.cause.result.error_field(PG::PG_DIAG_MESSAGE_PRIMARY) : e.message
1115
+ def on_retry_job (count, delay, jobs)
1116
+ td = thread_data
1117
+ new_delay = jobs[:validity].to_i + (delay.to_i * count)
1118
+ $redis.expire(td.job_key, new_delay)
421
1119
  end
422
1120
 
423
- def get_percentage(total: 1, count: 0) ; (count * 100 / total).to_i ; end
1121
+ def print_and_archive (payload, entity_id)
1122
+ payload[:ttr] ||= 300
1123
+ payload[:validity] ||= 500
1124
+ payload[:auto_printable] ||= false
1125
+ payload[:documents] ||= []
1126
+
1127
+ jwt = JWTHelper.jobify(
1128
+ key: config[:nginx_broker_private_key],
1129
+ tube: 'casper-print-queue',
1130
+ payload: payload
1131
+ )
1132
+
1133
+ pdf_response = HttpClient.get_klass.post(
1134
+ url: get_cdn_public_url,
1135
+ headers: {
1136
+ 'Content-Type' => 'application/text'
1137
+ },
1138
+ body: jwt,
1139
+ expect: {
1140
+ code: 200,
1141
+ content: {
1142
+ type: 'application/pdf'
1143
+ }
1144
+ },
1145
+ conn_options: {
1146
+ connection_timeout: payload[:ttr],
1147
+ request_timeout: payload[:ttr]
1148
+ }
1149
+ )
1150
+
1151
+ tmp_file = Unique::File.create("/tmp/#{(Date.today + 2).to_s}", ".pdf")
1152
+ File.open(tmp_file, 'wb') { |f| f.write(pdf_response[:body]) }
1153
+ file_identifier = send_to_upload_server(src_file: tmp_file, id: entity_id, extension: ".pdf")
1154
+ file_identifier
1155
+ end
1156
+
1157
+ def print_and_archive_http (payload:, entity_id:, access:, file_name: '', billing_type:)
1158
+ payload[:ttr] ||= 300
1159
+ payload[:validity] ||= 500
1160
+ payload[:auto_printable] ||= false
1161
+ payload[:documents] ||= []
1162
+
1163
+ jwt = JWTHelper.jobify(
1164
+ key: config[:nginx_broker_private_key],
1165
+ tube: 'casper-print-queue',
1166
+ payload: payload
1167
+ )
1168
+
1169
+ pdf_response = HttpClient.get_klass.post(
1170
+ url: get_cdn_public_url,
1171
+ headers: {
1172
+ 'Content-Type' => 'application/text'
1173
+ },
1174
+ body: jwt,
1175
+ expect: {
1176
+ code: 200,
1177
+ content: {
1178
+ type: 'application/pdf'
1179
+ }
1180
+ },
1181
+ conn_options: {
1182
+ connection_timeout: payload[:ttr],
1183
+ request_timeout: payload[:ttr]
1184
+ }
1185
+ )
1186
+
1187
+ tmp_file = Unique::File.create("/tmp/#{(Date.today + 2).to_s}", ".pdf")
1188
+ File.open(tmp_file, 'wb') { |f| f.write(pdf_response[:body]) }
1189
+
1190
+ response = send_to_file_server(file_name: file_name,
1191
+ src_file: tmp_file,
1192
+ content_type: 'application/pdf',
1193
+ access: access,
1194
+ billing_type: billing_type,
1195
+ billing_id: entity_id,
1196
+ company_id: entity_id)
1197
+ response
1198
+ end
1199
+
1200
+ class Exception < StandardError
1201
+
1202
+ private
1203
+
1204
+ @status_code = nil
1205
+ @content_type = nil
1206
+ @body = nil
1207
+
1208
+ public
1209
+ attr_accessor :status_code
1210
+ attr_accessor :content_type
1211
+ attr_accessor :body
1212
+
1213
+ public
1214
+ def initialize(status_code:, content_type:, body:)
1215
+ @status_code = status_code
1216
+ @content_type = content_type
1217
+ @body = body
1218
+ end
1219
+
1220
+ end # class Error
1221
+
1222
+ private
1223
+
1224
+ def get_random_folder
1225
+ ALPHABET[rand(26)] + ALPHABET[rand(26)]
1226
+ end
1227
+
1228
+ def get_cdn_public_url
1229
+ cdn_public_url = "#{config[:broker][:cdn][:protocol]}://#{config[:broker][:cdn][:host]}"
1230
+ if config[:broker][:cdn][:port] && 80 != config[:broker][:cdn][:port]
1231
+ cdn_public_url += ":#{config[:broker][:cdn][:port]}"
1232
+ end
1233
+ if config[:broker][:cdn][:path]
1234
+ cdn_public_url += "/#{config[:broker][:cdn][:path]}"
1235
+ end
1236
+ cdn_public_url
1237
+ end
424
1238
 
425
1239
  end # Module Common
426
1240
  end # Module Job