rest-ftp-daemon 0.72b → 0.85.2

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.
@@ -3,31 +3,37 @@
3
3
  require 'uri'
4
4
  require 'net/ftp'
5
5
  require 'double_bag_ftps'
6
+ require 'timeout'
6
7
 
7
8
  module RestFtpDaemon
8
9
  class Job < RestFtpDaemon::Common
10
+ attr_accessor :wid
9
11
 
10
12
  def initialize(id, params={})
11
13
  # Call super
12
- super()
14
+ # super()
15
+ info "Job.initialize"
13
16
 
14
- # Grab params
15
- @params = params
17
+ # Generate new Job.id
18
+ # $queue.counter_add :transferred, source_size
19
+
20
+ # Logger
21
+ @logger = RestFtpDaemon::Logger.new(:workers, "JOB #{id}")
22
+
23
+ # Protect with a mutex
24
+ @mutex = Mutex.new
16
25
 
17
26
  # Init context
27
+ @params = params
18
28
  set :id, id
19
29
  set :started_at, Time.now
20
- set :status, :created
30
+ status :created
21
31
 
22
32
  # Send first notification
33
+ info "Job.initialize/notify"
23
34
  notify "rftpd.queued"
24
35
  end
25
36
 
26
- def progname
27
- job_id = get(:id)
28
- "JOB #{job_id}"
29
- end
30
-
31
37
  def id
32
38
  get :id
33
39
  end
@@ -40,60 +46,99 @@ module RestFtpDaemon
40
46
  end
41
47
 
42
48
  def process
43
- # Init
44
- info "Job.process starting"
45
- set :status, :starting
46
- set :error, 0
49
+ # Update job's status
50
+ set :error, nil
47
51
 
52
+ # Prepare job
48
53
  begin
49
- # Validate job and params
54
+ info "Job.process/prepare"
55
+ status :preparing
50
56
  prepare
51
57
 
52
- # Process
58
+ rescue RestFtpDaemon::JobMissingAttribute => exception
59
+ return oops "rftpd.started", exception, :job_missing_attribute
60
+
61
+ rescue RestFtpDaemon::JobSourceNotFound => exception
62
+ return oops "rftpd.started", exception, :job_source_not_found
63
+
64
+ rescue RestFtpDaemon::RestFtpDaemonException => exception
65
+ return oops "rftpd.started", exception, :job_prepare_failed
66
+
67
+ rescue Exception => exception
68
+ return oops "rftpd.started", exception, :job_prepare_unhandled, true
69
+
70
+ rescue exception
71
+ return oops "rftpd.started", exception, :WOUHOU, true
72
+
73
+ else
74
+ # Update job's status
75
+ info "Job.process/prepare ok"
76
+ status :prepared
77
+ info "Job.process/prepare status updated"
78
+
79
+ # Notify rftpd.start
80
+ info "Job.process/prepare notify started"
81
+ notify "rftpd.started", 0
82
+ info "Job.process/prepare notified started"
83
+ end
84
+
85
+ info "Job.process prepare>transfer"
86
+
87
+ # Process job
88
+ begin
89
+ info "Job.process/transfer"
90
+ status :starting
53
91
  transfer
54
92
 
93
+ rescue Timeout::Error => exception
94
+ return oops "rftpd.ended", exception, :job_timeout_error
95
+
55
96
  rescue Net::FTPPermError => exception
56
- info "Job.process failed [Net::FTPPermError]"
57
- set :status, :failed
58
- set :error, exception.class
97
+ return oops "rftpd.ended", exception, :job_ftp_perm_error
98
+
99
+ rescue Errno::ECONNREFUSED => exception
100
+ return oops "rftpd.ended", exception, :job_connexion_refused
101
+
102
+ rescue Errno::EMFILE => exception
103
+ return oops "rftpd.ended", exception, :job_too_many_open_files
104
+
105
+ rescue RestFtpDaemon::JobTargetFileExists => exception
106
+ return oops "rftpd.ended", exception, :job_target_file_exists
59
107
 
60
- rescue RestFtpDaemonException => exception
61
- info "Job.process failed [::#{exception.class}]"
62
- set :status, :failed
63
- set :error, exception.class
108
+ rescue RestFtpDaemon::RestFtpDaemonException => exception
109
+ return oops "rftpd.ended", exception, :job_transfer_failed
64
110
 
65
111
  rescue Exception => exception
66
- info "Job.process exception [#{exception.class}] #{exception.backtrace.inspect}"
67
- set :status, :crashed
68
- set :error, exception.class
112
+ return oops "rftpd.ended", exception, :job_transfer_unhandled, true
69
113
 
70
114
  else
115
+ # Update job's status
71
116
  info "Job.process finished"
72
- set :status, :finished
117
+ status :finished
118
+
119
+ # Notify rftpd.ended
120
+ notify "rftpd.ended", 0
73
121
  end
74
122
 
75
123
  end
76
124
 
77
125
  def describe
78
- # Update realtime info
79
- #w = wandering_time
80
- #set :wandering, w.round(2) unless w.nil?
81
-
82
126
  # Update realtime info
83
127
  u = up_time
84
128
  set :uptime, u.round(2) unless u.nil?
85
129
 
86
- # Return the whole structure
130
+ # Return the whole structure FIXME
87
131
  @params
88
- end
89
-
90
- def status text
91
- @status = text
132
+ # @mutex.synchronize do
133
+ # out = @params.clone
134
+ # end
92
135
  end
93
136
 
94
137
  def get attribute
95
- return nil unless @params.is_a? Enumerable
96
- @params[attribute.to_s]
138
+ @mutex.synchronize do
139
+ @params || {}
140
+ @params[attribute]
141
+ end
97
142
  end
98
143
 
99
144
  protected
@@ -116,15 +161,17 @@ module RestFtpDaemon
116
161
  @wander_for.to_f - (Time.now - @wander_started)
117
162
  end
118
163
 
119
- # def exception_handler(actor, reason)
120
- # set :status, :crashed
121
- # set :error, reason
122
- # end
123
-
124
164
  def set attribute, value
125
- return unless @params.is_a? Enumerable
126
- @params[:updated_at] = Time.now
127
- @params[attribute.to_s] = value
165
+ @mutex.synchronize do
166
+ @params || {}
167
+ # return unless @params.is_a? Enumerable
168
+ @params[:updated_at] = Time.now
169
+ @params[attribute] = value
170
+ end
171
+ end
172
+
173
+ def status status
174
+ set :status, status
128
175
  end
129
176
 
130
177
  def expand_path path
@@ -138,169 +185,246 @@ module RestFtpDaemon
138
185
  def replace_token path
139
186
  # Ensure endpoints are not a nil value
140
187
  return path unless Settings.endpoints.is_a? Enumerable
141
- newpath = path.clone
188
+ vectors = Settings.endpoints.clone
189
+
190
+ # Stack RANDOM into tokens
191
+ vectors['RANDOM'] = SecureRandom.hex(IDENT_RANDOM_LEN)
142
192
 
143
193
  # Replace endpoints defined in config
144
- Settings.endpoints.each do |from, to|
145
- newpath.gsub! "[#{from}]", to
194
+ newpath = path.clone
195
+ vectors.each do |from, to|
196
+ next if to.to_s.blank?
197
+ #info "Job.replace_token #{Helpers.tokenize(from)} > #{to}"
198
+ newpath.gsub! Helpers.tokenize(from), to
146
199
  end
147
200
 
148
- # Replace with the special RAND token
149
- newpath.gsub! "[RANDOM]", SecureRandom.hex(8)
150
-
151
201
  return newpath
152
202
  end
153
203
 
154
204
  def prepare
155
205
  # Init
156
- info "Job.prepare"
157
- set :status, :preparing
206
+ status :preparing
158
207
  @source_method = :file
159
208
  @target_method = nil
160
209
  @source_path = nil
161
210
  @target_url = nil
162
211
 
163
212
  # Check source
164
- raise JobSourceMissing unless @params["source"]
165
- #@source = expand_path @params["source"]
166
- @source_path = expand_path @params["source"]
213
+ raise RestFtpDaemon::JobMissingAttribute unless @params[:source]
214
+ @source_path = expand_path @params[:source]
167
215
  set :source_path, @source_path
168
216
  set :source_method, :file
169
217
 
170
218
  # Check target
171
- raise JobTargetMissing unless @params["target"]
172
- @target_url = expand_url @params["target"]
173
- set :target_url, @target_url.inspect
219
+ raise RestFtpDaemon::JobMissingAttribute unless @params[:target]
220
+ @target_url = expand_url @params[:target]
221
+ set :target_url, @target_url.to_s
174
222
 
175
223
  if @target_url.kind_of? URI::FTP
176
224
  @target_method = :ftp
225
+ elsif @target_url.kind_of? URI::FTPES
226
+ @target_method = :ftps
177
227
  elsif @target_url.kind_of? URI::FTPS
178
228
  @target_method = :ftps
179
229
  end
180
230
  set :target_method, @target_method
181
231
 
182
232
  # Check compliance
183
- raise JobTargetUnparseable if @target_url.nil?
184
- raise JobTargetUnsupported if @target_method.nil?
185
- raise JobSourceNotFound unless File.exists? @source_path
186
-
233
+ raise RestFtpDaemon::JobTargetUnparseable if @target_url.nil?
234
+ raise RestFtpDaemon::JobTargetUnsupported if @target_method.nil?
235
+ raise RestFtpDaemon::JobSourceNotFound unless File.exists? @source_path
187
236
  end
188
237
 
189
- def transfer_fake
238
+ def transfer
239
+ # Method assertions
240
+ info "Job.transfer checking_source"
241
+ status :checking_source
242
+ raise RestFtpDaemon::JobAssertionFailed unless @source_path && @target_url
243
+
190
244
  # Init
191
- set :status, :faking
192
245
 
193
- # Work
194
- (0..9).each do |i|
195
- set :faking, i
196
- sleep 0.5
246
+ target_name = File.basename @target_url.path
247
+
248
+ # Scheme-aware config
249
+ ftp_init
250
+
251
+ # Connect remote server, login and chdir
252
+ ftp_connect
253
+
254
+ # Check for target file presence
255
+ if get(:overwrite).nil? && (ftp_presence target_name)
256
+ @ftp.close
257
+ raise RestFtpDaemon::JobTargetFileExists
197
258
  end
198
- end
199
259
 
200
- def transfer
201
- # Init
202
- info "Job.transfer"
260
+ # Do transfer
261
+ ftp_transfer target_name
203
262
 
204
- # Send first notification
205
- transferred = 0
206
- notify "rftpd.started"
263
+ # Close FTP connexion
264
+ info "Job.transfer disconnecting"
265
+ status :disconnecting
266
+ @ftp.close
267
+ end
207
268
 
208
- # Ensure @source and @target are there
209
- info "Job.transfer checking_source"
210
- set :status, :checking_source
211
- raise RestFtpDaemon::JobPrerequisitesNotMet unless @source_path
212
- raise RestFtpDaemon::JobPrerequisitesNotMet unless @target_url
213
- target_path = File.dirname @target_url.path
214
- target_name = File.basename @target_url.path
269
+ private
215
270
 
216
- # Read source file size
217
- source_size = File.size @source_path
218
- set :file_size, source_size
271
+ def oops signal_name, exception, error_name = nil, include_backtrace = false
272
+ # Log this error
273
+ error_name = exception.class if error_name.nil?
274
+ info "Job.oops si[#{signal_name}] er[#{error_name.to_s}] ex[#{exception.class}]"
275
+
276
+ # Update job's internal status
277
+ set :status, :failed
278
+ set :error, error_name
279
+ set :error_exception, exception.class
280
+
281
+ # Build status stack
282
+ status = nil
283
+ if include_backtrace
284
+ set :error_backtrace, exception.backtrace
285
+ status = {
286
+ backtrace: exception.backtrace,
287
+ }
288
+ end
219
289
 
220
- # Prepare FTP transfer
221
- info "Job.transfer preparing"
290
+ # Prepare notification if signal given
291
+ return unless signal_name
292
+ notify signal_name, error_name, status
293
+ end
294
+
295
+ def ftp_init
296
+ # Method assertions
297
+ info "Job.ftp_init"
298
+ status :ftp_init
299
+ raise RestFtpDaemon::JobAssertionFailed if @target_method.nil? || @target_url.nil?
222
300
 
223
- # Scheme-aware config
224
301
  case @target_method
225
302
  when :ftp
226
- info "Job.transfer scheme FTP"
227
- ftp = Net::FTP.new
303
+ info "Job.ftp_init scheme: ftp"
304
+ @ftp = Net::FTP.new
228
305
  when :ftps
229
- info "Job.transfer scheme FTPS"
230
- ftp = DoubleBagFTPS.new
231
- ftp.ssl_context = DoubleBagFTPS.create_ssl_context(:verify_mode => OpenSSL::SSL::VERIFY_NONE)
232
- ftp.ftps_mode = DoubleBagFTPS::EXPLICIT
306
+ info "Job.transfer scheme: ftps"
307
+ @ftp = DoubleBagFTPS.new
308
+ @ftp.ssl_context = DoubleBagFTPS.create_ssl_context(:verify_mode => OpenSSL::SSL::VERIFY_NONE)
309
+ @ftp.ftps_mode = DoubleBagFTPS::EXPLICIT
233
310
  else
234
- info "Job.transfer scheme other: [#{@target_url.scheme}]"
311
+ info "Job.transfer scheme: other [#{@target_url.scheme}]"
235
312
  end
313
+ end
236
314
 
237
- # Connect remote server
238
- info "Job.transfer connecting"
239
- set :status, :connecting
240
- ftp.connect(@target_url.host)
241
- ftp.passive = true
315
+ def ftp_connect
316
+ #status :ftp_connect
317
+ # connect_timeout_sec = (Settings.transfer.connect_timeout_sec rescue nil) || DEFAULT_CONNECT_TIMEOUT_SEC
242
318
 
243
- # Logging in
244
- info "Job.transfer login"
245
- begin
246
- u = ftp.login @target_url.user, @target_url.password
247
- rescue Exception => exception
248
- info "Job.process login failed [#{exception.class}] #{u.inspect}"
249
- set :status, :login_failed
250
- set :error, exception.class
251
- end
319
+ # Method assertions
320
+ info "Job.ftp_connect connect"
321
+ status :ftp_connect
322
+ raise RestFtpDaemon::JobAssertionFailed if @ftp.nil? || @target_url.nil?
252
323
 
253
- # Changing to directory
254
- info "Job.transfer chdir"
255
- set :status, :chdir
256
- ftp.chdir(target_path)
324
+ ret = @ftp.connect(@target_url.host)
325
+ @ftp.passive = true
257
326
 
258
- # Check for target file presence
259
- if get(:overwrite).nil?
260
- info "Job.transfer remote_check (#{target_name})"
261
- set :status, :remote_check
262
-
263
- # Get file list, sometimes the response can be an empty value
264
- results = ftp.list(target_name) rescue nil
265
-
266
- # Result can be nil or a list of files
267
- if results.nil? || results.count.zero?
268
- info "Job.transfer remote_absent"
269
- set :status, :remote_absent
270
- else
271
- info "Job.transfer remote_present"
272
- set :status, :remote_present
273
- ftp.close
274
- notify "rftpd.ended", RestFtpDaemon::JobTargetFileExists
275
- raise RestFtpDaemon::JobTargetFileExists
276
- end
327
+ info "Job.ftp_connect login"
328
+ status :ftp_login
329
+ ret = @ftp.login @target_url.user, @target_url.password
277
330
 
278
- end
331
+ info "Job.ftp_connect chdir"
332
+ status :ftp_chdir
333
+ path = File.dirname @target_url.path
334
+ ret = @ftp.chdir(path)
335
+ end
279
336
 
280
- # Do transfer
281
- info "Job.transfer uploading"
282
- set :status, :uploading
283
- chunk_size = Settings.transfer.chunk_size || Settings[:default_chunk_size]
284
- notify_size = Settings.transfer.chunk_size || Settings[:default_notify_size]
285
- ftp.putbinaryfile(@source_path, target_name, chunk_size) do |block|
337
+ def ftp_presence target_name
338
+ # Method assertions
339
+ info "Job.ftp_presence"
340
+ status :ftp_presence
341
+ raise RestFtpDaemon::JobAssertionFailed if @ftp.nil? || @target_url.nil?
342
+
343
+ # Get file list, sometimes the response can be an empty value
344
+ results = @ftp.list(target_name) rescue nil
345
+
346
+ # Result can be nil or a list of files
347
+ return false if results.nil?
348
+ return results.count >0
349
+ end
350
+
351
+ def ftp_transfer target_name
352
+ # Method assertions
353
+ info "Job.ftp_transfer starting"
354
+ status :ftp_transfer
355
+ raise RestFtpDaemon::JobAssertionFailed if @ftp.nil? || @source_path.nil?
356
+
357
+ # Read source file size and parameters
358
+ source_size = File.size @source_path
359
+ set :transfer_size, source_size
360
+ update_every_kb = (Settings.transfer.update_every_kb rescue nil) || DEFAULT_UPDATE_EVERY_KB
361
+ notify_after_sec = Settings.transfer.notify_after_sec rescue nil
362
+
363
+ # Start transfer
364
+ transferred = 0
365
+ chunk_size = update_every_kb * 1024
366
+ t0 = tstart = Time.now
367
+ notified_at = Time.now
368
+ status :uploading
369
+ @ftp.putbinaryfile(@source_path, target_name, chunk_size) do |block|
286
370
  # Update counters
287
371
  transferred += block.bytesize
372
+ set :transfer_sent, transferred
373
+
374
+ # Update bitrate
375
+ dt = Time.now - t0
376
+ bitrate0 = (8 * chunk_size/dt).round(0)
377
+ set :transfer_bitrate, bitrate0
288
378
 
289
379
  # Update job info
290
- percent = (100.0 * transferred / source_size).round(1)
291
- set :progress, percent
292
- set :file_sent, transferred
380
+ percent1 = (100.0 * transferred / source_size).round(1)
381
+ set :progress, percent1
382
+
383
+ # Log progress
384
+ status = []
385
+ status << "#{percent1} %"
386
+ status << (Helpers.format_bytes transferred, "B")
387
+ status << (Helpers.format_bytes source_size, "B")
388
+ status << (Helpers.format_bytes bitrate0, "bps")
389
+ info "Job.ftp_transfer" + status.map{|txt| ("%#{DEFAULT_LOGS_PROGNAME_TRIM.to_i}s" % txt)}.join("\t")
390
+
391
+ # Update time pointer
392
+ t0 = Time.now
393
+
394
+ # Notify if requested
395
+ unless notify_after_sec.nil? || (notified_at + notify_after_sec > Time.now)
396
+ status = {
397
+ progress: percent1,
398
+ transfer_sent: transferred,
399
+ transfer_size: source_size,
400
+ transfer_bitrate: bitrate0
401
+ }
402
+ notify "rftpd.progress", 0, status
403
+ notified_at = Time.now
404
+ end
405
+
293
406
  end
294
407
 
295
- # Close FTP connexion
296
- info "Job.transfer closing"
297
- set :status, :disconnecting
298
- notify "rftpd.ended"
299
- set :progress, nil
300
- ftp.close
408
+ # Compute final bitrate
409
+ tbitrate0 = (8 * source_size.to_f / (Time.now - tstart)).round(0)
410
+ set :transfer_bitrate, tbitrate0
411
+
412
+ # Add total transferred to counter
413
+ $queue.counter_add :transferred, source_size
414
+
415
+ # Done
416
+ #set :progress, nil
417
+ info "Job.ftp_transfer finished"
301
418
  end
302
419
 
303
- private
420
+ def notify signal, error = 0, status = {}
421
+ RestFtpDaemon::Notification.new get(:notify), {
422
+ id: get(:id),
423
+ signal: signal,
424
+ error: error,
425
+ status: status,
426
+ }
427
+ end
304
428
 
305
429
  end
306
430
  end