aspera-cli 4.14.0 → 4.16.0

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 (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