aspera-cli 4.0.0.pre1

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.
Files changed (88) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +3592 -0
  3. data/bin/ascli +7 -0
  4. data/bin/asession +89 -0
  5. data/docs/Makefile +59 -0
  6. data/docs/README.erb.md +3012 -0
  7. data/docs/README.md +13 -0
  8. data/docs/diagrams.txt +49 -0
  9. data/docs/secrets.make +38 -0
  10. data/docs/test_env.conf +117 -0
  11. data/docs/transfer_spec.html +99 -0
  12. data/examples/aoc.rb +17 -0
  13. data/examples/proxy.pac +60 -0
  14. data/examples/transfer.rb +115 -0
  15. data/lib/aspera/api_detector.rb +60 -0
  16. data/lib/aspera/ascmd.rb +151 -0
  17. data/lib/aspera/ats_api.rb +43 -0
  18. data/lib/aspera/cli/basic_auth_plugin.rb +38 -0
  19. data/lib/aspera/cli/extended_value.rb +88 -0
  20. data/lib/aspera/cli/formater.rb +238 -0
  21. data/lib/aspera/cli/listener/line_dump.rb +17 -0
  22. data/lib/aspera/cli/listener/logger.rb +20 -0
  23. data/lib/aspera/cli/listener/progress.rb +52 -0
  24. data/lib/aspera/cli/listener/progress_multi.rb +91 -0
  25. data/lib/aspera/cli/main.rb +304 -0
  26. data/lib/aspera/cli/manager.rb +440 -0
  27. data/lib/aspera/cli/plugin.rb +90 -0
  28. data/lib/aspera/cli/plugins/alee.rb +24 -0
  29. data/lib/aspera/cli/plugins/ats.rb +231 -0
  30. data/lib/aspera/cli/plugins/bss.rb +71 -0
  31. data/lib/aspera/cli/plugins/config.rb +806 -0
  32. data/lib/aspera/cli/plugins/console.rb +62 -0
  33. data/lib/aspera/cli/plugins/cos.rb +106 -0
  34. data/lib/aspera/cli/plugins/faspex.rb +377 -0
  35. data/lib/aspera/cli/plugins/faspex5.rb +93 -0
  36. data/lib/aspera/cli/plugins/node.rb +438 -0
  37. data/lib/aspera/cli/plugins/oncloud.rb +937 -0
  38. data/lib/aspera/cli/plugins/orchestrator.rb +169 -0
  39. data/lib/aspera/cli/plugins/preview.rb +464 -0
  40. data/lib/aspera/cli/plugins/server.rb +216 -0
  41. data/lib/aspera/cli/plugins/shares.rb +63 -0
  42. data/lib/aspera/cli/plugins/shares2.rb +114 -0
  43. data/lib/aspera/cli/plugins/sync.rb +65 -0
  44. data/lib/aspera/cli/plugins/xnode.rb +115 -0
  45. data/lib/aspera/cli/transfer_agent.rb +251 -0
  46. data/lib/aspera/cli/version.rb +5 -0
  47. data/lib/aspera/colors.rb +39 -0
  48. data/lib/aspera/command_line_builder.rb +137 -0
  49. data/lib/aspera/fasp/aoc.rb +24 -0
  50. data/lib/aspera/fasp/connect.rb +99 -0
  51. data/lib/aspera/fasp/error.rb +21 -0
  52. data/lib/aspera/fasp/error_info.rb +60 -0
  53. data/lib/aspera/fasp/http_gw.rb +81 -0
  54. data/lib/aspera/fasp/installation.rb +240 -0
  55. data/lib/aspera/fasp/listener.rb +11 -0
  56. data/lib/aspera/fasp/local.rb +377 -0
  57. data/lib/aspera/fasp/manager.rb +69 -0
  58. data/lib/aspera/fasp/node.rb +88 -0
  59. data/lib/aspera/fasp/parameters.rb +235 -0
  60. data/lib/aspera/fasp/resume_policy.rb +76 -0
  61. data/lib/aspera/fasp/uri.rb +51 -0
  62. data/lib/aspera/faspex_gw.rb +196 -0
  63. data/lib/aspera/hash_ext.rb +28 -0
  64. data/lib/aspera/log.rb +80 -0
  65. data/lib/aspera/nagios.rb +71 -0
  66. data/lib/aspera/node.rb +14 -0
  67. data/lib/aspera/oauth.rb +319 -0
  68. data/lib/aspera/on_cloud.rb +421 -0
  69. data/lib/aspera/open_application.rb +72 -0
  70. data/lib/aspera/persistency_action_once.rb +42 -0
  71. data/lib/aspera/persistency_folder.rb +91 -0
  72. data/lib/aspera/preview/file_types.rb +300 -0
  73. data/lib/aspera/preview/generator.rb +258 -0
  74. data/lib/aspera/preview/image_error.png +0 -0
  75. data/lib/aspera/preview/options.rb +35 -0
  76. data/lib/aspera/preview/utils.rb +131 -0
  77. data/lib/aspera/preview/video_error.png +0 -0
  78. data/lib/aspera/proxy_auto_config.erb.js +287 -0
  79. data/lib/aspera/proxy_auto_config.rb +34 -0
  80. data/lib/aspera/rest.rb +296 -0
  81. data/lib/aspera/rest_call_error.rb +13 -0
  82. data/lib/aspera/rest_error_analyzer.rb +98 -0
  83. data/lib/aspera/rest_errors_aspera.rb +58 -0
  84. data/lib/aspera/ssh.rb +53 -0
  85. data/lib/aspera/sync.rb +82 -0
  86. data/lib/aspera/temp_file_manager.rb +37 -0
  87. data/lib/aspera/uri_reader.rb +25 -0
  88. metadata +288 -0
@@ -0,0 +1,11 @@
1
+ module Aspera
2
+ module Fasp
3
+ # imlement this class to get transfer events
4
+ class Listener
5
+ # define one of the following methods:
6
+ # event_text(text_data)
7
+ # event_struct(legacy_names)
8
+ # event_enhanced(snake_names)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,377 @@
1
+ #!/bin/echo this is a ruby class:
2
+ #
3
+ # FASP manager for Ruby
4
+ # Aspera 2016
5
+ # Laurent Martin
6
+ #
7
+ ##############################################################################
8
+ require 'aspera/fasp/manager'
9
+ require 'aspera/fasp/error'
10
+ require 'aspera/fasp/parameters'
11
+ require 'aspera/fasp/installation'
12
+ require 'aspera/fasp/resume_policy'
13
+ require 'aspera/log'
14
+ require 'socket'
15
+ require 'timeout'
16
+ require 'securerandom'
17
+
18
+ module Aspera
19
+ module Fasp
20
+ # default transfer username for access key based transfers
21
+ ACCESS_KEY_TRANSFER_USER='xfer'
22
+ # executes a local "ascp", connects mgt port, equivalent of "Fasp Manager"
23
+ class Local < Manager
24
+ # set to false to keep ascp progress bar display (basically: removes ascp's option -q)
25
+ attr_accessor :quiet
26
+ # start FASP transfer based on transfer spec (hash table)
27
+ # note that it is asynchronous
28
+ def start_transfer(transfer_spec,options={})
29
+ raise "option: must be hash (or nil)" unless options.is_a?(Hash)
30
+ job_id=options[:job_id] || SecureRandom.uuid
31
+ # if there is aspera tags
32
+ if transfer_spec['tags'].is_a?(Hash) and transfer_spec['tags']['aspera'].is_a?(Hash)
33
+ # TODO: what is this for ? only on local ascp ?
34
+ # NOTE: important: transfer id must be unique: generate random id
35
+ # using a non unique id results in discard of tags in AoC, and a package is never finalized
36
+ transfer_spec['tags']['aspera']['xfer_id']||=SecureRandom.uuid
37
+ Log.log.debug("xfer id=#{transfer_spec['xfer_id']}")
38
+ # TODO: useful ? node only ?
39
+ transfer_spec['tags']['aspera']['xfer_retry']||=3600
40
+ end
41
+ Log.dump('ts',transfer_spec)
42
+ # add bypass keys when authentication is token and no auth is provided
43
+ if transfer_spec.has_key?('token') and
44
+ !transfer_spec.has_key?('remote_password') and
45
+ !transfer_spec.has_key?('EX_ssh_key_paths')
46
+ keys=Installation.instance.bypass_keys
47
+ transfer_spec['remote_password'] = keys.shift
48
+ transfer_spec['EX_ssh_key_paths'] = keys
49
+ end
50
+
51
+ # compute known args
52
+ env_args=Parameters.ts_to_env_args(transfer_spec,wss: @enable_wss)
53
+
54
+ # add fallback cert and key as arguments if needed
55
+ if ['1','force'].include?(transfer_spec['http_fallback'])
56
+ env_args[:args].unshift('-Y',Installation.instance.path(:fallback_key))
57
+ env_args[:args].unshift('-I',Installation.instance.path(:fallback_cert))
58
+ end
59
+
60
+ env_args[:args].unshift('-q') if @quiet
61
+
62
+ # transfer job can be multi session
63
+ xfer_job={
64
+ :id => job_id,
65
+ :sessions => []
66
+ }
67
+
68
+ # generic session information
69
+ session={
70
+ :state => :initial, # :initial, :started, :success, :failed
71
+ :env_args => env_args,
72
+ :resumer => options['resume_policy'] || @resume_policy,
73
+ :options => options
74
+ }
75
+
76
+ Log.log.debug("starting session thread(s)")
77
+ if !transfer_spec.has_key?('multi_session')
78
+ # single session for transfer : simple
79
+ session[:thread] = Thread.new(session) {|s|transfer_thread_entry(s)}
80
+ xfer_job[:sessions].push(session)
81
+ else
82
+ # default value overriden by fasp_port
83
+ multi_session_udp_port_base=33001
84
+ multi_session=transfer_spec['multi_session'].to_i
85
+ raise "multi_session(#{transfer_spec['multi_session']}) shall be integer > 1" unless multi_session >= 1
86
+ # managed here, so delete from transfer spec
87
+ transfer_spec.delete('multi_session')
88
+ # TODO: check if changing fasp(UDP) port is really necessary, not clear from doc
89
+ if transfer_spec.has_key?('fasp_port')
90
+ multi_session_udp_port_base=transfer_spec['fasp_port']
91
+ transfer_spec.delete('fasp_port')
92
+ end
93
+ 1.upto(multi_session) do |i|
94
+ # do deep copy (each thread has its own copy because it is modified here below and in thread)
95
+ this_session=session.clone()
96
+ this_session[:env_args]=this_session[:env_args].clone()
97
+ this_session[:env_args][:args]=this_session[:env_args][:args].clone()
98
+ this_session[:env_args][:args].unshift("-C#{i}:#{multi_session}")
99
+ # necessary only if server is not linux, i.e. server does not support port re-use
100
+ this_session[:env_args][:args].unshift("-O","#{multi_session_udp_port_base+i-1}")
101
+ this_session[:thread] = Thread.new(this_session) {|s|transfer_thread_entry(s)}
102
+ xfer_job[:sessions].push(this_session)
103
+ end
104
+ end
105
+ Log.log.debug("started session thread(s)")
106
+
107
+ # add job to list of jobs
108
+ @jobs[job_id]=xfer_job
109
+
110
+ Log.log.debug("jobs: #{@jobs.keys.count}")
111
+ return job_id
112
+ end # start_transfer
113
+
114
+ # wait for completion of all jobs started
115
+ # @return list of :success or error message
116
+ def wait_for_transfers_completion
117
+ Log.log.debug("wait_for_sessions: #{@jobs.values.inject(0){|m,j|m+j[:sessions].count}}")
118
+ @mutex.synchronize do
119
+ loop do
120
+ running=0
121
+ result=[]
122
+ @jobs.each do |id,job|
123
+ job[:sessions].each do |session|
124
+ case session[:state]
125
+ when :failed; result.push(session[:error])
126
+ when :success; result.push(:success)
127
+ else running+=1
128
+ end
129
+ end
130
+ end
131
+ if running.eql?(0)
132
+ # since all are finished and we return the result, clear statuses
133
+ @jobs.clear
134
+ return result
135
+ end
136
+ Log.log.debug("wait for completed: running: #{running}")
137
+ # wait for session termination
138
+ @cond_var.wait(@mutex)
139
+ end # loop
140
+ end # mutex
141
+ # never reach here
142
+ raise "internal error"
143
+ end
144
+
145
+ # terminates monitor thread
146
+ def shutdown
147
+ Log.log.debug("fasp local shutdown")
148
+ Log.log.debug("send signal to monitor")
149
+ # tell monitor to stop
150
+ @mutex.synchronize do
151
+ @monitor_stop=true
152
+ @cond_var.broadcast
153
+ end
154
+ # wait for thread termination
155
+ @monitor_thread.join
156
+ @monitor_thread=nil
157
+ Log.log.debug("joined monitor")
158
+ end
159
+
160
+ # This is the low level method to start FASP
161
+ # currently, relies on command line arguments
162
+ # start ascp with management port.
163
+ # raises FaspError on error
164
+ # if there is a thread info: set and broadcast session id
165
+ # @param env_args a hash containing :args :env :ascp_version
166
+ # cloud be private method
167
+ def start_transfer_with_args_env(env_args,session=nil)
168
+ begin
169
+ Log.log.debug("env_args=#{env_args.inspect}")
170
+ ascp_path=Fasp::Installation.instance.path(env_args[:ascp_version])
171
+ raise Fasp::Error.new("no such file: #{ascp_path}") unless File.exist?(ascp_path)
172
+ ascp_pid=nil
173
+ ascp_arguments=env_args[:args].clone
174
+ # open random local TCP port listening
175
+ mgt_sock = TCPServer.new('127.0.0.1',0 )
176
+ # add management port
177
+ ascp_arguments.unshift('-M', mgt_sock.addr[1].to_s)
178
+ # start ascp in sub process
179
+ Log.log.debug("execute: #{env_args[:env].map{|k,v| "#{k}=\"#{v}\""}.join(' ')} \"#{ascp_path}\" \"#{ascp_arguments.join('" "')}\"")
180
+ # start process
181
+ ascp_pid = Process.spawn(env_args[:env],[ascp_path,ascp_path],*ascp_arguments)
182
+ # in parent, wait for connection to socket max 3 seconds
183
+ Log.log.debug("before accept for pid (#{ascp_pid})")
184
+ ascp_mgt_io=nil
185
+ Timeout.timeout( 3 ) do
186
+ ascp_mgt_io = mgt_sock.accept
187
+ # management messages include file names which may be utf8
188
+ # by default socket is US-ASCII
189
+ # TODO: use same value as Encoding.default_external
190
+ ascp_mgt_io.set_encoding(Encoding::UTF_8)
191
+ end
192
+ Log.log.debug("after accept (#{ascp_mgt_io})")
193
+
194
+ unless session.nil?
195
+ @mutex.synchronize do
196
+ session[:io]=ascp_mgt_io
197
+ @cond_var.broadcast
198
+ end
199
+ end
200
+ # exact text for event, with \n
201
+ current_event_text=''
202
+ # parsed event (hash)
203
+ current_event_data=nil
204
+
205
+ # this is the last full status
206
+ last_status_event=nil
207
+
208
+ # read management port
209
+ loop do
210
+ # TODO: timeout here ?
211
+ line = ascp_mgt_io.gets
212
+ # nil when ascp process exits
213
+ break if line.nil?
214
+ current_event_text=current_event_text+line
215
+ line.chomp!
216
+ Log.log.debug("line=[#{line}]")
217
+ case line
218
+ when 'FASPMGR 2'
219
+ # begin event
220
+ current_event_data = Hash.new
221
+ current_event_text = ''
222
+ when /^([^:]+): (.*)$/
223
+ # event field
224
+ current_event_data[$1] = $2
225
+ when ''
226
+ # end event
227
+ raise "unexpected empty line" if current_event_data.nil?
228
+ current_event_data[Manager::LISTENER_SESSION_ID_B]=ascp_pid
229
+ notify_listeners(current_event_text,current_event_data)
230
+ # TODO: check if this is always the last event
231
+ case current_event_data['Type']
232
+ when 'DONE','ERROR'
233
+ last_status_event = current_event_data
234
+ when 'INIT'
235
+ unless session.nil?
236
+ @mutex.synchronize do
237
+ session[:state]=:started
238
+ session[:id]=current_event_data['SessionId']
239
+ Log.log.debug("session id: #{session[:id]}")
240
+ @cond_var.broadcast
241
+ end
242
+ end
243
+ end # event type
244
+ else
245
+ raise "unexpected line:[#{line}]"
246
+ end # case
247
+ end # loop
248
+ # check that last status was received before process exit
249
+ raise "INTERNAL: nil last status" if last_status_event.nil?
250
+ case last_status_event['Type']
251
+ when 'DONE'
252
+ return
253
+ when 'ERROR'
254
+ Log.log.error("code: #{last_status_event['Code']}")
255
+ if last_status_event['Description'] =~ /bearer token/i
256
+ Log.log.error("need to regenerate token".red)
257
+ if !session.nil? and session[:options].is_a?(Hash) and session[:options].has_key?(:regenerate_token)
258
+ # regenerate token here, expired, or error on it
259
+ env_args[:env]['ASPERA_SCP_TOKEN']=session[:options][:regenerate_token].call(true)
260
+ end
261
+ end
262
+ raise Fasp::Error.new(last_status_event['Description'],last_status_event['Code'].to_i)
263
+ else
264
+ raise "INTERNAL ERROR: unexpected last event"
265
+ end
266
+ rescue SystemCallError => e
267
+ # Process.spawn
268
+ raise Fasp::Error.new(e.message)
269
+ rescue Timeout::Error => e
270
+ raise Fasp::Error.new('timeout waiting mgt port connect')
271
+ rescue Interrupt => e
272
+ raise Fasp::Error.new('transfer interrupted by user')
273
+ ensure
274
+ # ensure there is no ascp left running
275
+ unless ascp_pid.nil?
276
+ begin
277
+ Process.kill('INT',ascp_pid)
278
+ rescue
279
+ end
280
+ # avoid zombie
281
+ Process.wait(ascp_pid)
282
+ ascp_pid=nil
283
+ session.delete(:io)
284
+ end
285
+ end # begin-ensure
286
+ end # start_transfer_with_args_env
287
+
288
+ # send command on mgt port, examples:
289
+ # {'type'=>'START','source'=>_path_,'destination'=>_path_}
290
+ # {'type'=>'DONE'}
291
+ def send_command(job_id,session_index,data)
292
+ @mutex.synchronize do
293
+ job=@jobs[job_id]
294
+ raise "no such job" if job.nil?
295
+ session=job[:sessions][session_index]
296
+ raise "no such session" if session.nil?
297
+ Log.log.debug("command: #{data}")
298
+ command=data.
299
+ keys.
300
+ map{|k|"#{k.capitalize}: #{data[k]}"}.
301
+ unshift('FASPMGR 2').
302
+ push('','').
303
+ join("\n")
304
+ session[:io].puts(command)
305
+ end
306
+ end
307
+
308
+ private
309
+
310
+ def initialize(agent_options=nil)
311
+ agent_options||={}
312
+ super()
313
+ # by default no interactive progress bar
314
+ @quiet=true
315
+ # shared data between transfer threads and others: protected by mutex, CV on change
316
+ @jobs={}
317
+ # mutex protects jobs data
318
+ @mutex=Mutex.new
319
+ # cond var is waited or broadcast on jobs data change
320
+ @cond_var=ConditionVariable.new
321
+ # must be set before starting monitor, set to false to stop thread. also shared and protected by mutex
322
+ @monitor_stop=false
323
+ @monitor_thread=Thread.new{monitor_thread_entry}
324
+ @resume_policy=ResumePolicy.new(agent_options)
325
+ @enable_wss = agent_options[:wss] || false
326
+ end
327
+
328
+ # transfer thread entry
329
+ # implements resumable transfer
330
+ # TODO: extract resume algorithm in a specific object
331
+ def transfer_thread_entry(session)
332
+ begin
333
+ # set name for logging
334
+ Thread.current[:name]="transfer"
335
+ # update state once in thread
336
+ session[:state]=:started
337
+ Log.log.debug("ENTER (#{Thread.current[:name]})")
338
+ # start transfer with selected resumer policy
339
+ session[:resumer].process do
340
+ start_transfer_with_args_env(session[:env_args],session)
341
+ end
342
+ Log.log.debug('transfer ok'.bg_green)
343
+ session[:state]=:success
344
+ rescue => e
345
+ session[:state]=:failed
346
+ session[:error]=e
347
+ Log.log.error("#{e.class}:\n#{e.message}:\n#{e.backtrace.join("\n")}".red) if Log.instance.level.eql?(:debug)
348
+ ensure
349
+ @mutex.synchronize do
350
+ # ensure id is set to unblock start procedure
351
+ session[:id]||=nil
352
+ @cond_var.broadcast
353
+ end
354
+ end
355
+ Log.log.debug("EXIT (#{Thread.current[:name]})")
356
+ end
357
+
358
+ # main thread method for monitor
359
+ # currently: just joins started threads
360
+ def monitor_thread_entry
361
+ Thread.current[:name]="monitor"
362
+ @mutex.synchronize do
363
+ until @monitor_stop do
364
+ # wait for session termination
365
+ @cond_var.wait(@mutex)
366
+ @jobs.values do |job|
367
+ job[:sessions].each do |session|
368
+ session[:thread].join if [:success,:failed].include?(session[:state])
369
+ end # sessions
370
+ end # jobs
371
+ end # monitor run
372
+ end # sync
373
+ Log.log.debug("EXIT (#{Thread.current[:name]})")
374
+ end # monitor_thread_entry
375
+ end # Local
376
+ end
377
+ end
@@ -0,0 +1,69 @@
1
+ module Aspera
2
+ module Fasp
3
+ # Base class for FASP transfer agents
4
+ # sub classes shall implement start_transfer and shutdown
5
+ class Manager
6
+
7
+ private
8
+
9
+ # fields description for JSON generation
10
+ IntegerFields=['Bytescont','FaspFileArgIndex','StartByte','Rate','MinRate','Port','Priority','RateCap','MinRateCap','TCPPort','CreatePolicy','TimePolicy','DatagramSize','XoptFlags','VLinkVersion','PeerVLinkVersion','DSPipelineDepth','PeerDSPipelineDepth','ReadBlockSize','WriteBlockSize','ClusterNumNodes','ClusterNodeId','Size','Written','Loss','FileBytes','PreTransferBytes','TransferBytes','PMTU','Elapsedusec','ArgScansAttempted','ArgScansCompleted','PathScansAttempted','FileScansCompleted','TransfersAttempted','TransfersPassed','Delay']
11
+ BooleanFields=['Encryption','Remote','RateLock','MinRateLock','PolicyLock','FilesEncrypt','FilesDecrypt','VLinkLocalEnabled','VLinkRemoteEnabled','MoveRange','Keepalive','TestLogin','UseProxy','Precalc','RTTAutocorrect']
12
+ ExpectedMethod=[:text,:struct,:enhanced]
13
+
14
+ # translates legacy event into enhanced (JSON) event
15
+ def enhanced_event_format(event)
16
+ return event.keys.inject({}) do |h,e|
17
+ # capital_to_snake_case
18
+ new_name=e.
19
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
20
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
21
+ gsub(/([a-z\d])(usec)$/,'\1_\2').
22
+ downcase
23
+ value=event[e]
24
+ value=value.to_i if IntegerFields.include?(e)
25
+ value=value.eql?('Yes') ? true : false if BooleanFields.include?(e)
26
+ h[new_name]=value
27
+ h
28
+ end
29
+ end
30
+
31
+ def initialize
32
+ @listeners=[]
33
+ end
34
+
35
+ def notify_listeners(current_event_text,current_event_data)
36
+ Log.log.debug("send event to listeners")
37
+ enhanced_event=nil
38
+ @listeners.each do |listener|
39
+ listener.send(:event_text,current_event_text) if listener.respond_to?(:event_text)
40
+ listener.send(:event_struct,current_event_data) if listener.respond_to?(:event_struct)
41
+ if listener.respond_to?(:event_enhanced)
42
+ enhanced_event=enhanced_event_format(current_event_data) if enhanced_event.nil?
43
+ listener.send(:event_enhanced,enhanced_event)
44
+ end
45
+ end
46
+ end # notify_listeners
47
+
48
+ public
49
+ LISTENER_SESSION_ID_B='ListenerSessionId'
50
+ LISTENER_SESSION_ID_S='listener_session_id'
51
+
52
+ # listener receives events
53
+ def add_listener(listener)
54
+ raise "expect one of #{ExpectedMethod}" if ExpectedMethod.inject(0){|m,e|m+=listener.respond_to?("event_#{e}")?1:0;m}.eql?(0)
55
+ @listeners.push(listener)
56
+ self
57
+ end
58
+
59
+ # the following methods must be implemented by subclass:
60
+ # start_transfer(transfer_spec,options) : start and wait for completion
61
+ # wait_for_transfers_completion : wait for termination of all transfers, @return list of : :success or error message
62
+ # optional: shutdown
63
+ def self.validate_status_list(statuses)
64
+ raise "internal error: bad statuses type: #{statuses.class}" unless statuses.is_a?(Array)
65
+ raise "internal error: bad statuses content: #{statuses}" unless statuses.select{|i|!i.eql?(:success) and !i.is_a?(StandardError)}.empty?
66
+ end
67
+ end
68
+ end
69
+ end