aspera-cli 4.14.0 → 4.16.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (104) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/BUGS.md +29 -3
  4. data/CHANGELOG.md +300 -185
  5. data/CONTRIBUTING.md +74 -23
  6. data/README.md +2346 -1619
  7. data/bin/ascli +16 -25
  8. data/bin/asession +15 -15
  9. data/examples/dascli +2 -2
  10. data/examples/proxy.pac +1 -1
  11. data/lib/aspera/aoc.rb +216 -150
  12. data/lib/aspera/ascmd.rb +25 -18
  13. data/lib/aspera/assert.rb +45 -0
  14. data/lib/aspera/cli/basic_auth_plugin.rb +9 -6
  15. data/lib/aspera/cli/error.rb +17 -0
  16. data/lib/aspera/cli/extended_value.rb +51 -16
  17. data/lib/aspera/cli/formatter.rb +276 -174
  18. data/lib/aspera/cli/hints.rb +81 -0
  19. data/lib/aspera/cli/main.rb +114 -147
  20. data/lib/aspera/cli/manager.rb +181 -136
  21. data/lib/aspera/cli/plugin.rb +82 -64
  22. data/lib/aspera/cli/plugins/alee.rb +0 -1
  23. data/lib/aspera/cli/plugins/aoc.rb +327 -331
  24. data/lib/aspera/cli/plugins/ats.rb +12 -8
  25. data/lib/aspera/cli/plugins/bss.rb +2 -2
  26. data/lib/aspera/cli/plugins/config.rb +575 -439
  27. data/lib/aspera/cli/plugins/console.rb +40 -0
  28. data/lib/aspera/cli/plugins/cos.rb +4 -5
  29. data/lib/aspera/cli/plugins/faspex.rb +111 -92
  30. data/lib/aspera/cli/plugins/faspex5.rb +245 -182
  31. data/lib/aspera/cli/plugins/node.rb +239 -160
  32. data/lib/aspera/cli/plugins/orchestrator.rb +56 -19
  33. data/lib/aspera/cli/plugins/preview.rb +54 -38
  34. data/lib/aspera/cli/plugins/server.rb +63 -20
  35. data/lib/aspera/cli/plugins/shares.rb +64 -38
  36. data/lib/aspera/cli/sync_actions.rb +68 -0
  37. data/lib/aspera/cli/transfer_agent.rb +64 -67
  38. data/lib/aspera/cli/transfer_progress.rb +73 -0
  39. data/lib/aspera/cli/version.rb +1 -1
  40. data/lib/aspera/colors.rb +3 -1
  41. data/lib/aspera/command_line_builder.rb +27 -22
  42. data/lib/aspera/cos_node.rb +6 -4
  43. data/lib/aspera/coverage.rb +22 -0
  44. data/lib/aspera/data_repository.rb +33 -2
  45. data/lib/aspera/environment.rb +21 -8
  46. data/lib/aspera/fasp/agent_alpha.rb +116 -0
  47. data/lib/aspera/fasp/agent_base.rb +40 -76
  48. data/lib/aspera/fasp/agent_connect.rb +21 -22
  49. data/lib/aspera/fasp/agent_direct.rb +169 -179
  50. data/lib/aspera/fasp/agent_httpgw.rb +200 -195
  51. data/lib/aspera/fasp/agent_node.rb +43 -35
  52. data/lib/aspera/fasp/agent_trsdk.rb +124 -41
  53. data/lib/aspera/fasp/error_info.rb +2 -2
  54. data/lib/aspera/fasp/faux_file.rb +52 -0
  55. data/lib/aspera/fasp/installation.rb +89 -191
  56. data/lib/aspera/fasp/management.rb +249 -0
  57. data/lib/aspera/fasp/parameters.rb +86 -47
  58. data/lib/aspera/fasp/parameters.yaml +75 -8
  59. data/lib/aspera/fasp/products.rb +162 -0
  60. data/lib/aspera/fasp/resume_policy.rb +7 -5
  61. data/lib/aspera/fasp/sync.rb +273 -0
  62. data/lib/aspera/fasp/transfer_spec.rb +10 -8
  63. data/lib/aspera/fasp/uri.rb +6 -6
  64. data/lib/aspera/faspex_gw.rb +11 -8
  65. data/lib/aspera/faspex_postproc.rb +8 -7
  66. data/lib/aspera/hash_ext.rb +2 -2
  67. data/lib/aspera/id_generator.rb +3 -1
  68. data/lib/aspera/json_rpc.rb +51 -0
  69. data/lib/aspera/keychain/encrypted_hash.rb +46 -11
  70. data/lib/aspera/keychain/macos_security.rb +15 -13
  71. data/lib/aspera/line_logger.rb +23 -0
  72. data/lib/aspera/log.rb +61 -19
  73. data/lib/aspera/nagios.rb +7 -2
  74. data/lib/aspera/node.rb +105 -21
  75. data/lib/aspera/node_simulator.rb +214 -0
  76. data/lib/aspera/oauth.rb +57 -36
  77. data/lib/aspera/open_application.rb +4 -4
  78. data/lib/aspera/persistency_action_once.rb +13 -14
  79. data/lib/aspera/persistency_folder.rb +5 -4
  80. data/lib/aspera/preview/file_types.rb +56 -268
  81. data/lib/aspera/preview/generator.rb +28 -39
  82. data/lib/aspera/preview/options.rb +2 -0
  83. data/lib/aspera/preview/terminal.rb +36 -16
  84. data/lib/aspera/preview/utils.rb +23 -29
  85. data/lib/aspera/proxy_auto_config.rb +6 -3
  86. data/lib/aspera/rest.rb +127 -80
  87. data/lib/aspera/rest_call_error.rb +1 -1
  88. data/lib/aspera/rest_error_analyzer.rb +16 -14
  89. data/lib/aspera/rest_errors_aspera.rb +39 -34
  90. data/lib/aspera/secret_hider.rb +18 -17
  91. data/lib/aspera/ssh.rb +10 -5
  92. data/lib/aspera/temp_file_manager.rb +11 -4
  93. data/lib/aspera/web_auth.rb +10 -7
  94. data/lib/aspera/web_server_simple.rb +11 -5
  95. data.tar.gz.sig +0 -0
  96. metadata +108 -39
  97. metadata.gz.sig +0 -0
  98. data/lib/aspera/cli/listener/line_dump.rb +0 -19
  99. data/lib/aspera/cli/listener/logger.rb +0 -22
  100. data/lib/aspera/cli/listener/progress.rb +0 -50
  101. data/lib/aspera/cli/listener/progress_multi.rb +0 -84
  102. data/lib/aspera/cli/plugins/sync.rb +0 -44
  103. data/lib/aspera/fasp/listener.rb +0 -13
  104. data/lib/aspera/sync.rb +0 -213
@@ -1,32 +1,37 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # cspell:ignore precalc
3
4
  require 'aspera/fasp/agent_base'
4
5
  require 'aspera/fasp/transfer_spec'
5
6
  require 'aspera/node'
6
7
  require 'aspera/log'
7
- require 'tty-spinner'
8
+ require 'aspera/assert'
9
+ require 'aspera/oauth'
8
10
 
9
11
  module Aspera
10
12
  module Fasp
11
13
  # this singleton class is used by the CLI to provide a common interface to start a transfer
12
14
  # before using it, the use must set the `node_api` member.
13
15
  class AgentNode < Aspera::Fasp::AgentBase
16
+ DEFAULT_OPTIONS = {
17
+ url: :required,
18
+ username: :required,
19
+ password: :required,
20
+ root_id: nil
21
+ }.freeze
14
22
  # option include: root_id if the node is an access key
15
- attr_writer :options
23
+ # attr_writer :options
16
24
 
17
- def initialize(options)
18
- raise 'node specification must be Hash' unless options.is_a?(Hash)
19
- %i[url username password].each { |k| raise "missing parameter [#{k}] in node specification: #{options}" unless options.key?(k) }
20
- super()
25
+ def initialize(opts)
26
+ assert_type(opts, Hash){'node agent options'}
27
+ super(opts)
28
+ options = AgentBase.options(default: DEFAULT_OPTIONS, options: opts)
21
29
  # root id is required for access key
22
30
  @root_id = options[:root_id]
23
31
  rest_params = { base_url: options[:url]}
24
- if /^Bearer /.match?(options[:password])
25
- rest_params[:headers] = {
26
- Aspera::Node::HEADER_X_ASPERA_ACCESS_KEY => options[:username],
27
- 'Authorization' => options[:password]
28
- }
32
+ if Oauth.bearer?(options[:password])
29
33
  raise 'root_id is required for access key' if @root_id.nil?
34
+ rest_params[:headers] = Aspera::Node.bearer_headers(options[:password], access_key: options[:username])
30
35
  else
31
36
  rest_params[:auth] = {
32
37
  type: :basic,
@@ -37,6 +42,7 @@ module Aspera
37
42
  @node_api = Rest.new(rest_params)
38
43
  # TODO: currently only supports one transfer. This is bad shortcut. but ok for CLI.
39
44
  @transfer_id = nil
45
+ # Log.log.debug{Log.dump(:agent_options, @options)}
40
46
  end
41
47
 
42
48
  # used internally to ensure node api is set before using.
@@ -45,7 +51,7 @@ module Aspera
45
51
  return @node_api
46
52
  end
47
53
  # use this to read the node_api end point.
48
- attr_reader :node_api
54
+ # attr_reader :node_api
49
55
 
50
56
  # use this to set the node_api end point before using the class.
51
57
  def node_api=(new_value)
@@ -62,7 +68,7 @@ module Aspera
62
68
  case transfer_spec['direction']
63
69
  when Fasp::TransferSpec::DIRECTION_SEND then transfer_spec['source_root_id'] = @root_id
64
70
  when Fasp::TransferSpec::DIRECTION_RECEIVE then transfer_spec['destination_root_id'] = @root_id
65
- else raise "unexpected direction in ts: #{transfer_spec['direction']}"
71
+ else error_unexpected_value(transfer_spec['direction'])
66
72
  end
67
73
  end
68
74
  # manage special additional parameter
@@ -94,43 +100,45 @@ module Aspera
94
100
 
95
101
  # generic method
96
102
  def wait_for_transfers_completion
97
- started = false
98
- spinner = nil
103
+ # set to true when we know the total size of the transfer
104
+ session_started = false
105
+ bytes_expected = nil
99
106
  # lets emulate management events to display progress bar
100
107
  loop do
101
108
  # status is empty sometimes with status 200...
102
- transfer_data = node_api_.read("ops/transfers/#{@transfer_id}")[:data] || {'status' => 'unknown'} rescue {'status' => 'waiting(read error)'}
109
+ transfer_data = node_api_.read("ops/transfers/#{@transfer_id}")[:data] || {'status' => 'unknown'} rescue {'status' => 'waiting(api error)'}
103
110
  case transfer_data['status']
104
- when 'completed'
105
- notify_end(@transfer_id)
106
- break
107
111
  when 'waiting', 'partially_completed', 'unknown', 'waiting(read error)'
108
- if spinner.nil?
109
- spinner = TTY::Spinner.new('[:spinner] :title', format: :classic)
110
- spinner.start
111
- end
112
- spinner.update(title: transfer_data['status'])
113
- spinner.spin
112
+ notify_progress(session_id: nil, type: :pre_start, info: transfer_data['status'])
114
113
  when 'running'
115
- if !started && transfer_data['precalc'].is_a?(Hash) &&
114
+ if !session_started
115
+ notify_progress(session_id: @transfer_id, type: :session_start)
116
+ session_started = true
117
+ end
118
+ message = transfer_data['status']
119
+ message = "#{message} (#{transfer_data['error_desc']})" if !transfer_data['error_desc']&.empty?
120
+ notify_progress(session_id: nil, type: :pre_start, info: message)
121
+ if bytes_expected.nil? &&
122
+ transfer_data['precalc'].is_a?(Hash) &&
116
123
  transfer_data['precalc']['status'].eql?('ready')
117
- notify_begin(@transfer_id, transfer_data['precalc']['bytes_expected'])
118
- started = true
119
- else
120
- notify_progress(@transfer_id, transfer_data['bytes_transferred'])
124
+ bytes_expected = transfer_data['precalc']['bytes_expected']
125
+ notify_progress(type: :session_size, session_id: @transfer_id, info: bytes_expected)
121
126
  end
127
+ notify_progress(type: :transfer, session_id: @transfer_id, info: transfer_data['bytes_transferred'])
128
+ when 'completed'
129
+ notify_progress(type: :transfer, session_id: @transfer_id, info: bytes_expected) if bytes_expected
130
+ notify_progress(type: :end, session_id: @transfer_id)
131
+ break
122
132
  when 'failed'
133
+ notify_progress(type: :end, session_id: @transfer_id)
123
134
  # Bug in HSTS ? transfer is marked failed, but there is no reason
124
- if transfer_data['error_code'].eql?(0) && transfer_data['error_desc'].empty?
125
- notify_end(@transfer_id)
126
- break
127
- end
135
+ break if transfer_data['error_code'].eql?(0) && transfer_data['error_desc'].empty?
128
136
  raise Fasp::Error, "status: #{transfer_data['status']}. code: #{transfer_data['error_code']}. description: #{transfer_data['error_desc']}"
129
137
  else
130
138
  Log.log.warn{"transfer_data -> #{transfer_data}"}
131
139
  raise Fasp::Error, "status: #{transfer_data['status']}. code: #{transfer_data['error_code']}. description: #{transfer_data['error_desc']}"
132
140
  end
133
- sleep(1)
141
+ sleep(1.0)
134
142
  end
135
143
  # TODO: get status of sessions
136
144
  return []
@@ -1,61 +1,121 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # cspell:words Transfersdk
4
-
5
3
  require 'aspera/fasp/agent_base'
6
4
  require 'aspera/fasp/installation'
5
+ require 'aspera/temp_file_manager'
6
+ require 'aspera/log'
7
+ require 'aspera/assert'
7
8
  require 'json'
9
+ require 'uri'
8
10
 
9
11
  module Aspera
10
12
  module Fasp
11
13
  class AgentTrsdk < Aspera::Fasp::AgentBase
14
+ # see https://github.com/grpc/grpc/blob/master/doc/naming.md
15
+ # https://grpc.io/docs/guides/custom-name-resolution/
16
+ LOCAL_SOCKET_ADDR = '127.0.0.1'
17
+ PORT_SEP = ':'
18
+ # port zero means select a random available high port
19
+ AUTO_LOCAL_TCP_PORT = "#{PORT_SEP}0"
12
20
  DEFAULT_OPTIONS = {
13
- address: '127.0.0.1',
14
- port: 55_002
21
+ url: AUTO_LOCAL_TCP_PORT,
22
+ external: false, # expect that an external daemon is already running
23
+ keep: false # do not shutdown daemon on exit
15
24
  }.freeze
16
25
  private_constant :DEFAULT_OPTIONS
17
26
 
18
- # options come from transfer_info
19
- def initialize(user_opts)
20
- raise "expecting Hash (or nil), but have #{user_opts.class}" unless user_opts.nil? || user_opts.is_a?(Hash)
21
- # set default options and override if specified
22
- options = DEFAULT_OPTIONS.dup
23
- user_opts&.each do |k, v|
24
- raise "Unknown local agent parameter: #{k}, expect one of #{DEFAULT_OPTIONS.keys.map(&:to_s).join(',')}" unless DEFAULT_OPTIONS.key?(k)
25
- options[k] = v
27
+ class << self
28
+ # Well, the port number is only in log file
29
+ def daemon_port_from_log(log_file)
30
+ result = nil
31
+ # if port is zero, a dynamic port was created, get it
32
+ File.open(log_file, 'r') do |file|
33
+ file.each_line do |line|
34
+ # Well, it's tricky to depend on log
35
+ if (m = line.match(/Info: API Server: Listening on ([^:]+):(\d+) /))
36
+ result = m[2].to_i
37
+ # no "break" , need to read last matching log line
38
+ end
39
+ end
40
+ end
41
+ raise 'Port not found in daemon logs' if result.nil?
42
+ Log.log.debug{"Got port #{result} from log"}
43
+ return result
26
44
  end
27
- Log.log.debug{"options= #{options}"}
28
- super()
29
- # load and create SDK stub
45
+ end
46
+
47
+ # options come from transfer_info
48
+ def initialize(user_opts={})
49
+ super(user_opts)
50
+ @options = AgentBase.options(default: DEFAULT_OPTIONS, options: user_opts)
51
+ is_local_auto_port = @options[:url].eql?(AUTO_LOCAL_TCP_PORT)
52
+ raise 'Cannot use options `keep` or `external` with port zero' if is_local_auto_port && (@options[:keep] || @options[:external])
53
+ Log.log.debug{Log.dump(:agent_options, @options)}
54
+ # load SDK stub class on demand, as it's an optional gem
30
55
  $LOAD_PATH.unshift(Installation.instance.sdk_ruby_folder)
31
56
  require 'transfer_services_pb'
32
- @transfer_client = Transfersdk::TransferService::Stub.new("#{options[:address]}:#{options[:port]}", :this_channel_is_insecure)
57
+ # keep PID for optional shutdown
58
+ @daemon_pid = nil
59
+ daemon_endpoint = @options[:url]
60
+ Log.log.debug{Log.dump(:daemon_endpoint, daemon_endpoint)}
61
+ # retry loop
33
62
  begin
63
+ # no address: local bind
64
+ daemon_endpoint = "#{LOCAL_SOCKET_ADDR}#{daemon_endpoint}" if daemon_endpoint.match?(/^#{PORT_SEP}[0-9]+$/o)
65
+ # Create stub (without credentials)
66
+ @transfer_client = Transfersdk::TransferService::Stub.new(daemon_endpoint, :this_channel_is_insecure)
67
+ # Initiate actual connection
34
68
  get_info_response = @transfer_client.get_info(Transfersdk::InstanceInfoRequest.new)
35
- Log.log.debug{"daemon info: #{get_info_response}"}
36
- rescue GRPC::Unavailable
37
- Log.log.warn('no daemon present, starting daemon...')
69
+ Log.log.debug{"Daemon info: #{get_info_response}"}
70
+ Log.log.warn{'Attached to existing daemon'} unless @daemon_pid || @options[:external] || @options[:keep]
71
+ at_exit{shutdown}
72
+ rescue GRPC::Unavailable => e
73
+ # if transferd is external: do not start it, or other error
74
+ raise if @options[:external] || !e.message.include?('failed to connect')
75
+ # we already tried to start a daemon, but it failed
76
+ assert(@daemon_pid.nil?){"Daemon started with PID #{@daemon_pid}, but connection failed to #{daemon_endpoint}}"}
77
+ Log.log.warn('no daemon present, starting daemon...') if @options[:external]
38
78
  # location of daemon binary
39
- bin_folder = File.realpath(File.join(Installation.instance.sdk_ruby_folder, '..'))
40
- # config file and logs are created in same folder
41
- conf_file = File.join(bin_folder, 'sdk.conf')
42
- log_base = File.join(bin_folder, 'transferd')
79
+ sdk_folder = File.realpath(File.join(Installation.instance.sdk_ruby_folder, '..'))
80
+ # transferd only supports local ip and port
81
+ daemon_uri = URI.parse("ipv4://#{daemon_endpoint}")
82
+ assert(daemon_uri.scheme.eql?('ipv4')){"Invalid scheme daemon URI #{daemon_endpoint}"}
43
83
  # create a config file for daemon
44
84
  config = {
45
- address: options[:address],
46
- port: options[:port],
85
+ address: daemon_uri.host,
86
+ port: daemon_uri.port,
47
87
  fasp_runtime: {
48
88
  use_embedded: false,
49
89
  user_defined: {
50
- bin: bin_folder,
51
- etc: bin_folder
90
+ bin: sdk_folder,
91
+ etc: sdk_folder
52
92
  }
53
93
  }
54
94
  }
95
+ # config file and logs are created in same folder
96
+ transferd_base_tmp = TempFileManager.instance.new_file_path_global('transferd')
97
+ Log.log.debug{"transferd base tmp #{transferd_base_tmp}"}
98
+ conf_file = "#{transferd_base_tmp}.conf"
99
+ log_stdout = "#{transferd_base_tmp}.out"
100
+ log_stderr = "#{transferd_base_tmp}.err"
55
101
  File.write(conf_file, config.to_json)
56
- trd_pid = Process.spawn(Installation.instance.path(:transferd), '--config', conf_file, out: "#{log_base}.out", err: "#{log_base}.err")
57
- Process.detach(trd_pid)
58
- sleep(2.0)
102
+ @daemon_pid = Process.spawn(Installation.instance.path(:transferd), '--config', conf_file, out: log_stdout, err: log_stderr)
103
+ begin
104
+ # wait for process to initialize, max 2 seconds
105
+ Timeout.timeout(2.0) do
106
+ # this returns if process dies (within 2 seconds)
107
+ _, status = Process.wait2(@daemon_pid)
108
+ raise "Transfer daemon exited with status #{status.exitstatus}. Check files: #{log_stdout} and #{log_stderr}"
109
+ end
110
+ rescue Timeout::Error
111
+ nil
112
+ end
113
+ Log.log.debug{"Daemon started with pid #{@daemon_pid}"}
114
+ Process.detach(@daemon_pid) if @options[:keep]
115
+ at_exit {shutdown}
116
+ # update port for next connection attempt (if auto high port was requested)
117
+ daemon_endpoint = "#{LOCAL_SOCKET_ADDR}#{PORT_SEP}#{self.class.daemon_port_from_log(log_stdout)}" if is_local_auto_port
118
+ # local daemon started, try again
59
119
  retry
60
120
  end
61
121
  end
@@ -74,25 +134,34 @@ module Aspera
74
134
  end
75
135
 
76
136
  def wait_for_transfers_completion
77
- started = false
137
+ # set to true when we know the total size of the transfer
138
+ session_started = false
139
+ bytes_expected = nil
78
140
  # monitor transfer status
79
141
  @transfer_client.monitor_transfers(Transfersdk::RegistrationRequest.new(transferId: [@transfer_id])) do |response|
80
- Log.dump(:response, response.to_h)
142
+ Log.log.debug{Log.dump(:response, response.to_h)}
81
143
  # Log.log.debug{"#{response.sessionInfo.preTransferBytes} #{response.transferInfo.bytesTransferred}"}
82
144
  case response.status
83
145
  when :RUNNING
84
- if !started && !response.sessionInfo.preTransferBytes.eql?(0)
85
- notify_begin(@transfer_id, response.sessionInfo.preTransferBytes)
86
- started = true
87
- elsif started
88
- notify_progress(@transfer_id, response.transferInfo.bytesTransferred)
146
+ if !session_started
147
+ notify_progress(session_id: @transfer_id, type: :session_start)
148
+ session_started = true
89
149
  end
90
- when :FAILED, :COMPLETED, :CANCELED
91
- notify_end(@transfer_id)
92
- raise Fasp::Error, JSON.parse(response.message)['Description'] unless :COMPLETED.eql?(response.status)
150
+ if bytes_expected.nil? &&
151
+ !response.sessionInfo.preTransferBytes.eql?(0)
152
+ bytes_expected = response.sessionInfo.preTransferBytes
153
+ notify_progress(type: :session_size, session_id: @transfer_id, info: bytes_expected)
154
+ end
155
+ notify_progress(type: :transfer, session_id: @transfer_id, info: response.transferInfo.bytesTransferred)
156
+ when :COMPLETED
157
+ notify_progress(type: :transfer, session_id: @transfer_id, info: bytes_expected) if bytes_expected
158
+ notify_progress(type: :end, session_id: @transfer_id)
93
159
  break
160
+ when :FAILED, :CANCELED
161
+ notify_progress(type: :end, session_id: @transfer_id)
162
+ raise Fasp::Error, JSON.parse(response.message)['Description']
94
163
  when :QUEUED, :UNKNOWN_STATUS, :PAUSED, :ORPHANED
95
- # ignore
164
+ notify_progress(session_id: nil, type: :pre_start, info: response.status.to_s.downcase)
96
165
  else
97
166
  Log.log.error{"unknown status#{response.status}"}
98
167
  end
@@ -100,6 +169,20 @@ module Aspera
100
169
  # TODO: return status
101
170
  return []
102
171
  end
172
+
173
+ def shutdown
174
+ stop_daemon unless @options[:keep]
175
+ end
176
+
177
+ def stop_daemon
178
+ if !@daemon_pid.nil?
179
+ Log.log.debug("Stopping daemon #{@daemon_pid}")
180
+ Process.kill('INT', @daemon_pid)
181
+ _, status = Process.wait2(@daemon_pid)
182
+ Log.log.debug("daemon stopped #{status}")
183
+ @daemon_pid = nil
184
+ end
185
+ end
103
186
  end
104
187
  end
105
188
  end
@@ -5,11 +5,11 @@
5
5
  module Aspera
6
6
  module Fasp
7
7
  # from https://www.google.com/search?q=FASP+error+codes
8
- # Note that the fact that an error is retryable is not internally defined by protocol, it's client-side responsibility
8
+ # Note that the fact that an error is retry-able is not internally defined by protocol, it's client-side responsibility
9
9
  # rubocop:disable Layout/MultilineHashKeyLineBreaks
10
10
  # rubocop:disable Layout/FirstHashElementLineBreak
11
11
  ERROR_INFO = {
12
- # id retryable mnemo message additional info
12
+ # id retry-able mnemo message additional info
13
13
  1 => { r: false, c: 'FASP_PROTO', m: 'Generic fasp(tm) protocol error', a: 'fasp(tm) error'},
14
14
  2 => { r: false, c: 'ASCP', m: 'Generic SCP error', a: 'ASCP error'},
15
15
  3 => { r: false, c: 'AMBIGUOUS_TARGET', m: 'Target incorrectly specified', a: 'Ambiguous target'},
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aspera
4
+ module Fasp
5
+ # generates a pseudo file stream
6
+ class FauxFile
7
+ # marker for faux file
8
+ PREFIX = 'faux:///'
9
+ # size suffix
10
+ SUFFIX = %w[k m g t p e]
11
+ class << self
12
+ def open(name)
13
+ return nil unless name.start_with?(PREFIX)
14
+ parts = name[PREFIX.length..-1].split('?')
15
+ raise 'Format: #{PREFIX}<file path>?<size>' unless parts.length.eql?(2)
16
+ raise "Format: <integer>[#{SUFFIX.join(',')}]" unless (m = parts[1].downcase.match(/^(\d+)([#{SUFFIX.join('')}])$/))
17
+ size = m[1].to_i
18
+ suffix = m[2]
19
+ SUFFIX.each do |s|
20
+ size *= 1024
21
+ break if s.eql?(suffix)
22
+ end
23
+ return FauxFile.new(parts[0], size)
24
+ end
25
+ end
26
+ attr_reader :path, :size
27
+
28
+ def initialize(path, size)
29
+ @path = path
30
+ @size = size
31
+ @offset = 0
32
+ # we cache large chunks, anyway most of them will be the same size
33
+ @chunk_by_size = {}
34
+ end
35
+
36
+ def read(chunk_size)
37
+ return nil if eof?
38
+ bytes_to_read = [chunk_size, @size - @offset].min
39
+ @offset += bytes_to_read
40
+ @chunk_by_size[bytes_to_read] = "\x00" * bytes_to_read unless @chunk_by_size.key?(bytes_to_read)
41
+ return @chunk_by_size[bytes_to_read]
42
+ end
43
+
44
+ def close
45
+ end
46
+
47
+ def eof?
48
+ return @offset >= @size
49
+ end
50
+ end
51
+ end
52
+ end