aspera-cli 4.9.0 → 4.10.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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