aspera-cli 4.0.0 → 4.2.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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +843 -304
  3. data/bin/dascli +13 -0
  4. data/docs/Makefile +4 -4
  5. data/docs/README.erb.md +805 -172
  6. data/docs/test_env.conf +22 -3
  7. data/examples/aoc.rb +14 -3
  8. data/examples/faspex4.rb +89 -0
  9. data/lib/aspera/aoc.rb +87 -108
  10. data/lib/aspera/cli/formater.rb +2 -0
  11. data/lib/aspera/cli/main.rb +89 -49
  12. data/lib/aspera/cli/plugin.rb +9 -4
  13. data/lib/aspera/cli/plugins/alee.rb +1 -1
  14. data/lib/aspera/cli/plugins/aoc.rb +188 -173
  15. data/lib/aspera/cli/plugins/ats.rb +2 -2
  16. data/lib/aspera/cli/plugins/config.rb +218 -145
  17. data/lib/aspera/cli/plugins/console.rb +2 -2
  18. data/lib/aspera/cli/plugins/faspex.rb +114 -61
  19. data/lib/aspera/cli/plugins/faspex5.rb +85 -43
  20. data/lib/aspera/cli/plugins/node.rb +3 -3
  21. data/lib/aspera/cli/plugins/preview.rb +59 -45
  22. data/lib/aspera/cli/plugins/server.rb +23 -8
  23. data/lib/aspera/cli/transfer_agent.rb +77 -49
  24. data/lib/aspera/cli/version.rb +1 -1
  25. data/lib/aspera/command_line_builder.rb +49 -31
  26. data/lib/aspera/cos_node.rb +33 -28
  27. data/lib/aspera/environment.rb +2 -2
  28. data/lib/aspera/fasp/connect.rb +28 -21
  29. data/lib/aspera/fasp/http_gw.rb +140 -28
  30. data/lib/aspera/fasp/installation.rb +93 -46
  31. data/lib/aspera/fasp/local.rb +88 -45
  32. data/lib/aspera/fasp/manager.rb +15 -0
  33. data/lib/aspera/fasp/node.rb +4 -4
  34. data/lib/aspera/fasp/parameters.rb +59 -101
  35. data/lib/aspera/fasp/parameters.yaml +531 -0
  36. data/lib/aspera/fasp/resume_policy.rb +13 -12
  37. data/lib/aspera/fasp/uri.rb +1 -1
  38. data/lib/aspera/log.rb +1 -1
  39. data/lib/aspera/node.rb +61 -1
  40. data/lib/aspera/oauth.rb +49 -46
  41. data/lib/aspera/persistency_folder.rb +9 -4
  42. data/lib/aspera/preview/file_types.rb +53 -21
  43. data/lib/aspera/preview/generator.rb +3 -3
  44. data/lib/aspera/rest.rb +29 -18
  45. data/lib/aspera/secrets.rb +20 -0
  46. data/lib/aspera/sync.rb +40 -35
  47. data/lib/aspera/temp_file_manager.rb +19 -0
  48. data/lib/aspera/web_auth.rb +105 -0
  49. metadata +54 -20
  50. data/docs/transfer_spec.html +0 -99
data/lib/aspera/oauth.rb CHANGED
@@ -1,22 +1,26 @@
1
1
  require 'aspera/open_application'
2
+ require 'aspera/web_auth'
2
3
  require 'base64'
3
4
  require 'date'
4
5
  require 'socket'
5
6
  require 'securerandom'
6
7
 
7
8
  module Aspera
8
- # implement OAuth 2 for the REST client and generate a bearer token
9
+ # Implement OAuth 2 for the REST client and generate a bearer token
9
10
  # call get_authorization() to get a token.
10
11
  # bearer tokens are kept in memory and also in a file cache for later re-use
11
12
  # if a token is expired (api returns 4xx), call again get_authorization({:refresh=>true})
12
13
  class Oauth
13
14
  private
14
15
  # remove 5 minutes to account for time offset (TODO: configurable?)
15
- JWT_NOTBEFORE_OFFSET=300
16
+ JWT_NOTBEFORE_OFFSET_SEC=300
16
17
  # one hour validity (TODO: configurable?)
17
- JWT_EXPIRY_OFFSET=3600
18
+ JWT_EXPIRY_OFFSET_SEC=3600
19
+ # tokens older than 30 minutes will be discarded from cache
20
+ TOKEN_CACHE_EXPIRY_SEC=1800
21
+ # a prefix for persistency of tokens (garbage collect)
18
22
  PERSIST_CATEGORY_TOKEN='token'
19
- private_constant :JWT_NOTBEFORE_OFFSET,:JWT_EXPIRY_OFFSET,:PERSIST_CATEGORY_TOKEN
23
+ private_constant :JWT_NOTBEFORE_OFFSET_SEC,:JWT_EXPIRY_OFFSET_SEC,:PERSIST_CATEGORY_TOKEN,:TOKEN_CACHE_EXPIRY_SEC
20
24
  class << self
21
25
  # OAuth methods supported
22
26
  def auth_types
@@ -28,12 +32,18 @@ module Aspera
28
32
  end
29
33
 
30
34
  def persist_mgr
31
- raise "set persistency manager first" if @persist.nil?
35
+ if @persist.nil?
36
+ Log.log.warn('Not using persistency (use Aspera::Oauth.persist_mgr=Aspera::PersistencyFolder.new)')
37
+ # create NULL persistency class
38
+ @persist=Class.new do
39
+ def get(x);nil;end;def delete(x);nil;end;def put(x,y);nil;end;def garbage_collect(x,y);nil;end
40
+ end.new
41
+ end
32
42
  return @persist
33
43
  end
34
44
 
35
45
  def flush_tokens
36
- persist_mgr.flush_by_prefix(PERSIST_CATEGORY_TOKEN)
46
+ persist_mgr.garbage_collect(PERSIST_CATEGORY_TOKEN,nil)
37
47
  end
38
48
  end
39
49
 
@@ -79,13 +89,19 @@ module Aspera
79
89
  raise "redirect_uri must have a port" if uri.port.nil?
80
90
  # we could check that host is localhost or local address
81
91
  end
92
+ # cleanup expired tokens
93
+ self.class.persist_mgr.garbage_collect(PERSIST_CATEGORY_TOKEN,TOKEN_CACHE_EXPIRY_SEC)
82
94
  end
83
95
 
84
- THANK_YOU_HTML = "<html><head><title>Ok</title></head><body><h1>Thank you !</h1><p>You can close this window.</p></body></html>"
85
-
86
96
  # open the login page, wait for code and check_code, then return code
87
97
  def goto_page_and_get_code(login_page_url,check_code)
88
- request_params=self.class.goto_page_and_get_request(@params[:redirect_uri],login_page_url)
98
+ Log.log.info("login_page_url=#{login_page_url}".bg_red.gray)
99
+ # start a web server to receive request code
100
+ webserver=WebAuth.new(@params[:redirect_uri])
101
+ # start browser on login page
102
+ OpenApplication.instance.uri(login_page_url)
103
+ # wait for code in request
104
+ request_params=webserver.get_request
89
105
  Log.log.error("state does not match") if !check_code.eql?(request_params['state'])
90
106
  code=request_params['code']
91
107
  return code
@@ -117,6 +133,9 @@ module Aspera
117
133
 
118
134
  public
119
135
 
136
+ # used to change parameter, such as scope
137
+ attr_reader :params
138
+
120
139
  # @param options : :scope and :refresh
121
140
  def get_authorization(options={})
122
141
  # api scope can be overriden to get auth for other scope
@@ -184,14 +203,13 @@ module Aspera
184
203
  when :web
185
204
  # AoC Web based Auth
186
205
  check_code=SecureRandom.uuid
187
- login_page_url=Rest.build_uri(
188
- "#{@params[:base_url]}/#{@params[:path_authorize]}",
189
- p_client_id_and_scope.merge({
206
+ auth_params=p_client_id_and_scope.merge({
190
207
  :response_type => 'code',
191
208
  :redirect_uri => @params[:redirect_uri],
192
- :client_secret => @params[:client_secret],
193
209
  :state => check_code
194
- }))
210
+ })
211
+ auth_params[:client_secret]=@params[:client_secret] if @params.has_key?(:client_secret)
212
+ login_page_url=Rest.build_uri("#{@params[:base_url]}/#{@params[:path_authorize]}",auth_params)
195
213
  # here, we need a human to authorize on a web page
196
214
  code=goto_page_and_get_code(login_page_url,check_code)
197
215
  # exchange code for token
@@ -211,9 +229,19 @@ module Aspera
211
229
  :iss => @params[:client_id], # issuer
212
230
  :sub => @params[:jwt_subject], # subject
213
231
  :aud => @params[:jwt_audience], # audience
214
- :nbf => seconds_since_epoch-JWT_NOTBEFORE_OFFSET, # not before
215
- :exp => seconds_since_epoch+JWT_EXPIRY_OFFSET # expiration
232
+ :nbf => seconds_since_epoch-JWT_NOTBEFORE_OFFSET_SEC, # not before
233
+ :exp => seconds_since_epoch+JWT_EXPIRY_OFFSET_SEC # expiration
216
234
  }
235
+ # Hum.. compliant ? TODO: remove when Faspex5 API is clarified
236
+ if @params.has_key?(:f5_username)
237
+ payload[:jti] = SecureRandom.uuid # JWT id
238
+ payload[:iat] = seconds_since_epoch # issued at
239
+ payload.delete(:nbf)
240
+ p_scope[:redirect_uri]="https://127.0.0.1:5000/token"
241
+ p_scope[:state]=SecureRandom.uuid
242
+ p_scope[:client_id]=@params[:client_id]
243
+ @token_auth_api.params[:auth]={type: :basic,username: @params[:f5_username], password: @params[:f5_password]}
244
+ end
217
245
 
218
246
  # non standard, only for global ids
219
247
  payload.merge!(@params[:jwt_add]) if @params.has_key?(:jwt_add)
@@ -222,8 +250,8 @@ module Aspera
222
250
 
223
251
  Log.log.debug("private=[#{rsa_private}]")
224
252
 
225
- Log.log.debug("JWT assertion=[#{payload}]")
226
- assertion = JWT.encode(payload, rsa_private, 'RS256')
253
+ Log.log.debug("JWT payload=[#{payload}]")
254
+ assertion = JWT.encode(payload, rsa_private, 'RS256',@params[:jwt_headers]||{})
227
255
 
228
256
  Log.log.debug("assertion=[#{assertion}]")
229
257
 
@@ -233,8 +261,10 @@ module Aspera
233
261
  }))
234
262
  when :url_token
235
263
  # AoC Public Link
264
+ params={:url_token=>@params[:url_token]}
265
+ params[:password]=@params[:password] if @params.has_key?(:password)
236
266
  resp=create_token_advanced({
237
- :json_params => {:url_token=>@params[:url_token]},
267
+ :json_params => params,
238
268
  :url_params => p_scope.merge({
239
269
  :grant_type => 'url_token'
240
270
  })})
@@ -288,32 +318,5 @@ module Aspera
288
318
  return 'Bearer '+token_data[@params[:token_field]]
289
319
  end
290
320
 
291
- # open the login page, wait for code and return parameters
292
- def self.goto_page_and_get_request(redirect_uri,login_page_url,html_page=THANK_YOU_HTML)
293
- Log.log.info "login_page_url=#{login_page_url}".bg_red().gray()
294
- # browser start is not blocking, we hope here that starting is slower than opening port
295
- OpenApplication.instance.uri(login_page_url)
296
- port=URI.parse(redirect_uri).port
297
- Log.log.info "listening on port #{port}"
298
- request_params=nil
299
- TCPServer.open('127.0.0.1', port) { |webserver|
300
- Log.log.info "server=#{webserver}"
301
- websession = webserver.accept
302
- sleep 1 # TODO: sometimes: returns nil ? use webrick ?
303
- line = websession.gets.chomp
304
- Log.log.info "line=#{line}"
305
- if ! line.start_with?('GET /?') then
306
- raise "unexpected request"
307
- end
308
- request = line.partition('?').last.partition(' ').first
309
- data=URI.decode_www_form(request)
310
- request_params=data.to_h
311
- Log.log.debug "request_params=#{request_params}"
312
- websession.print "HTTP/1.1 200/OK\r\nContent-type:text/html\r\n\r\n#{html_page}"
313
- websession.close
314
- }
315
- return request_params
316
- end
317
-
318
321
  end # OAuth
319
322
  end # Aspera
@@ -55,12 +55,17 @@ module Aspera
55
55
  @cache.delete(object_id)
56
56
  end
57
57
 
58
- def flush_by_prefix(persist_category)
59
- persist_files=Dir[File.join(@folder,persist_category+'*'+FILE_SUFFIX)]
60
- persist_files.each do |filepath|
58
+ def garbage_collect(persist_category,max_age_seconds=nil)
59
+ garbage_files=Dir[File.join(@folder,persist_category+'*'+FILE_SUFFIX)]
60
+ if !max_age_seconds.nil?
61
+ current_time = Time.now
62
+ garbage_files.select! { |filepath| (current_time - File.stat(filepath).mtime).to_i > max_age_seconds}
63
+ end
64
+ garbage_files.each do |filepath|
61
65
  File.delete(filepath)
66
+ Log.log.debug("Deleted expired: #{filepath}")
62
67
  end
63
- return persist_files
68
+ return garbage_files
64
69
  end
65
70
 
66
71
  private
@@ -1,9 +1,11 @@
1
- require 'mimemagic'
2
- require 'mimemagic/overlay'
1
+ require 'aspera/log'
2
+ require 'singleton'
3
3
 
4
4
  module Aspera
5
5
  module Preview
6
+ # function conversion_type returns one of the types: CONVERSION_TYPES
6
7
  class FileTypes
8
+ include Singleton
7
9
  # values for conversion_type : input format
8
10
  CONVERSION_TYPES=[
9
11
  :image,
@@ -148,6 +150,7 @@ module Aspera
148
150
  'dif' => :office,
149
151
  'divx' => :video,
150
152
  'dng' => :image,
153
+ 'docx' => :office,
151
154
  'dpx' => :image,
152
155
  'epdf' => :image,
153
156
  'epi' => :image,
@@ -204,6 +207,7 @@ module Aspera
204
207
  'pam' => :image,
205
208
  'pcd' => :image,
206
209
  'pcds' => :image,
210
+ 'pdf' => :pdf,
207
211
  'pef' => :image,
208
212
  'picon' => :image,
209
213
  'pict' => :image,
@@ -261,39 +265,67 @@ module Aspera
261
265
  'x3f' => :image,
262
266
  'xcf' => :image,
263
267
  'xlk' => :office,
268
+ 'xlsx' => :office,
269
+ 'xls' => :office,
264
270
  'ycbcr' => :image,
265
271
  'ycbcra' => :image,
266
272
  'yuv' => :image,
267
273
  'zabw' => :office}
268
274
 
269
- #private_constant :SUPPORTED_MIME_TYPES, :SUPPORTED_EXTENSIONS
275
+ private_constant :SUPPORTED_MIME_TYPES, :SUPPORTED_EXTENSIONS
270
276
 
271
- def self.mime_from_file(filepath)
277
+ # @attr use_mimemagic [bool] true to use mimemagic to determine real mime type based on file content
278
+ attr_accessor :use_mimemagic
279
+
280
+ def initialize
281
+ @use_mimemagic=false
282
+ end
283
+
284
+ # use mime magic to find mime type based on file content (magic numbers)
285
+ def mime_from_file(filepath)
286
+ # moved here, as mimemagic can cause installation issues
287
+ require 'mimemagic'
288
+ require 'mimemagic/version'
289
+ require 'mimemagic/overlay' if MimeMagic::VERSION.start_with?('0.3.')
290
+ # check magic number inside file (empty string if not found)
272
291
  detected_mime=MimeMagic.by_magic(File.open(filepath)).to_s
273
- detected_mime=MimeMagic.by_path(filepath).to_s if detected_mime.empty?
292
+ # check extension only
293
+ if !SUPPORTED_MIME_TYPES.has_key?(detected_mime)
294
+ Log.log.debug("no conversion for #{detected_mime}, trying extension")
295
+ detected_mime=MimeMagic.by_extension(File.extname(filepath)).to_s
296
+ end
274
297
  detected_mime=nil if detected_mime.empty?
298
+ Log.log.debug("mimemagic: #{detected_mime.class.name} [#{detected_mime}]")
275
299
  return detected_mime
276
300
  end
277
301
 
278
- def self.conversion_type(filepath,mimetype,try_local_mime)
279
- detected_mime=nil
280
- if try_local_mime
302
+ # return file type, one of enum CONVERSION_TYPES
303
+ # @param filepath [String] full path to file
304
+ # @param mimetype [String] provided by node api
305
+ def conversion_type(filepath,mimetype)
306
+ Log.log.debug("conversion_type(#{filepath},m=#{mimetype},t=#{@use_mimemagic})")
307
+ # 1- get type from provided mime type, using local mapping
308
+ conv_type=SUPPORTED_MIME_TYPES[mimetype] if ! mimetype.nil?
309
+ # 2- else, from computed mime type (if available)
310
+ if conv_type.nil? and @use_mimemagic
281
311
  detected_mime=mime_from_file(filepath)
282
- if mimetype.eql?(detected_mime)
283
- Log.log.debug("matching mime type per magic number")
284
- else
285
- # note: detected can be nil
286
- Log.log.debug("non matching mime types: node=[#{mimetype}], magic=[#{detected_mime}]")
312
+ if ! detected_mime.nil?
313
+ conv_type=SUPPORTED_MIME_TYPES[detected_mime]
314
+ if ! mimetype.nil?
315
+ if mimetype.eql?(detected_mime)
316
+ Log.log.debug("matching mime type per magic number")
317
+ else
318
+ # note: detected can be nil
319
+ Log.log.debug("non matching mime types: node=[#{mimetype}], magic=[#{detected_mime}]")
320
+ end
321
+ end
287
322
  end
288
323
  end
289
- # 1- get type from provided mime type
290
- result=FileTypes::SUPPORTED_MIME_TYPES[mimetype] unless mimetype.nil?
291
- # 2- else, from computed mime type
292
- result||=FileTypes::SUPPORTED_MIME_TYPES[detected_mime] unless detected_mime.nil?
293
- extension = File.extname(filepath).downcase.gsub(/^\./,'')
294
- # 3- else, from local extensions
295
- result||=FileTypes::SUPPORTED_EXTENSIONS[extension]
296
- return result
324
+ # 3- else, from extensions, using local mapping
325
+ extension = File.extname(filepath.downcase)[1..-1]
326
+ conv_type=SUPPORTED_EXTENSIONS[extension] if conv_type.nil?
327
+ Log.log.debug("conversion_type(#{extension}): #{conv_type.class.name} [#{conv_type}]")
328
+ return conv_type
297
329
  end
298
330
  end
299
331
  end
@@ -15,7 +15,7 @@ module Aspera
15
15
 
16
16
  # @param src source file path
17
17
  # @param dst destination file path
18
- # @param api_mime_type optional mime type as provided by node api
18
+ # @param api_mime_type optional mime type as provided by node api (or nil)
19
19
  # node API mime types are from: http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types
20
20
  # supported preview type is one of Preview::PREVIEW_FORMATS
21
21
  # the resulting preview file type is taken from destination file extension.
@@ -25,14 +25,14 @@ module Aspera
25
25
  # -> preview_format is one of Generator::PREVIEW_FORMATS
26
26
  # the conversion video->mp4 is implemented in methods: convert_video_to_mp4_using_<video_conversion>
27
27
  # -> conversion method is one of Generator::VIDEO_CONVERSION_METHODS
28
- def initialize(options,src,dst,main_temp_dir,api_mime_type=nil,try_local_mime=true)
28
+ def initialize(options,src,dst,main_temp_dir,api_mime_type)
29
29
  @options=options
30
30
  @source_file_path=src
31
31
  @destination_file_path=dst
32
32
  @temp_folder=File.join(main_temp_dir,@source_file_path.split('/').last.gsub(/\s/, '_').gsub(/\W/, ''))
33
33
  # extract preview format from extension of target file
34
34
  @preview_format_symb=File.extname(@destination_file_path).gsub(/^\./,'').to_sym
35
- @conversion_type=FileTypes.conversion_type(@source_file_path,api_mime_type,try_local_mime)
35
+ @conversion_type=FileTypes.instance.conversion_type(@source_file_path,api_mime_type)
36
36
  end
37
37
 
38
38
  # name of processing method in this object
data/lib/aspera/rest.rb CHANGED
@@ -7,6 +7,7 @@ require 'net/http'
7
7
  require 'net/https'
8
8
  require 'json'
9
9
  require 'base64'
10
+ require 'cgi'
10
11
  require 'ruby-progressbar'
11
12
 
12
13
  # add cancel method to http
@@ -33,6 +34,7 @@ module Aspera
33
34
  # true if https ignore certificate
34
35
  @@insecure=false
35
36
  @@user_agent='Ruby'
37
+ @@download_partial_suffix='.http_partial'
36
38
 
37
39
  public
38
40
  def self.insecure=(v); @@insecure=v;Log.log.debug("insecure => #{@@insecure}".red);end
@@ -98,9 +100,10 @@ module Aspera
98
100
  public
99
101
 
100
102
  attr_reader :params
103
+ attr_reader :oauth
101
104
 
102
- # @param a_rest_params default call parameters and authentication (:auth) :
103
- # :type (:basic, :oauth2, :url)
105
+ # @param a_rest_params default call parameters (merged at call) and (+) authentication (:auth) :
106
+ # :type (:basic, :oauth2, :url, :none)
104
107
  # :username [:basic]
105
108
  # :password [:basic]
106
109
  # :url_creds [:url]
@@ -117,16 +120,6 @@ module Aspera
117
120
  # default is no auth
118
121
  @params[:auth]||={:type=>:none}
119
122
  @params[:not_auth_codes]||=['401']
120
- # translate old auth parameters, remove prefix, place in auth
121
- [:auth,:basic,:oauth].each do |p_sym|
122
- p_str=p_sym.to_s+'_'
123
- @params.keys.select{|k|k.to_s.start_with?(p_str)}.each do |k_sym|
124
- name=k_sym.to_s[p_str.length..-1]
125
- name='grant' if k_sym.eql?(:oauth_type)
126
- @params[:auth][name.to_sym]=@params[k_sym]
127
- @params.delete(k_sym)
128
- end
129
- end
130
123
  @oauth=Oauth.new(@params[:auth]) if @params[:auth][:type].eql?(:oauth2)
131
124
  Log.dump('REST params(2)',@params)
132
125
  end
@@ -153,6 +146,7 @@ module Aspera
153
146
  Log.log.debug("accessing #{call_data[:subpath]}".red.bold.bg_green)
154
147
  call_data[:headers]||={}
155
148
  call_data[:headers]['User-Agent'] ||= @@user_agent
149
+ # defaults from @params are overriden by call dataz
156
150
  call_data=@params.deep_merge(call_data)
157
151
  case call_data[:auth][:type]
158
152
  when :none
@@ -206,10 +200,13 @@ module Aspera
206
200
 
207
201
  Log.log.debug("call_data = #{call_data}")
208
202
  result={:http=>nil}
203
+ # start a block to be able to retry the actual HTTP request
209
204
  begin
210
205
  # we try the call, and will retry only if oauth, as we can, first with refresh, and then re-auth if refresh is bad
211
206
  oauth_tries ||= 2
207
+ tries_remain_redirect||=4
212
208
  Log.log.debug("send request")
209
+ # make http request (pipelined)
213
210
  http_session.request(req) do |response|
214
211
  result[:http] = response
215
212
  if call_data.has_key?(:save_to_file)
@@ -223,12 +220,12 @@ module Aspera
223
220
  target_file=call_data[:save_to_file]
224
221
  # override user's path to path in header
225
222
  if !response['Content-Disposition'].nil? and m=response['Content-Disposition'].match(/filename="([^"]+)"/)
226
- target_file=m[1]
223
+ target_file=File.join(File.dirname(target_file),m[1])
227
224
  end
228
225
  # download with temp filename
229
- target_file_tmp=target_file+'.http.partial'
226
+ target_file_tmp="#{target_file}#{@@download_partial_suffix}"
230
227
  Log.log.debug("saving to: #{target_file}")
231
- File.open(target_file_tmp, "wb") do |file|
228
+ File.open(target_file_tmp, 'wb') do |file|
232
229
  result[:http].read_body do |fragment|
233
230
  file.write(fragment)
234
231
  new_process=progress.progress+fragment.length
@@ -239,7 +236,7 @@ module Aspera
239
236
  # rename at the end
240
237
  File.rename(target_file_tmp, target_file)
241
238
  progress=nil
242
- end
239
+ end # save_to_file
243
240
  end
244
241
  # sometimes there is a ITF8 char (e.g. (c) )
245
242
  result[:http].body.force_encoding("UTF-8") if result[:http].body.is_a?(String)
@@ -267,10 +264,24 @@ module Aspera
267
264
  end
268
265
  Log.log.debug("using new token=#{call_data[:headers]['Authorization']}")
269
266
  retry unless (oauth_tries -= 1).zero?
270
- end # if
267
+ end # if oauth
268
+ # moved ?
269
+ if e.response.is_a?(Net::HTTPRedirection)
270
+ if tries_remain_redirect > 0
271
+ tries_remain_redirect-=1
272
+ Log.log.info("URL is moved: #{e.response['location']}")
273
+ raise e
274
+ # TODO: rebuild request with new location
275
+ #retry
276
+ else
277
+ raise "too many redirect"
278
+ end
279
+ else
280
+ raise e
281
+ end
271
282
  # raise exception if could not retry and not return error in result
272
283
  raise e unless call_data[:return_error]
273
- end
284
+ end # begin request
274
285
  Log.log.debug("result=#{result}")
275
286
  return result
276
287
 
@@ -0,0 +1,20 @@
1
+ module Aspera
2
+ # Manage secrets in CLI using secure way (encryption, wallet, etc...)
3
+ class Secrets
4
+ attr_accessor :default_secret,:all_secrets
5
+ def initialize()
6
+ @default_secret=nil
7
+ @all_secrets={}
8
+ end
9
+
10
+ def get_secret(id=nil,mandatory=true)
11
+ secret=@default_secret || @all_secrets[id]
12
+ raise "please provide secret for #{id}" if secret.nil? and mandatory
13
+ return secret
14
+ end
15
+
16
+ def get_secrets
17
+ return @all_secrets
18
+ end
19
+ end
20
+ end
data/lib/aspera/sync.rb CHANGED
@@ -1,48 +1,53 @@
1
+ require 'aspera/command_line_builder'
2
+
1
3
  module Aspera
2
4
  # builds command line arg for async
3
5
  class Sync
4
- private
5
6
  INSTANCE_PARAMS=
6
7
  {
7
- 'alt_logdir' => { :type => :opt_with_arg, :accepted_types=>String},
8
- 'watchd' => { :type => :opt_with_arg, :accepted_types=>String},
9
- 'apply_local_docroot' => { :type => :opt_without_arg},
10
- 'quiet' => { :type => :opt_without_arg},
8
+ 'alt_logdir' => { :cltype => :opt_with_arg, :accepted_types=>:string},
9
+ 'watchd' => { :cltype => :opt_with_arg, :accepted_types=>:string},
10
+ 'apply_local_docroot' => { :cltype => :opt_without_arg},
11
+ 'quiet' => { :cltype => :opt_without_arg},
11
12
  }
12
13
  SESSION_PARAMS=
13
14
  {
14
- 'name' => { :type => :opt_with_arg, :accepted_types=>String},
15
- 'local_dir' => { :type => :opt_with_arg, :accepted_types=>String},
16
- 'remote_dir' => { :type => :opt_with_arg, :accepted_types=>String},
17
- 'local_db_dir' => { :type => :opt_with_arg, :accepted_types=>String},
18
- 'remote_db_dir' => { :type => :opt_with_arg, :accepted_types=>String},
19
- 'host' => { :type => :opt_with_arg, :accepted_types=>String},
20
- 'user' => { :type => :opt_with_arg, :accepted_types=>String},
21
- 'private_key_path' => { :type => :opt_with_arg, :accepted_types=>String},
22
- 'direction' => { :type => :opt_with_arg, :accepted_types=>String},
23
- 'checksum' => { :type => :opt_with_arg, :accepted_types=>String},
24
- 'tcp_port' => { :type => :opt_with_arg, :accepted_types=>Integer},
25
- 'rate_policy' => { :type => :opt_with_arg, :accepted_types=>String},
26
- 'target_rate' => { :type => :opt_with_arg, :accepted_types=>String},
27
- 'cooloff' => { :type => :opt_with_arg, :accepted_types=>Integer},
28
- 'pending_max' => { :type => :opt_with_arg, :accepted_types=>Integer},
29
- 'scan_intensity' => { :type => :opt_with_arg, :accepted_types=>String},
30
- 'cipher' => { :type => :opt_with_arg, :accepted_types=>String},
31
- 'transfer_threads' => { :type => :opt_with_arg, :accepted_types=>Integer},
32
- 'preserve_time' => { :type => :opt_without_arg},
33
- 'preserve_access_time' => { :type => :opt_without_arg},
34
- 'preserve_modification_time' => { :type => :opt_without_arg},
35
- 'preserve_uid' => { :type => :opt_without_arg},
36
- 'preserve_gid' => { :type => :opt_without_arg},
37
- 'create_dir' => { :type => :opt_without_arg},
38
- 'reset' => { :type => :opt_without_arg},
15
+ 'name' => { :cltype => :opt_with_arg, :accepted_types=>:string},
16
+ 'local_dir' => { :cltype => :opt_with_arg, :accepted_types=>:string},
17
+ 'remote_dir' => { :cltype => :opt_with_arg, :accepted_types=>:string},
18
+ 'local_db_dir' => { :cltype => :opt_with_arg, :accepted_types=>:string},
19
+ 'remote_db_dir' => { :cltype => :opt_with_arg, :accepted_types=>:string},
20
+ 'host' => { :cltype => :opt_with_arg, :accepted_types=>:string},
21
+ 'user' => { :cltype => :opt_with_arg, :accepted_types=>:string},
22
+ 'private_key_path' => { :cltype => :opt_with_arg, :accepted_types=>:string},
23
+ 'direction' => { :cltype => :opt_with_arg, :accepted_types=>:string},
24
+ 'checksum' => { :cltype => :opt_with_arg, :accepted_types=>:string},
25
+ 'tcp_port' => { :cltype => :opt_with_arg, :accepted_types=>:int},
26
+ 'rate_policy' => { :cltype => :opt_with_arg, :accepted_types=>:string},
27
+ 'target_rate' => { :cltype => :opt_with_arg, :accepted_types=>:string},
28
+ 'cooloff' => { :cltype => :opt_with_arg, :accepted_types=>:int},
29
+ 'pending_max' => { :cltype => :opt_with_arg, :accepted_types=>:int},
30
+ 'scan_intensity' => { :cltype => :opt_with_arg, :accepted_types=>:string},
31
+ 'cipher' => { :cltype => :opt_with_arg, :accepted_types=>:string},
32
+ 'transfer_threads' => { :cltype => :opt_with_arg, :accepted_types=>:int},
33
+ 'preserve_time' => { :cltype => :opt_without_arg},
34
+ 'preserve_access_time' => { :cltype => :opt_without_arg},
35
+ 'preserve_modification_time' => { :cltype => :opt_without_arg},
36
+ 'preserve_uid' => { :cltype => :opt_without_arg},
37
+ 'preserve_gid' => { :cltype => :opt_without_arg},
38
+ 'create_dir' => { :cltype => :opt_without_arg},
39
+ 'reset' => { :cltype => :opt_without_arg},
39
40
  # note: only one env var, but multiple sessions... may be a problem
40
- 'remote_password' => { :type => :envvar, :variable=>'ASPERA_SCP_PASS'},
41
- 'cookie' => { :type => :envvar, :variable=>'ASPERA_SCP_COOKIE'},
42
- 'token' => { :type => :envvar, :variable=>'ASPERA_SCP_TOKEN'},
43
- 'license' => { :type => :envvar, :variable=>'ASPERA_SCP_LICENSE'},
41
+ 'remote_password' => { :cltype => :envvar, :clvarname=>'ASPERA_SCP_PASS'},
42
+ 'cookie' => { :cltype => :envvar, :clvarname=>'ASPERA_SCP_COOKIE'},
43
+ 'token' => { :cltype => :envvar, :clvarname=>'ASPERA_SCP_TOKEN'},
44
+ 'license' => { :cltype => :envvar, :clvarname=>'ASPERA_SCP_LICENSE'},
44
45
  }
45
- public
46
+
47
+ Aspera::CommandLineBuilder.normalize_description(INSTANCE_PARAMS)
48
+ Aspera::CommandLineBuilder.normalize_description(SESSION_PARAMS)
49
+
50
+ private_constant :INSTANCE_PARAMS,:SESSION_PARAMS
46
51
 
47
52
  def initialize(sync_params)
48
53
  @sync_params=sync_params
@@ -6,6 +6,11 @@ module Aspera
6
6
  # create a temp file name for a given folder
7
7
  # files can be deleted on process exit by calling cleanup
8
8
  class TempFileManager
9
+ SEC_IN_DAY=86400
10
+ # assume no transfer last longer than this
11
+ # (garbage collect file list which were not deleted after transfer)
12
+ FILE_LIST_AGE_MAX_SEC=5*SEC_IN_DAY
13
+ private_constant :SEC_IN_DAY,:FILE_LIST_AGE_MAX_SEC
9
14
  include Singleton
10
15
  def initialize
11
16
  @created_files=[]
@@ -33,5 +38,19 @@ module Aspera
33
38
  username = Etc.getlogin || Etc.getpwuid(Process.uid).name || 'unknown_user' rescue 'unknown_user'
34
39
  return new_file_path_in_folder(Etc.systmpdir,base_name+'_'+username+'_')
35
40
  end
41
+
42
+ def cleanup_expired(temp_folder)
43
+ # garbage collect undeleted files
44
+ Dir.entries(temp_folder).each do |name|
45
+ file_path=File.join(temp_folder,name)
46
+ age_sec=(Time.now - File.stat(file_path).mtime).to_i
47
+ # check age of file, delete too old
48
+ if File.file?(file_path) and age_sec > FILE_LIST_AGE_MAX_SEC
49
+ Log.log.debug("garbage collecting #{name}")
50
+ File.delete(file_path)
51
+ end
52
+ end
53
+
54
+ end
36
55
  end
37
56
  end