aspera-cli 4.24.0 → 4.24.2
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.
- checksums.yaml +4 -4
 - checksums.yaml.gz.sig +0 -0
 - data/CHANGELOG.md +19 -1
 - data/README.md +1264 -941
 - data/bin/ascli +20 -1
 - data/bin/asession +23 -27
 - data/lib/aspera/agent/base.rb +10 -21
 - data/lib/aspera/agent/connect.rb +2 -3
 - data/lib/aspera/agent/desktop.rb +2 -2
 - data/lib/aspera/agent/direct.rb +49 -32
 - data/lib/aspera/agent/factory.rb +31 -0
 - data/lib/aspera/api/aoc.rb +79 -49
 - data/lib/aspera/api/faspex.rb +212 -0
 - data/lib/aspera/api/node.rb +99 -84
 - data/lib/aspera/ascp/installation.rb +22 -21
 - data/lib/aspera/ascp/management.rb +119 -23
 - data/lib/aspera/assert.rb +14 -8
 - data/lib/aspera/cli/extended_value.rb +15 -15
 - data/lib/aspera/cli/formatter.rb +7 -5
 - data/lib/aspera/cli/hints.rb +8 -0
 - data/lib/aspera/cli/info.rb +4 -4
 - data/lib/aspera/cli/main.rb +56 -71
 - data/lib/aspera/cli/manager.rb +7 -4
 - data/lib/aspera/cli/plugins/alee.rb +2 -1
 - data/lib/aspera/cli/plugins/aoc.rb +110 -186
 - data/lib/aspera/cli/plugins/ats.rb +4 -4
 - data/lib/aspera/cli/plugins/base.rb +335 -0
 - data/lib/aspera/cli/plugins/basic_auth.rb +45 -0
 - data/lib/aspera/cli/plugins/config.rb +263 -221
 - data/lib/aspera/cli/plugins/console.rb +15 -15
 - data/lib/aspera/cli/plugins/cos.rb +2 -2
 - data/lib/aspera/cli/plugins/factory.rb +78 -0
 - data/lib/aspera/cli/plugins/faspex.rb +17 -20
 - data/lib/aspera/cli/plugins/faspex5.rb +79 -193
 - data/lib/aspera/cli/plugins/faspio.rb +14 -13
 - data/lib/aspera/cli/plugins/httpgw.rb +13 -12
 - data/lib/aspera/cli/plugins/node.rb +34 -32
 - data/lib/aspera/cli/plugins/oauth.rb +48 -0
 - data/lib/aspera/cli/plugins/orchestrator.rb +15 -13
 - data/lib/aspera/cli/plugins/preview.rb +4 -4
 - data/lib/aspera/cli/plugins/server.rb +15 -13
 - data/lib/aspera/cli/plugins/shares.rb +18 -15
 - data/lib/aspera/cli/sync_actions.rb +1 -1
 - data/lib/aspera/cli/transfer_agent.rb +24 -20
 - data/lib/aspera/cli/transfer_progress.rb +6 -6
 - data/lib/aspera/cli/version.rb +3 -3
 - data/lib/aspera/cli/wizard.rb +74 -65
 - data/lib/aspera/colors.rb +6 -0
 - data/lib/aspera/command_line_builder.rb +45 -50
 - data/lib/aspera/command_line_converter.rb +2 -1
 - data/lib/aspera/coverage.rb +1 -1
 - data/lib/aspera/data_repository.rb +1 -1
 - data/lib/aspera/environment.rb +13 -9
 - data/lib/aspera/faspex_gw.rb +6 -4
 - data/lib/aspera/faspex_postproc.rb +1 -1
 - data/lib/aspera/keychain/macos_security.rb +1 -1
 - data/lib/aspera/log.rb +88 -37
 - data/lib/aspera/nagios.rb +1 -1
 - data/lib/aspera/oauth/base.rb +17 -10
 - data/lib/aspera/oauth/factory.rb +8 -8
 - data/lib/aspera/oauth/web.rb +2 -2
 - data/lib/aspera/products/connect.rb +4 -3
 - data/lib/aspera/products/desktop.rb +1 -4
 - data/lib/aspera/products/other.rb +9 -1
 - data/lib/aspera/products/transferd.rb +0 -1
 - data/lib/aspera/rest.rb +126 -83
 - data/lib/aspera/ssh.rb +3 -3
 - data/lib/aspera/sync/args.schema.yaml +46 -3
 - data/lib/aspera/sync/conf.schema.yaml +130 -94
 - data/lib/aspera/sync/operations.rb +71 -74
 - data/lib/aspera/temp_file_manager.rb +17 -5
 - data/lib/aspera/transfer/error.rb +16 -7
 - data/lib/aspera/transfer/parameters.rb +34 -20
 - data/lib/aspera/transfer/resumer.rb +74 -0
 - data/lib/aspera/transfer/spec.rb +4 -3
 - data/lib/aspera/transfer/spec.schema.yaml +132 -51
 - data/lib/aspera/transfer/spec_doc.rb +41 -35
 - data/lib/aspera/uri_reader.rb +1 -1
 - data/lib/aspera/web_auth.rb +6 -6
 - data.tar.gz.sig +0 -0
 - metadata +9 -7
 - metadata.gz.sig +2 -2
 - data/lib/aspera/cli/basic_auth_plugin.rb +0 -43
 - data/lib/aspera/cli/plugin.rb +0 -333
 - data/lib/aspera/cli/plugin_factory.rb +0 -81
 - data/lib/aspera/resumer.rb +0 -77
 - data/lib/aspera/transfer/error_info.rb +0 -91
 
| 
         @@ -0,0 +1,212 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require 'aspera/rest'
         
     | 
| 
      
 4 
     | 
    
         
            +
            require 'aspera/oauth/base'
         
     | 
| 
      
 5 
     | 
    
         
            +
            require 'digest'
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
            module Aspera
         
     | 
| 
      
 8 
     | 
    
         
            +
              # Implement OAuth for Faspex public link
         
     | 
| 
      
 9 
     | 
    
         
            +
              class FaspexPubLink < OAuth::Base
         
     | 
| 
      
 10 
     | 
    
         
            +
                class << self
         
     | 
| 
      
 11 
     | 
    
         
            +
                  attr_accessor :additional_info
         
     | 
| 
      
 12 
     | 
    
         
            +
                end
         
     | 
| 
      
 13 
     | 
    
         
            +
                # @param context         The `context` query parameter in public link
         
     | 
| 
      
 14 
     | 
    
         
            +
                # @param redirect_uri    URI of web UI login
         
     | 
| 
      
 15 
     | 
    
         
            +
                # @param path_authorize  Path to provide passcode
         
     | 
| 
      
 16 
     | 
    
         
            +
                def initialize(
         
     | 
| 
      
 17 
     | 
    
         
            +
                  context:,
         
     | 
| 
      
 18 
     | 
    
         
            +
                  redirect_uri:,
         
     | 
| 
      
 19 
     | 
    
         
            +
                  path_authorize: 'authorize_public_link',
         
     | 
| 
      
 20 
     | 
    
         
            +
                  **base_params
         
     | 
| 
      
 21 
     | 
    
         
            +
                )
         
     | 
| 
      
 22 
     | 
    
         
            +
                  # a unique identifier could also be the passcode inside
         
     | 
| 
      
 23 
     | 
    
         
            +
                  super(**base_params, cache_ids: [Digest::SHA256.hexdigest(context)[0..23]])
         
     | 
| 
      
 24 
     | 
    
         
            +
                  @context = context
         
     | 
| 
      
 25 
     | 
    
         
            +
                  @redirect_uri = redirect_uri
         
     | 
| 
      
 26 
     | 
    
         
            +
                  @path_authorize = path_authorize
         
     | 
| 
      
 27 
     | 
    
         
            +
                end
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
                def create_token
         
     | 
| 
      
 30 
     | 
    
         
            +
                  # Exchange context (passcode) for code
         
     | 
| 
      
 31 
     | 
    
         
            +
                  resp = api.call(
         
     | 
| 
      
 32 
     | 
    
         
            +
                    operation: 'GET',
         
     | 
| 
      
 33 
     | 
    
         
            +
                    subpath:   @path_authorize,
         
     | 
| 
      
 34 
     | 
    
         
            +
                    query: {
         
     | 
| 
      
 35 
     | 
    
         
            +
                      response_type: :code,
         
     | 
| 
      
 36 
     | 
    
         
            +
                      state:         @context,
         
     | 
| 
      
 37 
     | 
    
         
            +
                      client_id:     client_id,
         
     | 
| 
      
 38 
     | 
    
         
            +
                      redirect_uri:  @redirect_uri
         
     | 
| 
      
 39 
     | 
    
         
            +
                    },
         
     | 
| 
      
 40 
     | 
    
         
            +
                    exception: false
         
     | 
| 
      
 41 
     | 
    
         
            +
                  )
         
     | 
| 
      
 42 
     | 
    
         
            +
                  # code / state located in redirected URL query
         
     | 
| 
      
 43 
     | 
    
         
            +
                  info = Rest.query_to_h(URI.parse(resp[:http]['Location']).query)
         
     | 
| 
      
 44 
     | 
    
         
            +
                  Log.dump(:info, info)
         
     | 
| 
      
 45 
     | 
    
         
            +
                  raise Error, info['action_message'] if info['action_message']
         
     | 
| 
      
 46 
     | 
    
         
            +
                  Aspera.assert(info['code']){'Missing code in answer'}
         
     | 
| 
      
 47 
     | 
    
         
            +
                  # Exchange code for token
         
     | 
| 
      
 48 
     | 
    
         
            +
                  return create_token_call(optional_scope_client_id.merge(
         
     | 
| 
      
 49 
     | 
    
         
            +
                    grant_type:   'authorization_code',
         
     | 
| 
      
 50 
     | 
    
         
            +
                    code:         info['code'],
         
     | 
| 
      
 51 
     | 
    
         
            +
                    redirect_uri: @redirect_uri
         
     | 
| 
      
 52 
     | 
    
         
            +
                  ))
         
     | 
| 
      
 53 
     | 
    
         
            +
                end
         
     | 
| 
      
 54 
     | 
    
         
            +
              end
         
     | 
| 
      
 55 
     | 
    
         
            +
              OAuth::Factory.instance.register_token_creator(FaspexPubLink)
         
     | 
| 
      
 56 
     | 
    
         
            +
              module Api
         
     | 
| 
      
 57 
     | 
    
         
            +
                class Faspex < Aspera::Rest
         
     | 
| 
      
 58 
     | 
    
         
            +
                  # endpoint for authentication API
         
     | 
| 
      
 59 
     | 
    
         
            +
                  PATH_AUTH = 'auth'
         
     | 
| 
      
 60 
     | 
    
         
            +
                  PATH_API_V5 = 'api/v5'
         
     | 
| 
      
 61 
     | 
    
         
            +
                  PATH_HEALTH = 'configuration/ping'
         
     | 
| 
      
 62 
     | 
    
         
            +
                  private_constant :PATH_API_V5,
         
     | 
| 
      
 63 
     | 
    
         
            +
                    :PATH_HEALTH,
         
     | 
| 
      
 64 
     | 
    
         
            +
                    :PATH_AUTH
         
     | 
| 
      
 65 
     | 
    
         
            +
                  RECIPIENT_TYPES = %w[user workgroup external_user distribution_list shared_inbox].freeze
         
     | 
| 
      
 66 
     | 
    
         
            +
                  PACKAGE_TERMINATED = %w[completed failed].freeze
         
     | 
| 
      
 67 
     | 
    
         
            +
                  # list of supported mailbox types (to list packages)
         
     | 
| 
      
 68 
     | 
    
         
            +
                  API_LIST_MAILBOX_TYPES = %w[inbox inbox_history inbox_all inbox_all_history outbox outbox_history pending pending_history all].freeze
         
     | 
| 
      
 69 
     | 
    
         
            +
                  # PACKAGE_SEND_FROM_REMOTE_SOURCE = 'remote_source'
         
     | 
| 
      
 70 
     | 
    
         
            +
                  # Faspex API v5: get transfer spec for connect
         
     | 
| 
      
 71 
     | 
    
         
            +
                  TRANSFER_CONNECT = 'connect'
         
     | 
| 
      
 72 
     | 
    
         
            +
                  ADMIN_RESOURCES = %i[
         
     | 
| 
      
 73 
     | 
    
         
            +
                    accounts distribution_lists contacts jobs workgroups shared_inboxes nodes oauth_clients registrations saml_configs
         
     | 
| 
      
 74 
     | 
    
         
            +
                    metadata_profiles email_notifications alternate_addresses webhooks
         
     | 
| 
      
 75 
     | 
    
         
            +
                  ].freeze
         
     | 
| 
      
 76 
     | 
    
         
            +
                  # states for jobs not in final state
         
     | 
| 
      
 77 
     | 
    
         
            +
                  JOB_RUNNING = %w[queued working].freeze
         
     | 
| 
      
 78 
     | 
    
         
            +
                  PATH_STANDARD_ROOT = '/aspera/faspex'
         
     | 
| 
      
 79 
     | 
    
         
            +
                  PATH_API_DETECT = "#{PATH_API_V5}/#{PATH_HEALTH}"
         
     | 
| 
      
 80 
     | 
    
         
            +
                  HEADER_ITERATION_TOKEN = 'X-Aspera-Next-Iteration-Token'
         
     | 
| 
      
 81 
     | 
    
         
            +
                  HEADER_FASPEX_VERSION = 'X-IBM-Aspera'
         
     | 
| 
      
 82 
     | 
    
         
            +
                  EMAIL_NOTIF_LIST = %w[
         
     | 
| 
      
 83 
     | 
    
         
            +
                    welcome_email
         
     | 
| 
      
 84 
     | 
    
         
            +
                    forgot_password
         
     | 
| 
      
 85 
     | 
    
         
            +
                    package_received
         
     | 
| 
      
 86 
     | 
    
         
            +
                    package_received_cc
         
     | 
| 
      
 87 
     | 
    
         
            +
                    package_sent_cc
         
     | 
| 
      
 88 
     | 
    
         
            +
                    package_downloaded
         
     | 
| 
      
 89 
     | 
    
         
            +
                    package_downloaded_cc
         
     | 
| 
      
 90 
     | 
    
         
            +
                    workgroup_package
         
     | 
| 
      
 91 
     | 
    
         
            +
                    upload_result
         
     | 
| 
      
 92 
     | 
    
         
            +
                    upload_result_cc
         
     | 
| 
      
 93 
     | 
    
         
            +
                    relay_started_cc
         
     | 
| 
      
 94 
     | 
    
         
            +
                    relay_finished_cc
         
     | 
| 
      
 95 
     | 
    
         
            +
                    relay_error_cc
         
     | 
| 
      
 96 
     | 
    
         
            +
                    shared_inbox_invitation
         
     | 
| 
      
 97 
     | 
    
         
            +
                    shared_inbox_submit
         
     | 
| 
      
 98 
     | 
    
         
            +
                    personal_invitation
         
     | 
| 
      
 99 
     | 
    
         
            +
                    personal_submit
         
     | 
| 
      
 100 
     | 
    
         
            +
                    account_approved
         
     | 
| 
      
 101 
     | 
    
         
            +
                    account_denied
         
     | 
| 
      
 102 
     | 
    
         
            +
                    package_file_processing_failed_sender
         
     | 
| 
      
 103 
     | 
    
         
            +
                    package_file_processing_failed_recipient
         
     | 
| 
      
 104 
     | 
    
         
            +
                    relay_failed_admin
         
     | 
| 
      
 105 
     | 
    
         
            +
                    relay_failed
         
     | 
| 
      
 106 
     | 
    
         
            +
                    admin_sync_failed
         
     | 
| 
      
 107 
     | 
    
         
            +
                    sync_failed
         
     | 
| 
      
 108 
     | 
    
         
            +
                    account_exist
         
     | 
| 
      
 109 
     | 
    
         
            +
                    mfa_code
         
     | 
| 
      
 110 
     | 
    
         
            +
                  ]
         
     | 
| 
      
 111 
     | 
    
         
            +
                  class << self
         
     | 
| 
      
 112 
     | 
    
         
            +
                    # @return true if the URL is a public link
         
     | 
| 
      
 113 
     | 
    
         
            +
                    def public_link?(url)
         
     | 
| 
      
 114 
     | 
    
         
            +
                      url.include?('?context=')
         
     | 
| 
      
 115 
     | 
    
         
            +
                    end
         
     | 
| 
      
 116 
     | 
    
         
            +
                  end
         
     | 
| 
      
 117 
     | 
    
         
            +
                  attr_reader :pub_link_context
         
     | 
| 
      
 118 
     | 
    
         
            +
             
     | 
| 
      
 119 
     | 
    
         
            +
                  def initialize(
         
     | 
| 
      
 120 
     | 
    
         
            +
                    url:,
         
     | 
| 
      
 121 
     | 
    
         
            +
                    auth:,
         
     | 
| 
      
 122 
     | 
    
         
            +
                    password: nil,
         
     | 
| 
      
 123 
     | 
    
         
            +
                    client_id: nil,
         
     | 
| 
      
 124 
     | 
    
         
            +
                    client_secret: nil,
         
     | 
| 
      
 125 
     | 
    
         
            +
                    redirect_uri: nil,
         
     | 
| 
      
 126 
     | 
    
         
            +
                    username: nil,
         
     | 
| 
      
 127 
     | 
    
         
            +
                    private_key: nil,
         
     | 
| 
      
 128 
     | 
    
         
            +
                    passphrase: nil
         
     | 
| 
      
 129 
     | 
    
         
            +
                  )
         
     | 
| 
      
 130 
     | 
    
         
            +
                    auth = :public_link if self.class.public_link?(url)
         
     | 
| 
      
 131 
     | 
    
         
            +
                    @pub_link_context = nil
         
     | 
| 
      
 132 
     | 
    
         
            +
                    super(**
         
     | 
| 
      
 133 
     | 
    
         
            +
                      case auth
         
     | 
| 
      
 134 
     | 
    
         
            +
                      when :public_link
         
     | 
| 
      
 135 
     | 
    
         
            +
                        # Get URL of final redirect of public link
         
     | 
| 
      
 136 
     | 
    
         
            +
                        redir_url = Rest.new(base_url: url, redirect_max: 3).call(operation: 'GET')[:http].uri.to_s
         
     | 
| 
      
 137 
     | 
    
         
            +
                        Log.dump(:redir_url, redir_url, level: :trace1)
         
     | 
| 
      
 138 
     | 
    
         
            +
                        # get context from query
         
     | 
| 
      
 139 
     | 
    
         
            +
                        encoded_context = Rest.query_to_h(URI.parse(redir_url).query)['context']
         
     | 
| 
      
 140 
     | 
    
         
            +
                        raise ParameterError, 'Bad faspex5 public link, missing context in query' if encoded_context.nil?
         
     | 
| 
      
 141 
     | 
    
         
            +
                        # public link information (contains passcode and allowed usage)
         
     | 
| 
      
 142 
     | 
    
         
            +
                        @pub_link_context = JSON.parse(Base64.decode64(encoded_context))
         
     | 
| 
      
 143 
     | 
    
         
            +
                        Log.dump(:pub_link_context, @pub_link_context, level: :trace1)
         
     | 
| 
      
 144 
     | 
    
         
            +
                        # Get the base url, i.e. .../aspera/faspex
         
     | 
| 
      
 145 
     | 
    
         
            +
                        base_url = redir_url.gsub(%r{/public/.*}, '').gsub(/\?.*/, '')
         
     | 
| 
      
 146 
     | 
    
         
            +
                        # Get web UI client_id and redirect_uri
         
     | 
| 
      
 147 
     | 
    
         
            +
                        # TODO: change this for something more reliable
         
     | 
| 
      
 148 
     | 
    
         
            +
                        config = JSON.parse(Rest.new(base_url: "#{base_url}/config.js", redirect_max: 3).call(operation: 'GET')[:data].sub(/^[^=]+=/, '').gsub(/([a-z_]+):/, '"\1":').delete("\n ").tr("'", '"')).symbolize_keys
         
     | 
| 
      
 149 
     | 
    
         
            +
                        Log.dump(:configjs, config)
         
     | 
| 
      
 150 
     | 
    
         
            +
                        {
         
     | 
| 
      
 151 
     | 
    
         
            +
                          base_url: "#{base_url}/#{PATH_API_V5}",
         
     | 
| 
      
 152 
     | 
    
         
            +
                          auth:     {
         
     | 
| 
      
 153 
     | 
    
         
            +
                            type:         :oauth2,
         
     | 
| 
      
 154 
     | 
    
         
            +
                            base_url:     "#{base_url}/#{PATH_AUTH}",
         
     | 
| 
      
 155 
     | 
    
         
            +
                            grant_method: :faspex_pub_link,
         
     | 
| 
      
 156 
     | 
    
         
            +
                            context:      encoded_context,
         
     | 
| 
      
 157 
     | 
    
         
            +
                            client_id:    config[:client_id],
         
     | 
| 
      
 158 
     | 
    
         
            +
                            redirect_uri: config[:redirect_uri]
         
     | 
| 
      
 159 
     | 
    
         
            +
                          }
         
     | 
| 
      
 160 
     | 
    
         
            +
                        }
         
     | 
| 
      
 161 
     | 
    
         
            +
                      # old: headers:  {'Passcode' => @pub_link_context['passcode']}
         
     | 
| 
      
 162 
     | 
    
         
            +
                      when :boot
         
     | 
| 
      
 163 
     | 
    
         
            +
                        Aspera.assert(password, type: ParameterError){'Missing password'}
         
     | 
| 
      
 164 
     | 
    
         
            +
                        # the password here is the token copied directly from browser in developer mode
         
     | 
| 
      
 165 
     | 
    
         
            +
                        {
         
     | 
| 
      
 166 
     | 
    
         
            +
                          base_url: "#{url}/#{PATH_API_V5}",
         
     | 
| 
      
 167 
     | 
    
         
            +
                          headers:  {'Authorization' => password}
         
     | 
| 
      
 168 
     | 
    
         
            +
                        }
         
     | 
| 
      
 169 
     | 
    
         
            +
                      when :web
         
     | 
| 
      
 170 
     | 
    
         
            +
                        Aspera.assert(client_id, type: ParameterError){'Missing client_id'}
         
     | 
| 
      
 171 
     | 
    
         
            +
                        Aspera.assert(redirect_uri, type: ParameterError){'Missing redirect_uri'}
         
     | 
| 
      
 172 
     | 
    
         
            +
                        # opens a browser and ask user to auth using web
         
     | 
| 
      
 173 
     | 
    
         
            +
                        {
         
     | 
| 
      
 174 
     | 
    
         
            +
                          base_url: "#{url}/#{PATH_API_V5}",
         
     | 
| 
      
 175 
     | 
    
         
            +
                          auth:     {
         
     | 
| 
      
 176 
     | 
    
         
            +
                            type:         :oauth2,
         
     | 
| 
      
 177 
     | 
    
         
            +
                            base_url:     "#{url}/#{PATH_AUTH}",
         
     | 
| 
      
 178 
     | 
    
         
            +
                            grant_method: :web,
         
     | 
| 
      
 179 
     | 
    
         
            +
                            client_id:    client_id,
         
     | 
| 
      
 180 
     | 
    
         
            +
                            redirect_uri: redirect_uri
         
     | 
| 
      
 181 
     | 
    
         
            +
                          }
         
     | 
| 
      
 182 
     | 
    
         
            +
                        }
         
     | 
| 
      
 183 
     | 
    
         
            +
                      when :jwt
         
     | 
| 
      
 184 
     | 
    
         
            +
                        Aspera.assert(client_id, type: ParameterError){'Missing client_id'}
         
     | 
| 
      
 185 
     | 
    
         
            +
                        Aspera.assert(private_key, type: ParameterError){'Missing private_key'}
         
     | 
| 
      
 186 
     | 
    
         
            +
                        {
         
     | 
| 
      
 187 
     | 
    
         
            +
                          base_url: "#{url}/#{PATH_API_V5}",
         
     | 
| 
      
 188 
     | 
    
         
            +
                          auth:     {
         
     | 
| 
      
 189 
     | 
    
         
            +
                            type:            :oauth2,
         
     | 
| 
      
 190 
     | 
    
         
            +
                            base_url:        "#{url}/#{PATH_AUTH}",
         
     | 
| 
      
 191 
     | 
    
         
            +
                            grant_method:    :jwt,
         
     | 
| 
      
 192 
     | 
    
         
            +
                            client_id:       client_id,
         
     | 
| 
      
 193 
     | 
    
         
            +
                            payload:         {
         
     | 
| 
      
 194 
     | 
    
         
            +
                              iss: client_id, # issuer
         
     | 
| 
      
 195 
     | 
    
         
            +
                              aud: client_id, # audience (this field is not clear...)
         
     | 
| 
      
 196 
     | 
    
         
            +
                              sub: "user:#{username}" # subject is a user
         
     | 
| 
      
 197 
     | 
    
         
            +
                            },
         
     | 
| 
      
 198 
     | 
    
         
            +
                            private_key_obj: OpenSSL::PKey::RSA.new(private_key, passphrase),
         
     | 
| 
      
 199 
     | 
    
         
            +
                            headers:         {typ: 'JWT'}
         
     | 
| 
      
 200 
     | 
    
         
            +
                          }
         
     | 
| 
      
 201 
     | 
    
         
            +
                        }
         
     | 
| 
      
 202 
     | 
    
         
            +
                      else Aspera.error_unexpected_value(auth, type: ParameterError){'auth'}
         
     | 
| 
      
 203 
     | 
    
         
            +
                      end
         
     | 
| 
      
 204 
     | 
    
         
            +
                    )
         
     | 
| 
      
 205 
     | 
    
         
            +
                  end
         
     | 
| 
      
 206 
     | 
    
         
            +
             
     | 
| 
      
 207 
     | 
    
         
            +
                  def auth_api
         
     | 
| 
      
 208 
     | 
    
         
            +
                    Rest.new(**params, base_url: base_url.sub(PATH_API_V5, PATH_AUTH))
         
     | 
| 
      
 209 
     | 
    
         
            +
                  end
         
     | 
| 
      
 210 
     | 
    
         
            +
                end
         
     | 
| 
      
 211 
     | 
    
         
            +
              end
         
     | 
| 
      
 212 
     | 
    
         
            +
            end
         
     | 
    
        data/lib/aspera/api/node.rb
    CHANGED
    
    | 
         @@ -1,6 +1,5 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            # frozen_string_literal: true
         
     | 
| 
       2 
2 
     | 
    
         | 
| 
       3 
     | 
    
         
            -
            require 'aspera/cli/error'
         
     | 
| 
       4 
3 
     | 
    
         
             
            require 'aspera/transfer/spec'
         
     | 
| 
       5 
4 
     | 
    
         
             
            require 'aspera/rest'
         
     | 
| 
       6 
5 
     | 
    
         
             
            require 'aspera/oauth'
         
     | 
| 
         @@ -17,14 +16,18 @@ module Aspera 
     | 
|
| 
       17 
16 
     | 
    
         
             
              module Api
         
     | 
| 
       18 
17 
     | 
    
         
             
                # Provides additional functions using node API with gen4 extensions (access keys)
         
     | 
| 
       19 
18 
     | 
    
         
             
                class Node < Aspera::Rest
         
     | 
| 
      
 19 
     | 
    
         
            +
                  # Separator between node.AK and user:all
         
     | 
| 
       20 
20 
     | 
    
         
             
                  SCOPE_SEPARATOR = ':'
         
     | 
| 
       21 
21 
     | 
    
         
             
                  SCOPE_NODE_PREFIX = 'node.'
         
     | 
| 
      
 22 
     | 
    
         
            +
                  # Accepted types in `file_matcher`
         
     | 
| 
       22 
23 
     | 
    
         
             
                  MATCH_TYPES = [String, Proc, Regexp, NilClass].freeze
         
     | 
| 
      
 24 
     | 
    
         
            +
                  # Delimiter in decoded node token
         
     | 
| 
       23 
25 
     | 
    
         
             
                  SIGNATURE_DELIMITER = '==SIGNATURE=='
         
     | 
| 
      
 26 
     | 
    
         
            +
                  # Default validity when generating a bearer token "manually"
         
     | 
| 
       24 
27 
     | 
    
         
             
                  BEARER_TOKEN_VALIDITY_DEFAULT = 86400
         
     | 
| 
       25 
     | 
    
         
            -
                  #  
     | 
| 
      
 28 
     | 
    
         
            +
                  # Fields in @app_info
         
     | 
| 
       26 
29 
     | 
    
         
             
                  REQUIRED_APP_INFO_FIELDS = %i[api app node_info workspace_id workspace_name].freeze
         
     | 
| 
       27 
     | 
    
         
            -
                  #  
     | 
| 
      
 30 
     | 
    
         
            +
                  # Methods of @app_info[:api]
         
     | 
| 
       28 
31 
     | 
    
         
             
                  REQUIRED_APP_API_METHODS = %i[node_api_from add_ts_tags].freeze
         
     | 
| 
       29 
32 
     | 
    
         
             
                  private_constant :SCOPE_SEPARATOR, :SCOPE_NODE_PREFIX, :MATCH_TYPES,
         
     | 
| 
       30 
33 
     | 
    
         
             
                    :SIGNATURE_DELIMITER, :BEARER_TOKEN_VALIDITY_DEFAULT,
         
     | 
| 
         @@ -32,30 +35,40 @@ module Aspera 
     | 
|
| 
       32 
35 
     | 
    
         | 
| 
       33 
36 
     | 
    
         
             
                  # Node API permissions
         
     | 
| 
       34 
37 
     | 
    
         
             
                  ACCESS_LEVELS = %w[delete list mkdir preview read rename write].freeze
         
     | 
| 
      
 38 
     | 
    
         
            +
                  # Special HTTP Headers
         
     | 
| 
       35 
39 
     | 
    
         
             
                  HEADER_X_ASPERA_ACCESS_KEY = 'X-Aspera-AccessKey'
         
     | 
| 
       36 
40 
     | 
    
         
             
                  HEADER_X_TOTAL_COUNT = 'X-Total-Count'
         
     | 
| 
       37 
41 
     | 
    
         
             
                  HEADER_X_CACHE_CONTROL = 'X-Aspera-Cache-Control'
         
     | 
| 
       38 
42 
     | 
    
         
             
                  HEADER_X_NEXT_ITER_TOKEN = 'X-Aspera-Next-Iteration-Token'
         
     | 
| 
      
 43 
     | 
    
         
            +
                  # Node sub-scopes
         
     | 
| 
       39 
44 
     | 
    
         
             
                  SCOPE_USER = 'user:all'
         
     | 
| 
       40 
45 
     | 
    
         
             
                  SCOPE_ADMIN = 'admin:all'
         
     | 
| 
       41 
46 
     | 
    
         
             
                  # / in cloud
         
     | 
| 
       42 
47 
     | 
    
         
             
                  PATH_SEPARATOR = '/'
         
     | 
| 
       43 
48 
     | 
    
         | 
| 
       44 
     | 
    
         
            -
                  #  
     | 
| 
      
 49 
     | 
    
         
            +
                  # Register node special token decoder
         
     | 
| 
       45 
50 
     | 
    
         
             
                  OAuth::Factory.instance.register_decoder(lambda{ |token| Node.decode_bearer_token(token)})
         
     | 
| 
       46 
51 
     | 
    
         | 
| 
       47 
     | 
    
         
            -
                  #  
     | 
| 
      
 52 
     | 
    
         
            +
                  # Class instance variable, access with accessors on class
         
     | 
| 
       48 
53 
     | 
    
         
             
                  @use_standard_ports = true
         
     | 
| 
       49 
54 
     | 
    
         
             
                  @use_node_cache = true
         
     | 
| 
       50 
55 
     | 
    
         | 
| 
       51 
56 
     | 
    
         
             
                  class << self
         
     | 
| 
       52 
     | 
    
         
            -
                    #  
     | 
| 
      
 57 
     | 
    
         
            +
                    # Set to false to read transfer parameters from download_setup
         
     | 
| 
       53 
58 
     | 
    
         
             
                    attr_accessor :use_standard_ports
         
     | 
| 
       54 
     | 
    
         
            -
                    #  
     | 
| 
      
 59 
     | 
    
         
            +
                    # Set to false to bypass cache in redis
         
     | 
| 
       55 
60 
     | 
    
         
             
                    attr_accessor :use_node_cache
         
     | 
| 
       56 
61 
     | 
    
         
             
                    attr_reader :use_dynamic_key
         
     | 
| 
       57 
62 
     | 
    
         | 
| 
       58 
     | 
    
         
            -
                    #  
     | 
| 
      
 63 
     | 
    
         
            +
                    # Adds cache control header, as globally specified to read request
         
     | 
| 
      
 64 
     | 
    
         
            +
                    # Use like this: read(...,**cache_control)
         
     | 
| 
      
 65 
     | 
    
         
            +
                    def cache_control
         
     | 
| 
      
 66 
     | 
    
         
            +
                      headers = {'Accept' => Rest::MIME_JSON}
         
     | 
| 
      
 67 
     | 
    
         
            +
                      headers[HEADER_X_CACHE_CONTROL] = 'no-cache' unless use_node_cache
         
     | 
| 
      
 68 
     | 
    
         
            +
                      {headers: headers}
         
     | 
| 
      
 69 
     | 
    
         
            +
                    end
         
     | 
| 
      
 70 
     | 
    
         
            +
             
     | 
| 
      
 71 
     | 
    
         
            +
                    # Set private key to be used
         
     | 
| 
       59 
72 
     | 
    
         
             
                    # @param pem_content [String] PEM encoded private key
         
     | 
| 
       60 
73 
     | 
    
         
             
                    def use_dynamic_key=(pem_content)
         
     | 
| 
       61 
74 
     | 
    
         
             
                      Aspera.assert_type(pem_content, String)
         
     | 
| 
         @@ -67,7 +80,7 @@ module Aspera 
     | 
|
| 
       67 
80 
     | 
    
         
             
                    def add_public_key(h)
         
     | 
| 
       68 
81 
     | 
    
         
             
                      if @dynamic_key
         
     | 
| 
       69 
82 
     | 
    
         
             
                        ssh_key = Net::SSH::Buffer.from(:key, @dynamic_key)
         
     | 
| 
       70 
     | 
    
         
            -
                        #  
     | 
| 
      
 83 
     | 
    
         
            +
                        # Get pub key in OpenSSH public key format (authorized_keys)
         
     | 
| 
       71 
84 
     | 
    
         
             
                        h['public_keys'] = [
         
     | 
| 
       72 
85 
     | 
    
         
             
                          ssh_key.read_string,
         
     | 
| 
       73 
86 
     | 
    
         
             
                          Base64.strict_encode64(ssh_key.to_s)
         
     | 
| 
         @@ -93,14 +106,16 @@ module Aspera 
     | 
|
| 
       93 
106 
     | 
    
         
             
                      when String
         
     | 
| 
       94 
107 
     | 
    
         
             
                        return ->(f){File.fnmatch(match_expression, f['name'], File::FNM_DOTMATCH)}
         
     | 
| 
       95 
108 
     | 
    
         
             
                      when NilClass then return ->(_){true}
         
     | 
| 
       96 
     | 
    
         
            -
                      else Aspera.error_unexpected_value(match_expression.class.name, type:  
     | 
| 
      
 109 
     | 
    
         
            +
                      else Aspera.error_unexpected_value(match_expression.class.name, type: ParameterError)
         
     | 
| 
       97 
110 
     | 
    
         
             
                      end
         
     | 
| 
       98 
111 
     | 
    
         
             
                    end
         
     | 
| 
       99 
112 
     | 
    
         | 
| 
      
 113 
     | 
    
         
            +
                    # @return [Proc] lambda from provided CLI options
         
     | 
| 
       100 
114 
     | 
    
         
             
                    def file_matcher_from_argument(options)
         
     | 
| 
       101 
115 
     | 
    
         
             
                      return file_matcher(options.get_next_argument('filter', validation: MATCH_TYPES, mandatory: false))
         
     | 
| 
       102 
116 
     | 
    
         
             
                    end
         
     | 
| 
       103 
117 
     | 
    
         | 
| 
      
 118 
     | 
    
         
            +
                    # Split path into folder + filename
         
     | 
| 
       104 
119 
     | 
    
         
             
                    # @return [Array] containing folder + inside folder/file
         
     | 
| 
       105 
120 
     | 
    
         
             
                    def split_folder(path)
         
     | 
| 
       106 
121 
     | 
    
         
             
                      folder = path.split(PATH_SEPARATOR)
         
     | 
| 
         @@ -108,11 +123,14 @@ module Aspera 
     | 
|
| 
       108 
123 
     | 
    
         
             
                      [folder.join(PATH_SEPARATOR), inside]
         
     | 
| 
       109 
124 
     | 
    
         
             
                    end
         
     | 
| 
       110 
125 
     | 
    
         | 
| 
       111 
     | 
    
         
            -
                    #  
     | 
| 
      
 126 
     | 
    
         
            +
                    # Node API scopes
         
     | 
| 
      
 127 
     | 
    
         
            +
                    # @return [String] node scope
         
     | 
| 
       112 
128 
     | 
    
         
             
                    def token_scope(access_key, scope)
         
     | 
| 
       113 
129 
     | 
    
         
             
                      return [SCOPE_NODE_PREFIX, access_key, SCOPE_SEPARATOR, scope].join('')
         
     | 
| 
       114 
130 
     | 
    
         
             
                    end
         
     | 
| 
       115 
131 
     | 
    
         | 
| 
      
 132 
     | 
    
         
            +
                    # Decode node scope into access key and scope
         
     | 
| 
      
 133 
     | 
    
         
            +
                    # @return [Hash]
         
     | 
| 
       116 
134 
     | 
    
         
             
                    def decode_scope(scope)
         
     | 
| 
       117 
135 
     | 
    
         
             
                      items = scope.split(SCOPE_SEPARATOR, 2)
         
     | 
| 
       118 
136 
     | 
    
         
             
                      Aspera.assert(items.length.eql?(2)){"invalid scope: #{scope}"}
         
     | 
| 
         @@ -130,7 +148,7 @@ module Aspera 
     | 
|
| 
       130 
148 
     | 
    
         
             
                      Aspera.assert_type(payload['user_id'], String)
         
     | 
| 
       131 
149 
     | 
    
         
             
                      Aspera.assert(!payload['user_id'].empty?)
         
     | 
| 
       132 
150 
     | 
    
         
             
                      Aspera.assert_type(private_key, OpenSSL::PKey::RSA)
         
     | 
| 
       133 
     | 
    
         
            -
                      #  
     | 
| 
      
 151 
     | 
    
         
            +
                      # Manage convenience parameters
         
     | 
| 
       134 
152 
     | 
    
         
             
                      expiration_sec = payload['_validity'] || BEARER_TOKEN_VALIDITY_DEFAULT
         
     | 
| 
       135 
153 
     | 
    
         
             
                      payload.delete('_validity')
         
     | 
| 
       136 
154 
     | 
    
         
             
                      scope = payload['_scope'] || SCOPE_USER
         
     | 
| 
         @@ -152,8 +170,9 @@ module Aspera 
     | 
|
| 
       152 
170 
     | 
    
         
             
                      return JSON.parse(Zlib::Inflate.inflate(Base64.decode64(token)).partition(SIGNATURE_DELIMITER).first)
         
     | 
| 
       153 
171 
     | 
    
         
             
                    end
         
     | 
| 
       154 
172 
     | 
    
         | 
| 
      
 173 
     | 
    
         
            +
                    # @return [Hash] Headers to call node API with access key and auth
         
     | 
| 
       155 
174 
     | 
    
         
             
                    def bearer_headers(bearer_auth, access_key: nil)
         
     | 
| 
       156 
     | 
    
         
            -
                      #  
     | 
| 
      
 175 
     | 
    
         
            +
                      # If username is not provided, use the access key from the token
         
     | 
| 
       157 
176 
     | 
    
         
             
                      if access_key.nil?
         
     | 
| 
       158 
177 
     | 
    
         
             
                        access_key = Node.decode_scope(Node.decode_bearer_token(OAuth::Factory.bearer_token(bearer_auth))['scope'])[:access_key]
         
     | 
| 
       159 
178 
     | 
    
         
             
                        Aspera.assert(!access_key.nil?)
         
     | 
| 
         @@ -167,17 +186,17 @@ module Aspera 
     | 
|
| 
       167 
186 
     | 
    
         | 
| 
       168 
187 
     | 
    
         
             
                  attr_reader :app_info
         
     | 
| 
       169 
188 
     | 
    
         | 
| 
       170 
     | 
    
         
            -
                  # @param app_info  [Hash,NilClass]    
     | 
| 
      
 189 
     | 
    
         
            +
                  # @param app_info  [Hash,NilClass]   App information, typically AoC
         
     | 
| 
       171 
190 
     | 
    
         
             
                  # @param add_tspec [Hash,NilClass]   Additional transfer spec
         
     | 
| 
       172 
191 
     | 
    
         
             
                  # @param base_url  [String]          Rest parameters
         
     | 
| 
       173 
192 
     | 
    
         
             
                  # @param auth      [String,NilClass] Rest parameters
         
     | 
| 
       174 
193 
     | 
    
         
             
                  # @param headers   [String,NilClass] Rest parameters
         
     | 
| 
       175 
194 
     | 
    
         
             
                  def initialize(app_info: nil, add_tspec: nil, **rest_args)
         
     | 
| 
       176 
     | 
    
         
            -
                    #  
     | 
| 
      
 195 
     | 
    
         
            +
                    # Init Rest
         
     | 
| 
       177 
196 
     | 
    
         
             
                    super(**rest_args)
         
     | 
| 
       178 
197 
     | 
    
         
             
                    @dynamic_key = nil
         
     | 
| 
       179 
198 
     | 
    
         
             
                    @app_info = app_info
         
     | 
| 
       180 
     | 
    
         
            -
                    #  
     | 
| 
      
 199 
     | 
    
         
            +
                    # This is added to transfer spec, for instance to add tags (COS)
         
     | 
| 
       181 
200 
     | 
    
         
             
                    @add_tspec = add_tspec
         
     | 
| 
       182 
201 
     | 
    
         
             
                    @std_t_spec_cache = nil
         
     | 
| 
       183 
202 
     | 
    
         
             
                    if !@app_info.nil?
         
     | 
| 
         @@ -190,25 +209,15 @@ module Aspera 
     | 
|
| 
       190 
209 
     | 
    
         
             
                    end
         
     | 
| 
       191 
210 
     | 
    
         
             
                  end
         
     | 
| 
       192 
211 
     | 
    
         | 
| 
       193 
     | 
    
         
            -
                  #  
     | 
| 
       194 
     | 
    
         
            -
                   
     | 
| 
       195 
     | 
    
         
            -
             
     | 
| 
       196 
     | 
    
         
            -
                    headers[HEADER_X_CACHE_CONTROL] = 'no-cache' unless self.class.use_node_cache
         
     | 
| 
       197 
     | 
    
         
            -
                    return call(
         
     | 
| 
       198 
     | 
    
         
            -
                      operation: 'GET',
         
     | 
| 
       199 
     | 
    
         
            -
                      subpath:   subpath,
         
     | 
| 
       200 
     | 
    
         
            -
                      headers:   headers,
         
     | 
| 
       201 
     | 
    
         
            -
                      query:     query
         
     | 
| 
       202 
     | 
    
         
            -
                    )[:data]
         
     | 
| 
       203 
     | 
    
         
            -
                  end
         
     | 
| 
       204 
     | 
    
         
            -
             
     | 
| 
       205 
     | 
    
         
            -
                  # update transfer spec with special additional tags
         
     | 
| 
      
 212 
     | 
    
         
            +
                  # Update transfer spec with special additional tags
         
     | 
| 
      
 213 
     | 
    
         
            +
                  # @param tspec [Hash] Transfer spec to be modified
         
     | 
| 
      
 214 
     | 
    
         
            +
                  # @return [Hash] initial modified tspec
         
     | 
| 
       206 
215 
     | 
    
         
             
                  def add_tspec_info(tspec)
         
     | 
| 
       207 
216 
     | 
    
         
             
                    tspec.deep_merge!(@add_tspec) unless @add_tspec.nil?
         
     | 
| 
       208 
217 
     | 
    
         
             
                    return tspec
         
     | 
| 
       209 
218 
     | 
    
         
             
                  end
         
     | 
| 
       210 
219 
     | 
    
         | 
| 
       211 
     | 
    
         
            -
                  # @returns [Node] a Node or nil
         
     | 
| 
      
 220 
     | 
    
         
            +
                  # @returns [Node] a Node Api object or nil if no App defined
         
     | 
| 
       212 
221 
     | 
    
         
             
                  def node_id_to_node(node_id)
         
     | 
| 
       213 
222 
     | 
    
         
             
                    if !@app_info.nil?
         
     | 
| 
       214 
223 
     | 
    
         
             
                      return self if node_id.eql?(@app_info[:node_info]['id'])
         
     | 
| 
         @@ -226,7 +235,7 @@ module Aspera 
     | 
|
| 
       226 
235 
     | 
    
         
             
                  # @param entry [Hash] entry in folder
         
     | 
| 
       227 
236 
     | 
    
         
             
                  # @return [Boolean] true if target information is available
         
     | 
| 
       228 
237 
     | 
    
         
             
                  def entry_has_link_information(entry)
         
     | 
| 
       229 
     | 
    
         
            -
                    #  
     | 
| 
      
 238 
     | 
    
         
            +
                    # If target information is missing in folder, try to get it on entry
         
     | 
| 
       230 
239 
     | 
    
         
             
                    if entry['target_node_id'].nil? || entry['target_id'].nil?
         
     | 
| 
       231 
240 
     | 
    
         
             
                      link_entry = read("files/#{entry['id']}")
         
     | 
| 
       232 
241 
     | 
    
         
             
                      entry['target_node_id'] = link_entry['target_node_id']
         
     | 
| 
         @@ -238,27 +247,28 @@ module Aspera 
     | 
|
| 
       238 
247 
     | 
    
         
             
                  end
         
     | 
| 
       239 
248 
     | 
    
         | 
| 
       240 
249 
     | 
    
         
             
                  # Recursively browse in a folder (with non-recursive method)
         
     | 
| 
       241 
     | 
    
         
            -
                  #  
     | 
| 
       242 
     | 
    
         
            -
                  #  
     | 
| 
      
 250 
     | 
    
         
            +
                  # Entries of folders are processed if the processing method returns true
         
     | 
| 
      
 251 
     | 
    
         
            +
                  # Links are processed on the respective node
         
     | 
| 
       243 
252 
     | 
    
         
             
                  # @param method_sym [Symbol] processing method, arguments: entry, path, state
         
     | 
| 
       244 
253 
     | 
    
         
             
                  # @param state [Object] state object sent to processing method
         
     | 
| 
       245 
254 
     | 
    
         
             
                  # @param top_file_id [String] file id to start at (default = access key root file id)
         
     | 
| 
       246 
255 
     | 
    
         
             
                  # @param top_file_path [String] path of top folder (default = /)
         
     | 
| 
       247 
     | 
    
         
            -
                   
     | 
| 
      
 256 
     | 
    
         
            +
                  # @para query [Hash, nil] optional query for `read`
         
     | 
| 
      
 257 
     | 
    
         
            +
                  def process_folder_tree(method_sym:, state:, top_file_id:, top_file_path: '/', query: nil)
         
     | 
| 
       248 
258 
     | 
    
         
             
                    Aspera.assert(!top_file_path.nil?){'top_file_path not set'}
         
     | 
| 
       249 
259 
     | 
    
         
             
                    Log.log.debug{"process_folder_tree: node=#{@app_info ? @app_info[:node_info]['id'] : 'nil'}, file id=#{top_file_id},  path=#{top_file_path}"}
         
     | 
| 
       250 
     | 
    
         
            -
                    #  
     | 
| 
      
 260 
     | 
    
         
            +
                    # Start at top folder
         
     | 
| 
       251 
261 
     | 
    
         
             
                    folders_to_explore = [{id: top_file_id, path: top_file_path}]
         
     | 
| 
       252 
262 
     | 
    
         
             
                    Log.dump(:folders_to_explore, folders_to_explore)
         
     | 
| 
       253 
263 
     | 
    
         
             
                    until folders_to_explore.empty?
         
     | 
| 
       254 
     | 
    
         
            -
                      #  
     | 
| 
      
 264 
     | 
    
         
            +
                      # Consume first in job list
         
     | 
| 
       255 
265 
     | 
    
         
             
                      current_item = folders_to_explore.shift
         
     | 
| 
       256 
266 
     | 
    
         
             
                      Log.log.debug{"Exploring #{current_item[:path]}".bg_green}
         
     | 
| 
       257 
     | 
    
         
            -
                      #  
     | 
| 
      
 267 
     | 
    
         
            +
                      # Get folder content
         
     | 
| 
       258 
268 
     | 
    
         
             
                      folder_contents =
         
     | 
| 
       259 
269 
     | 
    
         
             
                        begin
         
     | 
| 
       260 
270 
     | 
    
         
             
                          # TODO: use header
         
     | 
| 
       261 
     | 
    
         
            -
                           
     | 
| 
      
 271 
     | 
    
         
            +
                          read("files/#{current_item[:id]}/files", query, **self.class.cache_control)
         
     | 
| 
       262 
272 
     | 
    
         
             
                        rescue StandardError => e
         
     | 
| 
       263 
273 
     | 
    
         
             
                          Log.log.warn{"#{current_item[:path]}: #{e.class} #{e.message}"}
         
     | 
| 
       264 
274 
     | 
    
         
             
                          []
         
     | 
| 
         @@ -271,9 +281,9 @@ module Aspera 
     | 
|
| 
       271 
281 
     | 
    
         
             
                        end
         
     | 
| 
       272 
282 
     | 
    
         
             
                        current_path = File.join(current_item[:path], entry['name'])
         
     | 
| 
       273 
283 
     | 
    
         
             
                        Log.log.debug{"process_folder_tree: checking #{current_path}"}
         
     | 
| 
       274 
     | 
    
         
            -
                        #  
     | 
| 
      
 284 
     | 
    
         
            +
                        # Call block, continue only if method returns true
         
     | 
| 
       275 
285 
     | 
    
         
             
                        next unless send(method_sym, entry, current_path, state)
         
     | 
| 
       276 
     | 
    
         
            -
                        #  
     | 
| 
      
 286 
     | 
    
         
            +
                        # Entry type is file, folder or link
         
     | 
| 
       277 
287 
     | 
    
         
             
                        case entry['type']
         
     | 
| 
       278 
288 
     | 
    
         
             
                        when 'folder'
         
     | 
| 
       279 
289 
     | 
    
         
             
                          folders_to_explore.push({id: entry['id'], path: current_path})
         
     | 
| 
         @@ -303,9 +313,9 @@ module Aspera 
     | 
|
| 
       303 
313 
     | 
    
         
             
                    process_last_link ||= path.end_with?(PATH_SEPARATOR)
         
     | 
| 
       304 
314 
     | 
    
         
             
                    path_elements = path.split(PATH_SEPARATOR).reject(&:empty?)
         
     | 
| 
       305 
315 
     | 
    
         
             
                    return {api: self, file_id: top_file_id} if path_elements.empty?
         
     | 
| 
       306 
     | 
    
         
            -
                    resolve_state = {path: path_elements, result: nil, process_last_link: process_last_link}
         
     | 
| 
      
 316 
     | 
    
         
            +
                    resolve_state = {path: path_elements, consumed: [], result: nil, process_last_link: process_last_link}
         
     | 
| 
       307 
317 
     | 
    
         
             
                    process_folder_tree(method_sym: :process_api_fid, state: resolve_state, top_file_id: top_file_id)
         
     | 
| 
       308 
     | 
    
         
            -
                    raise " 
     | 
| 
      
 318 
     | 
    
         
            +
                    raise ParameterError, "Entry not found: #{resolve_state[:path].first} in /#{resolve_state[:consumed].join(PATH_SEPARATOR)}" if resolve_state[:result].nil?
         
     | 
| 
       309 
319 
     | 
    
         
             
                    Log.log.debug{"resolve_api_fid: #{path} -> #{resolve_state[:result][:api].base_url} #{resolve_state[:result][:file_id]}"}
         
     | 
| 
       310 
320 
     | 
    
         
             
                    return resolve_state[:result]
         
     | 
| 
       311 
321 
     | 
    
         
             
                  end
         
     | 
| 
         @@ -338,14 +348,14 @@ module Aspera 
     | 
|
| 
       338 
348 
     | 
    
         
             
                      source_paths =
         
     | 
| 
       339 
349 
     | 
    
         
             
                        case file_info['type']
         
     | 
| 
       340 
350 
     | 
    
         
             
                        when 'file'
         
     | 
| 
       341 
     | 
    
         
            -
                          #  
     | 
| 
      
 351 
     | 
    
         
            +
                          # If the single source is a file, we need to split into folder path and filename
         
     | 
| 
       342 
352 
     | 
    
         
             
                          src_dir_elements = source_folder.split(Api::Node::PATH_SEPARATOR)
         
     | 
| 
       343 
353 
     | 
    
         
             
                          filename = src_dir_elements.pop
         
     | 
| 
       344 
354 
     | 
    
         
             
                          apifid = resolve_api_fid(top_file_id, src_dir_elements.join(Api::Node::PATH_SEPARATOR), true)
         
     | 
| 
       345 
     | 
    
         
            -
                          #  
     | 
| 
      
 355 
     | 
    
         
            +
                          # Filename is the last one, source folder is what remains
         
     | 
| 
       346 
356 
     | 
    
         
             
                          [{'source' => filename}]
         
     | 
| 
       347 
357 
     | 
    
         
             
                        when 'link', 'folder'
         
     | 
| 
       348 
     | 
    
         
            -
                          #  
     | 
| 
      
 358 
     | 
    
         
            +
                          # Single source is 'folder' or 'link'
         
     | 
| 
       349 
359 
     | 
    
         
             
                          # TODO: add this ? , 'destination'=>file_info['name']
         
     | 
| 
       350 
360 
     | 
    
         
             
                          [{'source' => '.'}]
         
     | 
| 
       351 
361 
     | 
    
         
             
                        else Aspera.error_unexpected_value(file_info['type']){'source type'}
         
     | 
| 
         @@ -354,6 +364,9 @@ module Aspera 
     | 
|
| 
       354 
364 
     | 
    
         
             
                    [apifid, source_paths]
         
     | 
| 
       355 
365 
     | 
    
         
             
                  end
         
     | 
| 
       356 
366 
     | 
    
         | 
| 
      
 367 
     | 
    
         
            +
                  # Recursively find files matching lambda
         
     | 
| 
      
 368 
     | 
    
         
            +
                  # @param top_file_id [String] Search root
         
     | 
| 
      
 369 
     | 
    
         
            +
                  # @param test_lambda [Proc] Test function
         
     | 
| 
       357 
370 
     | 
    
         
             
                  def find_files(top_file_id, test_lambda)
         
     | 
| 
       358 
371 
     | 
    
         
             
                    Log.log.debug{"find_files: file id=#{top_file_id}"}
         
     | 
| 
       359 
372 
     | 
    
         
             
                    find_state = {found: [], test_lambda: test_lambda}
         
     | 
| 
         @@ -361,25 +374,28 @@ module Aspera 
     | 
|
| 
       361 
374 
     | 
    
         
             
                    return find_state[:found]
         
     | 
| 
       362 
375 
     | 
    
         
             
                  end
         
     | 
| 
       363 
376 
     | 
    
         | 
| 
       364 
     | 
    
         
            -
                   
     | 
| 
      
 377 
     | 
    
         
            +
                  # Recursively list all files and folders
         
     | 
| 
      
 378 
     | 
    
         
            +
                  def list_files(top_file_id, query: nil)
         
     | 
| 
       365 
379 
     | 
    
         
             
                    find_state = {found: []}
         
     | 
| 
       366 
     | 
    
         
            -
                    process_folder_tree(method_sym: :process_list_files, state: find_state, top_file_id: top_file_id)
         
     | 
| 
      
 380 
     | 
    
         
            +
                    process_folder_tree(method_sym: :process_list_files, state: find_state, top_file_id: top_file_id, query: query)
         
     | 
| 
       367 
381 
     | 
    
         
             
                    return find_state[:found]
         
     | 
| 
       368 
382 
     | 
    
         
             
                  end
         
     | 
| 
       369 
383 
     | 
    
         | 
| 
      
 384 
     | 
    
         
            +
                  # Generate a refreshed auth token
         
     | 
| 
       370 
385 
     | 
    
         
             
                  def refreshed_transfer_token
         
     | 
| 
       371 
386 
     | 
    
         
             
                    return oauth.authorization(refresh: true)
         
     | 
| 
       372 
387 
     | 
    
         
             
                  end
         
     | 
| 
       373 
388 
     | 
    
         | 
| 
       374 
     | 
    
         
            -
                  #  
     | 
| 
      
 389 
     | 
    
         
            +
                  # Get generic part of transfer spec with transport parameters only
         
     | 
| 
      
 390 
     | 
    
         
            +
                  # @return [Hash] Base transfer spec
         
     | 
| 
       375 
391 
     | 
    
         
             
                  def transport_params
         
     | 
| 
       376 
392 
     | 
    
         
             
                    if @std_t_spec_cache.nil?
         
     | 
| 
       377 
     | 
    
         
            -
                      #  
     | 
| 
      
 393 
     | 
    
         
            +
                      # Retrieve values from API (and keep a copy/cache)
         
     | 
| 
       378 
394 
     | 
    
         
             
                      full_spec = create(
         
     | 
| 
       379 
395 
     | 
    
         
             
                        'files/download_setup',
         
     | 
| 
       380 
396 
     | 
    
         
             
                        {transfer_requests: [{transfer_request: {paths: [{source: '/'}]}}]}
         
     | 
| 
       381 
397 
     | 
    
         
             
                      )['transfer_specs'].first['transfer_spec']
         
     | 
| 
       382 
     | 
    
         
            -
                      #  
     | 
| 
      
 398 
     | 
    
         
            +
                      # Set available fields
         
     | 
| 
       383 
399 
     | 
    
         
             
                      @std_t_spec_cache = Transfer::Spec::TRANSPORT_FIELDS.each_with_object({}) do |i, h|
         
     | 
| 
       384 
400 
     | 
    
         
             
                        h[i] = full_spec[i] if full_spec.key?(i)
         
     | 
| 
       385 
401 
     | 
    
         
             
                      end
         
     | 
| 
         @@ -388,9 +404,9 @@ module Aspera 
     | 
|
| 
       388 
404 
     | 
    
         
             
                  end
         
     | 
| 
       389 
405 
     | 
    
         | 
| 
       390 
406 
     | 
    
         
             
                  # Create transfer spec for gen4
         
     | 
| 
       391 
     | 
    
         
            -
                  # @param file_id  
     | 
| 
       392 
     | 
    
         
            -
                  # @param direction  
     | 
| 
       393 
     | 
    
         
            -
                  # @param ts_merge  
     | 
| 
      
 407 
     | 
    
         
            +
                  # @param file_id   [String]   Destination or source folder (id)
         
     | 
| 
      
 408 
     | 
    
         
            +
                  # @param direction [Symbol]   One of Transfer::Spec::DIRECTION_SEND, Transfer::Spec::DIRECTION_RECEIVE
         
     | 
| 
      
 409 
     | 
    
         
            +
                  # @param ts_merge  [Hash,nil] Additional transfer spec to merge
         
     | 
| 
       394 
410 
     | 
    
         
             
                  def transfer_spec_gen4(file_id, direction, ts_merge = nil)
         
     | 
| 
       395 
411 
     | 
    
         
             
                    ak_name = nil
         
     | 
| 
       396 
412 
     | 
    
         
             
                    ak_token = nil
         
     | 
| 
         @@ -402,7 +418,7 @@ module Aspera 
     | 
|
| 
       402 
418 
     | 
    
         
             
                    when :oauth2
         
     | 
| 
       403 
419 
     | 
    
         
             
                      ak_name = params[:headers][HEADER_X_ASPERA_ACCESS_KEY]
         
     | 
| 
       404 
420 
     | 
    
         
             
                      # TODO: token_generation_lambda = lambda{|do_refresh|oauth.authorization(refresh: do_refresh)}
         
     | 
| 
       405 
     | 
    
         
            -
                      #  
     | 
| 
      
 421 
     | 
    
         
            +
                      # Get bearer token, possibly use cache
         
     | 
| 
       406 
422 
     | 
    
         
             
                      ak_token = oauth.authorization
         
     | 
| 
       407 
423 
     | 
    
         
             
                    when :none
         
     | 
| 
       408 
424 
     | 
    
         
             
                      ak_name = params[:headers][HEADER_X_ASPERA_ACCESS_KEY]
         
     | 
| 
         @@ -417,92 +433,91 @@ module Aspera 
     | 
|
| 
       417 
433 
     | 
    
         
             
                          'node' => {
         
     | 
| 
       418 
434 
     | 
    
         
             
                            'access_key' => ak_name,
         
     | 
| 
       419 
435 
     | 
    
         
             
                            'file_id'    => file_id
         
     | 
| 
       420 
     | 
    
         
            -
                          } 
     | 
| 
       421 
     | 
    
         
            -
                        } 
     | 
| 
       422 
     | 
    
         
            -
                      } 
     | 
| 
      
 436 
     | 
    
         
            +
                          }
         
     | 
| 
      
 437 
     | 
    
         
            +
                        }
         
     | 
| 
      
 438 
     | 
    
         
            +
                      }
         
     | 
| 
       423 
439 
     | 
    
         
             
                    }
         
     | 
| 
       424 
     | 
    
         
            -
                    #  
     | 
| 
      
 440 
     | 
    
         
            +
                    # Add specials tags (cos)
         
     | 
| 
       425 
441 
     | 
    
         
             
                    add_tspec_info(transfer_spec)
         
     | 
| 
       426 
442 
     | 
    
         
             
                    transfer_spec.deep_merge!(ts_merge) unless ts_merge.nil?
         
     | 
| 
       427 
     | 
    
         
            -
                    #  
     | 
| 
       428 
     | 
    
         
            -
                    app_info[:api].add_ts_tags(transfer_spec: transfer_spec, app_info: app_info) unless app_info.nil?
         
     | 
| 
       429 
     | 
    
         
            -
                    #  
     | 
| 
      
 443 
     | 
    
         
            +
                    # Add application specific tags (AoC)
         
     | 
| 
      
 444 
     | 
    
         
            +
                    @app_info[:api].add_ts_tags(transfer_spec: transfer_spec, app_info: @app_info) unless @app_info.nil?
         
     | 
| 
      
 445 
     | 
    
         
            +
                    # Add remote host info
         
     | 
| 
       430 
446 
     | 
    
         
             
                    if self.class.use_standard_ports
         
     | 
| 
       431 
     | 
    
         
            -
                      #  
     | 
| 
      
 447 
     | 
    
         
            +
                      # Get default TCP/UDP ports and transfer user
         
     | 
| 
       432 
448 
     | 
    
         
             
                      transfer_spec.merge!(Transfer::Spec::AK_TSPEC_BASE)
         
     | 
| 
       433 
     | 
    
         
            -
                      #  
     | 
| 
      
 449 
     | 
    
         
            +
                      # By default: same address as node API
         
     | 
| 
       434 
450 
     | 
    
         
             
                      transfer_spec['remote_host'] = URI.parse(base_url).host
         
     | 
| 
       435 
451 
     | 
    
         
             
                      # AoC allows specification of other url
         
     | 
| 
       436 
452 
     | 
    
         
             
                      transfer_spec['remote_host'] = @app_info[:node_info]['transfer_url'] if !@app_info.nil? && !@app_info[:node_info]['transfer_url'].nil? && !@app_info[:node_info]['transfer_url'].empty?
         
     | 
| 
       437 
453 
     | 
    
         
             
                      info = read('info')
         
     | 
| 
       438 
     | 
    
         
            -
                      #  
     | 
| 
      
 454 
     | 
    
         
            +
                      # Get the transfer user from info on access key
         
     | 
| 
       439 
455 
     | 
    
         
             
                      transfer_spec['remote_user'] = info['transfer_user'] if info['transfer_user']
         
     | 
| 
       440 
     | 
    
         
            -
                      #  
     | 
| 
      
 456 
     | 
    
         
            +
                      # Get settings from name.value array to hash key.value
         
     | 
| 
       441 
457 
     | 
    
         
             
                      settings = info['settings']&.each_with_object({}){ |i, h| h[i['name']] = i['value']}
         
     | 
| 
       442 
     | 
    
         
            -
                      #  
     | 
| 
      
 458 
     | 
    
         
            +
                      # Check WSS ports
         
     | 
| 
       443 
459 
     | 
    
         
             
                      Transfer::Spec::WSS_FIELDS.each do |i|
         
     | 
| 
       444 
460 
     | 
    
         
             
                        transfer_spec[i] = settings[i] if settings.key?(i)
         
     | 
| 
       445 
461 
     | 
    
         
             
                      end if settings.is_a?(Hash)
         
     | 
| 
       446 
462 
     | 
    
         
             
                    else
         
     | 
| 
       447 
463 
     | 
    
         
             
                      transfer_spec.merge!(transport_params)
         
     | 
| 
       448 
464 
     | 
    
         
             
                    end
         
     | 
| 
       449 
     | 
    
         
            -
                     
     | 
| 
       450 
     | 
    
         
            -
                      unless transfer_spec['remote_user'].eql?(Transfer::Spec::ACCESS_KEY_TRANSFER_USER)
         
     | 
| 
      
 465 
     | 
    
         
            +
                    Aspera.assert_values(transfer_spec['remote_user'], Transfer::Spec::ACCESS_KEY_TRANSFER_USER, type: :warn){'transfer user'}
         
     | 
| 
       451 
466 
     | 
    
         
             
                    return transfer_spec
         
     | 
| 
       452 
467 
     | 
    
         
             
                  end
         
     | 
| 
       453 
468 
     | 
    
         | 
| 
       454 
469 
     | 
    
         
             
                  private
         
     | 
| 
       455 
470 
     | 
    
         | 
| 
       456 
     | 
    
         
            -
                  #  
     | 
| 
      
 471 
     | 
    
         
            +
                  # Method called in loop for each entry for `resolve_api_fid`
         
     | 
| 
      
 472 
     | 
    
         
            +
                  # @return `true` to continue digging, `false` to stop processing: set state[:result] if found
         
     | 
| 
       457 
473 
     | 
    
         
             
                  def process_api_fid(entry, path, state)
         
     | 
| 
       458 
     | 
    
         
            -
                    #  
     | 
| 
      
 474 
     | 
    
         
            +
                    # Stop digging here if not in right path
         
     | 
| 
       459 
475 
     | 
    
         
             
                    return false unless entry['name'].eql?(state[:path].first)
         
     | 
| 
       460 
     | 
    
         
            -
                    #  
     | 
| 
       461 
     | 
    
         
            -
                    state[:path].shift
         
     | 
| 
      
 476 
     | 
    
         
            +
                    # Ok it matches, so we remove the match, and continue digging
         
     | 
| 
      
 477 
     | 
    
         
            +
                    state[:consumed].push(state[:path].shift)
         
     | 
| 
       462 
478 
     | 
    
         
             
                    path_fully_consumed = state[:path].empty?
         
     | 
| 
       463 
479 
     | 
    
         
             
                    case entry['type']
         
     | 
| 
       464 
480 
     | 
    
         
             
                    when 'file'
         
     | 
| 
       465 
     | 
    
         
            -
                      #  
     | 
| 
      
 481 
     | 
    
         
            +
                      # File must be terminal
         
     | 
| 
       466 
482 
     | 
    
         
             
                      raise "#{entry['name']} is a file, expecting folder to find: #{state[:path]}" unless path_fully_consumed
         
     | 
| 
       467 
     | 
    
         
            -
                      #  
     | 
| 
       468 
     | 
    
         
            -
                      Log.log.debug{" 
     | 
| 
      
 483 
     | 
    
         
            +
                      # It's terminal, we found it
         
     | 
| 
      
 484 
     | 
    
         
            +
                      Log.log.debug{"process_api_fid: found #{path} -> #{entry['id']}"}
         
     | 
| 
       469 
485 
     | 
    
         
             
                      state[:result] = {api: self, file_id: entry['id']}
         
     | 
| 
       470 
486 
     | 
    
         
             
                      return false
         
     | 
| 
       471 
487 
     | 
    
         
             
                    when 'folder'
         
     | 
| 
       472 
488 
     | 
    
         
             
                      if path_fully_consumed
         
     | 
| 
       473 
     | 
    
         
            -
                        #  
     | 
| 
      
 489 
     | 
    
         
            +
                        # We found it
         
     | 
| 
       474 
490 
     | 
    
         
             
                        state[:result] = {api: self, file_id: entry['id']}
         
     | 
| 
       475 
491 
     | 
    
         
             
                        return false
         
     | 
| 
       476 
492 
     | 
    
         
             
                      end
         
     | 
| 
       477 
493 
     | 
    
         
             
                    when 'link'
         
     | 
| 
       478 
494 
     | 
    
         
             
                      if path_fully_consumed
         
     | 
| 
       479 
495 
     | 
    
         
             
                        if state[:process_last_link]
         
     | 
| 
       480 
     | 
    
         
            -
                          #  
     | 
| 
      
 496 
     | 
    
         
            +
                          # We found it
         
     | 
| 
       481 
497 
     | 
    
         
             
                          other_node = nil
         
     | 
| 
       482 
498 
     | 
    
         
             
                          other_node = node_id_to_node(entry['target_node_id']) if entry_has_link_information(entry)
         
     | 
| 
       483 
499 
     | 
    
         
             
                          raise Error, 'Cannot resolve link' if other_node.nil?
         
     | 
| 
       484 
500 
     | 
    
         
             
                          state[:result] = {api: other_node, file_id: entry['target_id']}
         
     | 
| 
       485 
501 
     | 
    
         
             
                        else
         
     | 
| 
       486 
     | 
    
         
            -
                          #  
     | 
| 
      
 502 
     | 
    
         
            +
                          # We found it but we do not process the link
         
     | 
| 
       487 
503 
     | 
    
         
             
                          state[:result] = {api: self, file_id: entry['id']}
         
     | 
| 
       488 
504 
     | 
    
         
             
                        end
         
     | 
| 
       489 
505 
     | 
    
         
             
                        return false
         
     | 
| 
       490 
506 
     | 
    
         
             
                      end
         
     | 
| 
       491 
     | 
    
         
            -
                    else
         
     | 
| 
       492 
     | 
    
         
            -
                      Log.log.warn{"Unknown element type: #{entry['type']}"}
         
     | 
| 
      
 507 
     | 
    
         
            +
                    else Aspera.error_unexpected_value(entry['type'], type: :warn){'folder entry type'}
         
     | 
| 
       493 
508 
     | 
    
         
             
                    end
         
     | 
| 
       494 
     | 
    
         
            -
                    #  
     | 
| 
      
 509 
     | 
    
         
            +
                    # Continue to dig folder
         
     | 
| 
       495 
510 
     | 
    
         
             
                    return true
         
     | 
| 
       496 
511 
     | 
    
         
             
                  end
         
     | 
| 
       497 
512 
     | 
    
         | 
| 
       498 
     | 
    
         
            -
                  #  
     | 
| 
      
 513 
     | 
    
         
            +
                  # Method called in loop for each entry for `find_files`
         
     | 
| 
       499 
514 
     | 
    
         
             
                  def process_find_files(entry, path, state)
         
     | 
| 
       500 
515 
     | 
    
         
             
                    state[:found].push(entry.merge({'path' => path})) if state[:test_lambda].call(entry)
         
     | 
| 
       501 
     | 
    
         
            -
                    #  
     | 
| 
      
 516 
     | 
    
         
            +
                    # Test all files deeply
         
     | 
| 
       502 
517 
     | 
    
         
             
                    return true
         
     | 
| 
       503 
518 
     | 
    
         
             
                  end
         
     | 
| 
       504 
519 
     | 
    
         | 
| 
       505 
     | 
    
         
            -
                  #  
     | 
| 
      
 520 
     | 
    
         
            +
                  # Method called in loop for each entry for `list_files`
         
     | 
| 
       506 
521 
     | 
    
         
             
                  def process_list_files(entry, path, state)
         
     | 
| 
       507 
522 
     | 
    
         
             
                    state[:found].push(entry.merge({'path' => path}))
         
     | 
| 
       508 
523 
     | 
    
         
             
                    return false
         
     |