aspera-cli 4.9.0 → 4.10.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.
@@ -85,10 +85,28 @@ module Aspera
85
85
  if force || !File.exist?(path)
86
86
  File.unlink(path) rescue nil # Windows may give error
87
87
  File.write(path,yield)
88
- File.chmod(0400,path)
88
+ restrict_file_access(path)
89
89
  end
90
90
  return path
91
91
  end
92
+
93
+ def restrict_file_access(path,mode: nil)
94
+ begin
95
+ if mode.nil?
96
+ # or FileUtils ?
97
+ if File.file?(path)
98
+ mode=0600
99
+ elsif File.directory?(path)
100
+ mode=0700
101
+ else
102
+ Log.log.debug("No restriction can be set for #{path}");
103
+ end
104
+ end
105
+ File.chmod(mode,path) unless mode.nil?
106
+ rescue => e
107
+ Log.log.warn(e.message)
108
+ end
109
+ end
92
110
  end
93
111
  end
94
112
  end
@@ -272,6 +272,10 @@ module Aspera
272
272
  env_args[:env]['ASPERA_SCP_TOKEN'] = session[:options][:regenerate_token].call(true)
273
273
  end
274
274
  end
275
+ # cannot resolve address
276
+ #if last_status_event['Code'].to_i.eql?(14)
277
+ # Log.log.warn("host: #{}")
278
+ #end
275
279
  raise Fasp::Error.new(last_status_event['Description'],last_status_event['Code'].to_i)
276
280
  else # case
277
281
  raise "unexpected last event type: #{last_status_event['Type']}"
@@ -4,8 +4,8 @@ require 'aspera/fasp/agent_base'
4
4
  require 'aspera/fasp/transfer_spec'
5
5
  require 'aspera/log'
6
6
  require 'aspera/rest'
7
- require 'websocket-client-simple'
8
7
  require 'securerandom'
8
+ require 'websocket'
9
9
  require 'base64'
10
10
  require 'json'
11
11
 
@@ -13,16 +13,32 @@ require 'json'
13
13
  # https://developer.ibm.com/apis/catalog?search=%22aspera%20http%22
14
14
  module Aspera
15
15
  module Fasp
16
- # start a transfer using Aspera HTTP Gateway, using web socket session
16
+ # start a transfer using Aspera HTTP Gateway, using web socket session for uploads
17
17
  class AgentHttpgw < AgentBase
18
18
  # message returned by HTTP GW in case of success
19
- OK_MESSAGE = 'end upload'
20
- # refresh rate for progress
21
- UPLOAD_REFRESH_SEC = 0.5
22
- private_constant :OK_MESSAGE,:UPLOAD_REFRESH_SEC
19
+ MSG_END_UPLOAD = 'end upload'
20
+ MSG_END_SLICE = 'end_slice_upload'
21
+ DEFAULT_OPTIONS = {
22
+ url: nil,
23
+ upload_chunksize: 64_000,
24
+ upload_bar_refresh_sec: 0.5
25
+ }.freeze
26
+ DEFAULT_BASE_PATH='/aspera/http-gwy'
27
+ # upload endpoints
28
+ V1_UPLOAD='/v1/upload'
29
+ V2_UPLOAD='/v2/upload'
30
+ private_constant :DEFAULT_OPTIONS,:MSG_END_UPLOAD,:MSG_END_SLICE,:V1_UPLOAD,:V2_UPLOAD
31
+
23
32
  # send message on http gw web socket
24
- def ws_send(ws,type,data)
25
- ws.send(JSON.generate({type => data}))
33
+ def ws_snd_json(data)
34
+ @slice_uploads += 1 if data.has_key?(:slice_upload)
35
+ Log.log.debug{JSON.generate(data)}
36
+ ws_send(JSON.generate(data))
37
+ end
38
+
39
+ def ws_send(data, type: :text)
40
+ frame = ::WebSocket::Frame::Outgoing::Client.new(data: data, type: type, version: @ws_handshake.version)
41
+ @ws_io.write(frame.to_s)
26
42
  end
27
43
 
28
44
  def upload(transfer_spec)
@@ -48,93 +64,129 @@ module Aspera
48
64
  # save so that we can actually read the file later
49
65
  source_paths.push(full_src_filepath)
50
66
  end
51
-
67
+ # identify this session uniquely
52
68
  session_id = SecureRandom.uuid
53
- ws = ::WebSocket::Client::Simple::Client.new
54
- # error message if any in callback
55
- error = nil
56
- # number of files totally sent
57
- received = 0
58
- # setup callbacks on websocket
59
- ws.on(:message) do |msg|
60
- Log.log.info("ws: message: #{msg.data}")
61
- message = msg.data
62
- if message.eql?(OK_MESSAGE)
63
- received += 1
64
- else
65
- message.chomp!
66
- error =
67
- if message.start_with?('"') && message.end_with?('"')
68
- JSON.parse(Base64.strict_decode64(message.chomp[1..-2]))['message']
69
- else
70
- "expecting quotes in [#{message}]"
69
+ @slice_uploads=0
70
+ # web socket endpoint: by default use v2 (newer gateways), without base64 encoding
71
+ upload_api_version = V2_UPLOAD
72
+ # is the latest supported? else revert to old api
73
+ upload_api_version=V1_UPLOAD unless @api_info['endpoints'].any?{|i|i.include?(upload_api_version)}
74
+ Log.log.debug{"api version: #{upload_api_version}"}
75
+ url=File.join(@gw_api.params[:base_url],upload_api_version)
76
+ #uri = URI.parse(url)
77
+ # open web socket to end point (equivalent to Net::HTTP.start)
78
+ http_socket = Rest.start_http_session(url)
79
+ @ws_io = http_socket.instance_variable_get(:@socket)
80
+ #@ws_io.debug_output = Log.log
81
+ @ws_handshake = ::WebSocket::Handshake::Client.new(url: url, headers: {})
82
+ @ws_io.write(@ws_handshake.to_s)
83
+ sleep(0.1)
84
+ @ws_handshake << @ws_io.readuntil("\r\n\r\n")
85
+ raise 'Error in websocket handshake' unless @ws_handshake.finished?
86
+ Log.log.debug('ws: handshake success')
87
+ # data shared between main thread and read thread
88
+ shared_info={
89
+ read_exception: nil, # error message if any in callback
90
+ end_uploads: 0 # number of files totally sent
91
+ #mutex: Mutex.new
92
+ #cond_var: ConditionVariable.new
93
+ }
94
+ # start read thread
95
+ ws_read_thread = Thread.new do
96
+ Log.log.debug('ws: thread: started')
97
+ frame = ::WebSocket::Frame::Incoming::Client.new
98
+ loop do
99
+ begin
100
+ frame << @ws_io.readuntil("\n")
101
+ while (msg = frame.next)
102
+ Log.log.debug("ws: thread: message: #{msg.data} #{shared_info[:end_uploads]}")
103
+ message = msg.data
104
+ if message.eql?(MSG_END_UPLOAD)
105
+ shared_info[:end_uploads] += 1
106
+ elsif message.eql?(MSG_END_SLICE)
107
+ else
108
+ message.chomp!
109
+ error_message =
110
+ if message.start_with?('"') && message.end_with?('"')
111
+ JSON.parse(Base64.strict_decode64(message.chomp[1..-2]))['message']
112
+ elsif message.start_with?('{') && message.end_with?('}')
113
+ JSON.parse(message)['message']
114
+ else
115
+ "unknown message from gateway: [#{message}]"
116
+ end
117
+ raise error_message
118
+ end
71
119
  end
120
+ rescue => e
121
+ shared_info[:read_exception] = e unless e.is_a?(EOFError)
122
+ break
123
+ end
72
124
  end
73
- end
74
- ws.on(:error) do |e|
75
- error = e
76
- end
77
- ws.on(:open) do
78
- Log.log.info('ws: open')
79
- end
80
- ws.on(:close) do
81
- Log.log.info('ws: close')
82
- end
83
- # open web socket to end point
84
- ws.connect("#{@gw_api.params[:base_url]}/upload")
85
- # async wait ready
86
- while !ws.open? && error.nil?
87
- Log.log.info('ws: wait')
88
- sleep(0.2)
125
+ Log.log.debug("ws: thread: stopping #{shared_info[:read_exception]} #{shared_info[:read_exception].class}")
89
126
  end
90
127
  # notify progress bar
91
128
  notify_begin(session_id,total_size)
92
129
  # first step send transfer spec
93
130
  Log.dump(:ws_spec,transfer_spec)
94
- ws_send(ws,:transfer_spec,transfer_spec)
131
+ ws_snd_json(transfer_spec: transfer_spec)
95
132
  # current file index
96
133
  file_index = 0
97
134
  # aggregate size sent
98
135
  sent_bytes = 0
99
136
  # last progress event
100
- lastevent = nil
101
- transfer_spec['paths'].each do |item|
102
- # TODO: get mime type?
103
- file_mime_type = ''
104
- file_size = item['file_size']
105
- file_name = File.basename(item[item['destination'].nil? ? 'source' : 'destination'])
106
- # compute total number of slices
107
- numslices = 1 + ((file_size - 1) / @upload_chunksize)
108
- File.open(source_paths[file_index]) do |file|
109
- # current slice index
110
- slicenum = 0
111
- while !file.eof?
112
- data = file.read(@upload_chunksize)
113
- slice_data = {
114
- name: file_name,
115
- type: file_mime_type,
116
- size: file_size,
117
- data: Base64.strict_encode64(data),
118
- slice: slicenum,
119
- total_slices: numslices,
120
- fileIndex: file_index
121
- }
122
- # log without data
123
- Log.dump(:slide_data,slice_data.keys.each_with_object({}){|i,m|m[i] = i.eql?(:data) ? 'base64 data' : slice_data[i];}) if slicenum.eql?(0)
124
- ws_send(ws,:slice_upload, slice_data)
125
- sent_bytes += data.length
126
- currenttime = Time.now
127
- if lastevent.nil? || ((currenttime - lastevent) > UPLOAD_REFRESH_SEC)
128
- notify_progress(session_id,sent_bytes)
129
- lastevent = currenttime
137
+ last_progress_time = nil
138
+ begin
139
+ transfer_spec['paths'].each do |item|
140
+ # TODO: get mime type?
141
+ file_mime_type = ''
142
+ file_size = item['file_size']
143
+ file_name = File.basename(item[item['destination'].nil? ? 'source' : 'destination'])
144
+ # compute total number of slices
145
+ numslices = 1 + ((file_size - 1) / @options[:upload_chunksize])
146
+ File.open(source_paths[file_index]) do |file|
147
+ # current slice index
148
+ slicenum = 0
149
+ while !file.eof?
150
+ # interrupt main thread if read thread failed
151
+ raise shared_info[:read_exception] unless shared_info[:read_exception].nil?
152
+ data = file.read(@options[:upload_chunksize])
153
+ slice_data = {
154
+ name: file_name,
155
+ type: file_mime_type,
156
+ size: file_size,
157
+ slice: slicenum,
158
+ total_slices: numslices,
159
+ fileIndex: file_index
160
+ }
161
+ #Log.dump(:slice_data,slice_data) #if slicenum.eql?(0)
162
+ if upload_api_version.eql?(V1_UPLOAD)
163
+ slice_data[:data] = Base64.strict_encode64(data)
164
+ ws_snd_json(slice_upload: slice_data)
165
+ else
166
+ ws_snd_json(slice_upload: slice_data) if slicenum.eql?(0)
167
+ ws_send(data,type: :binary)
168
+ Log.log.debug{"ws: sent buffer: #{file_index} / #{slicenum}"}
169
+ ws_snd_json(slice_upload: slice_data) if slicenum.eql?(numslices-1)
170
+ end
171
+ sent_bytes += data.length
172
+ currenttime = Time.now
173
+ if last_progress_time.nil? || ((currenttime - last_progress_time) > @options[:upload_bar_refresh_sec])
174
+ notify_progress(session_id,sent_bytes)
175
+ last_progress_time = currenttime
176
+ end
177
+ slicenum += 1
130
178
  end
131
- slicenum += 1
132
- raise error unless error.nil?
133
179
  end
180
+ file_index += 1
134
181
  end
135
- file_index += 1
136
182
  end
137
- ws.close
183
+ Log.log.debug('Finished upload')
184
+ ws_read_thread.join
185
+ Log.log.debug{"result: #{shared_info[:end_uploads]} / #{@slice_uploads}"}
186
+ ws_send(nil, type: :close) unless @ws_io.nil?
187
+ @ws_io = nil
188
+ http_socket&.finish
189
+ notify_progress(session_id,sent_bytes)
138
190
  notify_end(session_id)
139
191
  end
140
192
 
@@ -153,7 +205,7 @@ module Aspera
153
205
  end
154
206
  transfer_spec['download_name'] = dname
155
207
  end
156
- creation = @gw_api.create('download',{'transfer_spec' => transfer_spec})[:data]
208
+ creation = @gw_api.create('v1/download',{'transfer_spec' => transfer_spec})[:data]
157
209
  transfer_uuid = creation['url'].split('/').last
158
210
  file_dest =
159
211
  if transfer_spec['zip_required'] || transfer_spec['paths'].length > 1
@@ -164,7 +216,7 @@ module Aspera
164
216
  File.basename(transfer_spec['paths'].first['source'])
165
217
  end
166
218
  file_dest = File.join(transfer_spec['destination_root'],file_dest)
167
- @gw_api.call({operation: 'GET',subpath: "download/#{transfer_uuid}",save_to_file: file_dest})
219
+ @gw_api.call({operation: 'GET',subpath: "v1/download/#{transfer_uuid}",save_to_file: file_dest})
168
220
  end
169
221
 
170
222
  # start FASP transfer based on transfer spec (hash table)
@@ -200,15 +252,22 @@ module Aspera
200
252
 
201
253
  private
202
254
 
203
- def initialize(params)
204
- raise 'params must be Hash' unless params.is_a?(Hash)
205
- params = params.symbolize_keys
206
- raise 'must have only one param: url' unless params.keys.eql?([:url])
255
+ def initialize(opts)
256
+ Log.log.debug("local options= #{opts}")
257
+ # set default options and override if specified
258
+ @options = DEFAULT_OPTIONS.dup
259
+ raise "httpgw agent parameters (transfer_info): expecting Hash, but have #{opts.class}" unless opts.is_a?(Hash)
260
+ opts.symbolize_keys.each do |k,v|
261
+ raise "httpgw agent parameter: Unknown: #{k}, expect one of #{DEFAULT_OPTIONS.keys.map(&:to_s).join(',')}" unless DEFAULT_OPTIONS.has_key?(k)
262
+ @options[k] = v
263
+ end
264
+ raise 'missing param: url' if @options[:url].nil?
265
+ # remove /v1 from end
266
+ @options[:url].gsub(%r{/v1/*$},'')
207
267
  super()
208
- @gw_api = Rest.new({base_url: params[:url]})
209
- api_info = @gw_api.read('info')[:data]
210
- Log.log.info(api_info.to_s)
211
- @upload_chunksize = 128_000 # TODO: configurable ?
268
+ @gw_api = Rest.new({base_url: @options[:url]})
269
+ @api_info = @gw_api.read('v1/info')[:data]
270
+ Log.log.info(@api_info.to_s)
212
271
  end
213
272
  end # AgentHttpgw
214
273
  end
@@ -219,7 +219,7 @@ module Aspera
219
219
  return nil unless File.exist?(exe_path)
220
220
  exe_version = nil
221
221
  cmd_out = %x("#{exe_path}" #{vers_arg})
222
- raise "An error occured when testing #{ascp_filename}: #{cmd_out}" unless $CHILD_STATUS == 0
222
+ raise "An error occurred when testing #{ascp_filename}: #{cmd_out}" unless $CHILD_STATUS == 0
223
223
  # get version from ascp, only after full extract, as windows requires DLLs (SSL/TLS/etc...)
224
224
  m = cmd_out.match(/ version ([0-9.]+)/)
225
225
  exe_version = m[1] unless m.nil?
@@ -269,12 +269,12 @@ module Aspera
269
269
  path(:aspera_conf)
270
270
  ascp_path = File.join(sdk_folder,ascp_filename)
271
271
  raise "No #{ascp_filename} found in SDK archive" unless File.exist?(ascp_path)
272
- FileUtils.chmod(0755,ascp_path)
273
- FileUtils.chmod(0755,ascp_path.gsub('ascp','ascp4'))
272
+ Environment.restrict_file_access(ascp_path, mode: 0755)
273
+ Environment.restrict_file_access(ascp_path.gsub('ascp','ascp4'), mode: 0755)
274
274
  ascp_version = get_ascp_version(File.join(sdk_folder,ascp_filename))
275
275
  trd_path = transferd_filepath
276
276
  Log.log.warn("No #{trd_path} in SDK archive") unless File.exist?(trd_path)
277
- FileUtils.chmod(0755,trd_path) if File.exist?(trd_path)
277
+ Environment.restrict_file_access(trd_path, mode: 0755) if File.exist?(trd_path)
278
278
  transferd_version = get_exe_version(trd_path,'version')
279
279
  sdk_version = transferd_version || ascp_version
280
280
  File.write(File.join(sdk_folder,PRODUCT_INFO),"<product><name>IBM Aspera SDK</name><version>#{sdk_version}</version></product>")
@@ -51,7 +51,19 @@ module Aspera
51
51
  param[:cli] =
52
52
  case i[:cltype]
53
53
  when :envvar then 'env:' + i[:clvarname]
54
- when :opt_without_arg,:opt_with_arg then i[:clswitch]
54
+ when :opt_without_arg then i[:clswitch]
55
+ when :opt_with_arg
56
+ values=if i.has_key?(:enum)
57
+ ['enum']
58
+ elsif i[:accepted_types].is_a?(Array)
59
+ i[:accepted_types]
60
+ elsif !i[:accepted_types].nil?
61
+ [i[:accepted_types]]
62
+ else
63
+ raise "error: #{param}"
64
+ end.map{|n|"{#{n}}"}.join('|')
65
+ conv=i.has_key?(:clconvert) ? '(conversion)' : ''
66
+ "#{i[:clswitch]} #{conv}#{values}"
55
67
  else ''
56
68
  end
57
69
  if i.has_key?(:enum)
@@ -4,7 +4,7 @@
4
4
  # enum : set with list of values for enum types accepted in transfer spec
5
5
  # tragents : supported agents (for doc only)
6
6
  # required : optional, default: false
7
- # cltype : ascp: type of parameter
7
+ # cltype : ascp: type of parameter: :opt_with_arg,:opt_without_arg,:envvar,:defer,:ignore,
8
8
  # clswitch : ascp: switch for ascp command line
9
9
  # clconvert : ascp: transform value: either a Hash with conversion values, or name of class
10
10
  # clvarname : ascp: name of env var
@@ -144,8 +144,11 @@ https_fallback_port:
144
144
  :cltype: :opt_with_arg
145
145
  :clswitch: "-t"
146
146
  move_after_transfer:
147
- :desc: The relative path to which the files will be moved after the transfer at the source side.
147
+ :desc: The relative path to which the files will be moved after the transfer at the source side. Available as of 3.8.0.
148
148
  :cltype: :opt_with_arg
149
+ :tragents:
150
+ - :direct
151
+ - :node
149
152
  multi_session:
150
153
  :desc: |
151
154
  Use multi-session transfer. max 128.
@@ -45,7 +45,7 @@ module Aspera
45
45
  yield
46
46
  break
47
47
  rescue Fasp::Error => e
48
- Log.log.warn("An error occured: #{e.message}");
48
+ Log.log.warn("An error occurred: #{e.message}");
49
49
  # failure in ascp
50
50
  if e.retryable?
51
51
  # exit if we exceed the max number of retry
@@ -1,134 +1,72 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'aspera/hash_ext'
4
- require 'openssl'
4
+ require 'symmetric_encryption/core'
5
+ require 'yaml'
5
6
 
6
7
  module Aspera
7
8
  module Keychain
8
- class SimpleCipher
9
- def initialize(key)
10
- @key = Digest::SHA1.hexdigest(key+('*'*23))[0..23]
11
- @cipher = OpenSSL::Cipher.new('DES-EDE3-CBC')
12
- end
13
-
14
- def encrypt(value)
15
- @cipher.encrypt
16
- @cipher.key = @key
17
- s = @cipher.update(value) + @cipher.final
18
- s.unpack1('H*')
19
- end
20
-
21
- def decrypt(value)
22
- @cipher.decrypt
23
- @cipher.key = @key
24
- s = [value].pack('H*').unpack('C*').pack('c*')
25
- @cipher.update(s) + @cipher.final
26
- end
27
- end
28
-
29
9
  # Manage secrets in a simple Hash
30
10
  class EncryptedHash
31
- SEPARATOR = '%'
32
- ACCEPTED_KEYS = %i[username url secret description].freeze
33
- private_constant :SEPARATOR
34
- attr_reader :legacy_detected
35
- def initialize(values)
36
- raise 'values shall be Hash' unless values.is_a?(Hash)
37
- @all_secrets = values
11
+ CIPHER_NAME='aes-256-cbc'
12
+ ACCEPTED_KEYS = %i[label username password url description].freeze
13
+ def initialize(path,current_password)
14
+ @path=path
15
+ self.password=current_password
16
+ raise 'path to vault file shall be String' unless @path.is_a?(String)
17
+ @all_secrets=File.exist?(@path) ? YAML.load_stream(@cipher.decrypt(File.read(@path))).first : {}
38
18
  end
39
19
 
40
- def identifier(options)
41
- return options[:username] if options[:url].to_s.empty?
42
- %i[url username].map{|s|options[s]}.join(SEPARATOR)
20
+ def password=(new_password)
21
+ key_bytes=CIPHER_NAME.split('-')[1].to_i/8
22
+ # derive key from passphrase
23
+ key="#{new_password}#{"\x0"*key_bytes}"[0..(key_bytes-1)]
24
+ Log.log.debug("key=[#{key}],#{key.length}")
25
+ SymmetricEncryption.cipher=@cipher = SymmetricEncryption::Cipher.new(cipher_name: CIPHER_NAME,key: key,encoding: :none)
26
+ end
27
+
28
+ def save
29
+ File.write(@path, @cipher.encrypt(YAML.dump(@all_secrets)),encoding: 'BINARY')
43
30
  end
44
31
 
45
32
  def set(options)
46
33
  raise 'options shall be Hash' unless options.is_a?(Hash)
47
34
  unsupported = options.keys - ACCEPTED_KEYS
48
35
  raise "unsupported options: #{unsupported}" unless unsupported.empty?
49
- username = options[:username]
50
- raise 'options shall have username' if username.nil?
51
- url = options[:url]
52
- raise 'options shall have username' if url.nil?
53
- secret = options[:secret]
54
- raise 'options shall have secret' if secret.nil?
55
- key = identifier(options)
56
- raise "secret #{key} already exist, delete first" if @all_secrets.has_key?(key)
57
- obj = {username: username, url: url, secret: SimpleCipher.new(key).encrypt(secret)}
58
- obj[:description] = options[:description] if options.has_key?(:description)
59
- @all_secrets[key] = obj.stringify_keys
60
- nil
36
+ label = options.delete(:label)
37
+ raise "secret #{label} already exist, delete first" if @all_secrets.has_key?(label)
38
+ @all_secrets[label] = options.symbolize_keys
39
+ save
61
40
  end
62
41
 
63
42
  def list
64
43
  result = []
65
- legacy_detected=false
66
- @all_secrets.each do |name,value|
67
- normal = # normalized version
68
- case value
69
- when String
70
- legacy_detected=true
71
- {username: name, url: '', secret: value}
72
- when Hash then value.symbolize_keys
73
- else raise 'error secret must be String (legacy) or Hash (new)'
74
- end
75
- normal[:description] = '' unless normal.has_key?(:description)
76
- extraneous_keys=normal.keys - ACCEPTED_KEYS
77
- Log.log.error("wrongs keys in secret hash: #{extraneous_keys.map(&:to_s).join(',')}") unless extraneous_keys.empty?
44
+ @all_secrets.each do |label,values|
45
+ normal = values.symbolize_keys
46
+ normal[:label] = label
47
+ ACCEPTED_KEYS.each{|k|normal[k] = '' unless normal.has_key?(k)}
78
48
  result.push(normal)
79
49
  end
80
- Log.log.warn('Legacy vault format detected in config file, please refer to documentation to convert to new format.') if legacy_detected
81
50
  return result
82
51
  end
83
52
 
84
53
  def delete(options)
85
54
  raise 'options shall be Hash' unless options.is_a?(Hash)
86
- unsupported = options.keys - %i[username url]
55
+ unsupported = options.keys - %i[label]
87
56
  raise "unsupported options: #{unsupported}" unless unsupported.empty?
88
- username = options[:username]
89
- raise 'options shall have username' if username.nil?
90
- url = options[:url]
91
- key = nil
92
- if !url.nil?
93
- extk = identifier(options)
94
- key = extk if @all_secrets.has_key?(extk)
95
- end
96
- # backward compatibility: TODO: remove in future ? (make url mandatory ?)
97
- key = username if key.nil? && @all_secrets.has_key?(username)
98
- raise 'no such secret' if key.nil?
99
- @all_secrets.delete(key)
57
+ label=options[:label]
58
+ @all_secrets.delete(label)
59
+ save
100
60
  end
101
61
 
102
62
  def get(options)
103
63
  raise 'options shall be Hash' unless options.is_a?(Hash)
104
- unsupported = options.keys - %i[username url]
64
+ unsupported = options.keys - %i[label]
105
65
  raise "unsupported options: #{unsupported}" unless unsupported.empty?
106
- username = options[:username]
107
- raise 'options shall have username' if username.nil?
108
- url = options[:url]
109
- info = nil
110
- if !url.nil?
111
- info = @all_secrets[identifier(options)]
112
- end
113
- # backward compatibility: TODO: remove in future ? (make url mandatory ?)
114
- if info.nil?
115
- info = @all_secrets[username]
116
- end
117
- result = options.clone
118
- case info
119
- when NilClass
120
- raise "no such secret: [#{url}|#{username}] in #{@all_secrets.keys.join(',')}"
121
- when String
122
- result[:secret] = info
123
- result[:description] = ''
124
- when Hash
125
- info=info.symbolize_keys
126
- key = identifier(options)
127
- plain = SimpleCipher.new(key).decrypt(info[:secret]) rescue info[:secret]
128
- result[:secret] = plain
129
- result[:description] = info[:description]
130
- else raise "#{info.class} is not an expected type"
131
- end
66
+ label=options[:label]
67
+ result = @all_secrets[label].clone
68
+ raise "no such entry #{label}" if result.nil?
69
+ result[:label]=label
132
70
  return result
133
71
  end
134
72
  end