Class: Aspera::Node

Inherits:
Rest
  • Object
show all
Defined in:
lib/aspera/node.rb

Overview

Provides additional functions using node API with gen4 extensions (access keys)

Direct Known Subclasses

CosNode

Constant Summary collapse

ACCESS_LEVELS =

permissions

%w[delete list mkdir preview read rename write].freeze
MATCH_EXEC_PREFIX =

prefix for ruby code for filter

'exec:'
HEADER_X_ASPERA_ACCESS_KEY =
'X-Aspera-AccessKey'
PATH_SEPARATOR =
'/'
TS_FIELDS_TO_COPY =
%w[remote_host remote_user ssh_port fasp_port wss_enabled wss_port].freeze

Constants inherited from Rest

Rest::ENTITY_NOT_FOUND

Class Attribute Summary collapse

Instance Attribute Summary collapse

Attributes inherited from Rest

#params

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Rest

array_params, basic_creds, #build_request, build_uri, #call, #cancel, #create, #delete, #lookup_by_name, #oauth, #oauth_token, #read, start_http_session, #update

Constructor Details

#initialize(params:, app_info: nil, add_tspec: nil) ⇒ Node

Returns a new instance of Node.

Parameters:

  • params (Hash)

    Rest parameters

  • app_info (Hash, NilClass) (defaults to: nil)

    special processing for AoC



54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/aspera/node.rb', line 54

def initialize(params:, app_info: nil, add_tspec: nil)
  super(params)
  @app_info = app_info
  # this is added to transfer spec, for instance to add tags (COS)
  @add_tspec = add_tspec
  if !@app_info.nil?
    REQUIRED_APP_INFO_FIELDS.each do |field|
      raise "INTERNAL ERROR: app_info lacks field #{field}" unless @app_info.key?(field)
    end
    REQUIRED_APP_API_METHODS.each do |method|
      raise "INTERNAL ERROR: #{@app_info[:api].class} lacks method #{method}" unless @app_info[:api].respond_to?(method)
    end
  end
end

Class Attribute Details

.use_standard_portsObject

Returns the value of attribute use_standard_ports.



29
30
31
# File 'lib/aspera/node.rb', line 29

def use_standard_ports
  @use_standard_ports
end

Instance Attribute Details

#app_infoObject (readonly)

Returns the value of attribute app_info.



50
51
52
# File 'lib/aspera/node.rb', line 50

def app_info
  @app_info
end

Class Method Details

.file_matcher(match_expression) ⇒ Object

for access keys: provide expression to match entry in folder if no prefix: regex if prefix: ruby code if expression is nil, then always match



35
36
37
38
39
40
41
# File 'lib/aspera/node.rb', line 35

def file_matcher(match_expression)
  match_expression ||= "#{MATCH_EXEC_PREFIX}true"
  if match_expression.start_with?(MATCH_EXEC_PREFIX)
    return Environment.secure_eval("lambda{|f|#{match_expression[MATCH_EXEC_PREFIX.length..-1]}}")
  end
  return lambda{|f|f['name'].match(/#{match_expression}/)}
end

Instance Method Details

#add_tspec_info(tspec) ⇒ Object

update transfer spec with special additional tags



70
71
72
73
# File 'lib/aspera/node.rb', line 70

def add_tspec_info(tspec)
  tspec.deep_merge!(@add_tspec) unless @add_tspec.nil?
  return tspec
end

#find_files(top_file_id, test_block) ⇒ Object



184
185
186
187
188
189
190
191
192
193
# File 'lib/aspera/node.rb', line 184

def find_files(top_file_id, test_block)
  Log.log.debug{"find_files: file id=#{top_file_id}"}
  find_state = {found: [], test_block: test_block}
  process_folder_tree(state: find_state, top_file_id: top_file_id) do |entry, path, state|
    state[:found].push(entry.merge({'path' => path})) if state[:test_block].call(entry)
    # test all files deeply
    true
  end
  return find_state[:found]
end

#node_id_to_node(node_id) ⇒ Object



76
77
78
79
80
81
82
83
84
85
86
# File 'lib/aspera/node.rb', line 76

def node_id_to_node(node_id)
  if !@app_info.nil?
    return self if node_id.eql?(@app_info[:node_info]['id'])
    return @app_info[:api].node_api_from(
      node_id: node_id,
      workspace_id: @app_info[:workspace_id],
      workspace_name: @app_info[:workspace_name])
  end
  Log.log.warn{"cannot resolve link with node id #{node_id}"}
  return nil
end

#process_folder_tree(state:, top_file_id:, top_file_path: '/', &block) ⇒ Object

Recursively browse in a folder (with non-recursive method) sub folders are processed if the processing method returns true

Parameters:

  • state (Object)

    state object sent to processing method

  • top_file_id (String)

    file id to start at (default = access key root file id)

  • top_file_path (String) (defaults to: '/')

    path of top folder (default = /)

  • block (Proc)

    processing method, args: entry, path, state



94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/aspera/node.rb', line 94

def process_folder_tree(state:, top_file_id:, top_file_path: '/', &block)
  raise 'INTERNAL ERROR: top_file_path not set' if top_file_path.nil?
  raise 'INTERNAL ERROR: Missing block' unless block
  # start at top folder
  folders_to_explore = [{id: top_file_id, path: top_file_path}]
  Log.dump(:folders_to_explore, folders_to_explore)
  until folders_to_explore.empty?
    current_item = folders_to_explore.shift
    Log.log.debug{"searching #{current_item[:path]}".bg_green}
    # get folder content
    folder_contents =
      begin
        read("files/#{current_item[:id]}/files")[:data]
      rescue StandardError => e
        Log.log.warn{"#{current_item[:path]}: #{e.class} #{e.message}"}
        []
      end
    Log.dump(:folder_contents, folder_contents)
    folder_contents.each do |entry|
      relative_path = File.join(current_item[:path], entry['name'])
      Log.log.debug{"process_folder_tree checking #{relative_path}"}
      # continue only if method returns true
      next unless yield(entry, relative_path, state)
      # entry type is file, folder or link
      case entry['type']
      when 'folder'
        folders_to_explore.push({id: entry['id'], path: relative_path})
      when 'link'
        node_id_to_node(entry['target_node_id'])&.process_folder_tree(
          state:         state,
          top_file_id:   entry['target_id'],
          top_file_path: relative_path,
          &block)
      end
    end
  end
end

#refreshed_transfer_tokenObject



195
196
197
# File 'lib/aspera/node.rb', line 195

def refreshed_transfer_token
  return oauth_token(force_refresh: true)
end

#resolve_api_fid(top_file_id, path) ⇒ Hash

Navigate the path from given file id

Parameters:

  • top_file_id (String)

    id initial file id

  • path (String)

    file path

Returns:

  • (Hash)

    Aspera::Node.api,.api,.file_id



136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
# File 'lib/aspera/node.rb', line 136

def resolve_api_fid(top_file_id, path)
  raise 'file id shall be String' unless top_file_id.is_a?(String)
  process_last_link = path.end_with?(PATH_SEPARATOR)
  path_elements = path.split(PATH_SEPARATOR).reject(&:empty?)
  return {api: self, file_id: top_file_id} if path_elements.empty?
  resolve_state = {path: path_elements, result: nil}
  process_folder_tree(state: resolve_state, top_file_id: top_file_id) do |entry, _path, state|
    # this block is called recursively for each entry in folder
    # stop digging here if not in right path
    next false unless entry['name'].eql?(state[:path].first)
    # ok it matches, so we remove the match
    state[:path].shift
    case entry['type']
    when 'file'
      # file must be terminal
      raise "#{entry['name']} is a file, expecting folder to find: #{state[:path]}" unless state[:path].empty?
      # it's terminal, we found it
      state[:result] = {api: self, file_id: entry['id']}
      next false
    when 'folder'
      if state[:path].empty?
        # we found it
        state[:result] = {api: self, file_id: entry['id']}
        next false
      end
    when 'link'
      if state[:path].empty?
        if process_last_link
          # we found it
          other_node = node_id_to_node(entry['target_node_id'])
          raise 'cannot resolve link' if other_node.nil?
          state[:result] = {api: other_node, file_id: entry['target_id']}
        else
          # we found it but we do not process the link
          state[:result] = {api: self, file_id: entry['id']}
        end
        next false
      end
    else
      Log.log.warn{"Unknown element type: #{entry['type']}"}
    end
    # continue to dig folder
    next true
  end
  raise "entry not found: #{resolve_state[:path]}" if resolve_state[:result].nil?
  return resolve_state[:result]
end

#transfer_spec_gen4(file_id, direction, ts_merge = nil) ⇒ Object

Create transfer spec for gen4



200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
# File 'lib/aspera/node.rb', line 200

def transfer_spec_gen4(file_id, direction, ts_merge=nil)
  ak_name = nil
  ak_token = nil
  case params[:auth][:type]
  when :basic
    ak_name = params[:auth][:username]
    raise 'ERROR: no secret in node object' unless params[:auth][:password]
    ak_token = Rest.basic_creds(params[:auth][:username], params[:auth][:password])
  when :oauth2
    ak_name = params[:headers][HEADER_X_ASPERA_ACCESS_KEY]
    # TODO: token_generation_lambda = lambda{|do_refresh|oauth_token(force_refresh: do_refresh)}
    # get bearer token, possibly use cache
    ak_token = oauth_token(force_refresh: false)
  else raise "Unsupported auth method for node gen4: #{params[:auth][:type]}"
  end
  transfer_spec = {
    'direction' => direction,
    'token'     => ak_token,
    'tags'      => {
      Fasp::TransferSpec::TAG_RESERVED => {
        'node' => {
          'access_key' => ak_name,
          'file_id'    => file_id
        } # node
      } # aspera
    } # tags
  }
  # add specials tags (cos)
  add_tspec_info(transfer_spec)
  transfer_spec.deep_merge!(ts_merge) unless ts_merge.nil?
  # add application specific tags (AoC)
  app_info[:api].add_ts_tags(transfer_spec: transfer_spec, app_info: app_info) unless app_info.nil?
  # add remote host info
  if self.class.use_standard_ports
    # get default TCP/UDP ports and transfer user
    transfer_spec.merge!(Fasp::TransferSpec::AK_TSPEC_BASE)
    # by default: same address as node API
    transfer_spec['remote_host'] = URI.parse(params[:base_url]).host
    if !@app_info.nil? && !@app_info[:node_info]['transfer_url'].nil? && !@app_info[:node_info]['transfer_url'].empty?
      transfer_spec['remote_host'] = @app_info[:node_info]['transfer_url']
    end
  else
    # retrieve values from API (and keep a copy/cache)
    @std_t_spec_cache ||= create(
      'files/download_setup',
      {transfer_requests: [{ transfer_request: {paths: [{'source' => '/'}] } }] }
    )[:data]['transfer_specs'].first['transfer_spec']
    # copy some parts
    TS_FIELDS_TO_COPY.each {|i| transfer_spec[i] = @std_t_spec_cache[i] if @std_t_spec_cache.key?(i)}
  end
  Log.log.warn{"Expected transfer user: #{Fasp::TransferSpec::ACCESS_KEY_TRANSFER_USER}, but have #{transfer_spec['remote_user']}"} \
    unless transfer_spec['remote_user'].eql?(Fasp::TransferSpec::ACCESS_KEY_TRANSFER_USER)
  return transfer_spec
end