Class: Aspera::Rest

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

Overview

a simple class to make HTTP calls, equivalent to rest-client rest call errors are raised as exception RestCallError and error are analyzed in RestErrorAnalyzer

Direct Known Subclasses

AoC, AtsApi, CosNode, Node

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(a_rest_params) ⇒ Rest

:type (:basic, :oauth2, :url) :username [:basic] :password [:basic] :url_creds [:url] :session_cb a lambda which takes @http_session as arg, use this to change parameters :* [:oauth2] see Oauth class

Parameters:

  • a_rest_params

    default call parameters and authentication (:auth) :



109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/aspera/rest.rb', line 109

def initialize(a_rest_params)
  raise "ERROR: expecting Hash" unless a_rest_params.is_a?(Hash)
  raise "ERROR: expecting base_url" unless a_rest_params[:base_url].is_a?(String)
  @params=a_rest_params.clone
  Log.dump('REST params',@params)
  # base url without trailing slashes (note: string may be frozen)
  @params[:base_url]=@params[:base_url].gsub(/\/+$/,'')
  @http_session=nil
  # default is no auth
  @params[:auth]||={:type=>:none}
  @params[:not_auth_codes]||=['401']
  # translate old auth parameters, remove prefix, place in auth
  [:auth,:basic,:oauth].each do |p_sym|
    p_str=p_sym.to_s+'_'
    @params.keys.select{|k|k.to_s.start_with?(p_str)}.each do |k_sym|
      name=k_sym.to_s[p_str.length..-1]
      name='grant' if k_sym.eql?(:oauth_type)
      @params[:auth][name.to_sym]=@params[k_sym]
      @params.delete(k_sym)
    end
  end
  @oauth=Oauth.new(@params[:auth]) if @params[:auth][:type].eql?(:oauth2)
  Log.dump('REST params(2)',@params)
end

Instance Attribute Details

#paramsObject (readonly)

Returns the value of attribute params.



100
101
102
# File 'lib/aspera/rest.rb', line 100

def params
  @params
end

Class Method Details

.basic_creds(user, pass) ⇒ Object



48
# File 'lib/aspera/rest.rb', line 48

def self.basic_creds(user,pass); return "Basic #{Base64.strict_encode64("#{user}:#{pass}")}";end

.build_uri(url, params = nil) ⇒ Object

build URI from URL and parameters and check it is http or https



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/aspera/rest.rb', line 51

def self.build_uri(url,params=nil)
  uri=URI.parse(url)
  raise "REST endpoint shall be http(s)" unless ['http','https'].include?(uri.scheme)
  if !params.nil?
    # support array url params, there is no standard. Either p[]=1&p[]=2, or p=1&p=2
    if params.is_a?(Hash)
      orig=params
      params=[]
      orig.each do |k,v|
        case v
        when Array
          suffix=v.first.eql?('[]') ? v.shift : ''
          v.each do |e|
            params.push([k+suffix,e])
          end
        else
          params.push([k,v])
        end
      end
    end
    # CGI.unescape to transform back %5D into []
    uri.query=CGI.unescape(URI.encode_www_form(params))
  end
  return uri
end

.debug=(flag) ⇒ Object



46
# File 'lib/aspera/rest.rb', line 46

def self.debug=(flag); @@debug=flag; Log.log.debug("debug http => #{flag}"); end

.insecureObject



40
# File 'lib/aspera/rest.rb', line 40

def self.insecure; @@insecure;end

.insecure=(v) ⇒ Object



38
# File 'lib/aspera/rest.rb', line 38

def self.insecure=(v); @@insecure=v;Log.log.debug("insecure => #{@@insecure}".red);end

.user_agentObject



44
# File 'lib/aspera/rest.rb', line 44

def self.user_agent; @@user_agent;end

.user_agent=(v) ⇒ Object



42
# File 'lib/aspera/rest.rb', line 42

def self.user_agent=(v); @@user_agent=v;Log.log.debug("user_agent => #{@@user_agent}".red);end

Instance Method Details

#call(call_data) ⇒ Object

HTTP/S REST call call_data has keys: :auth :operation :subpath :headers :json_params :url_params :www_body_params :text_body_params :save_to_file (filepath) :return_error (bool)



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
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
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
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
# File 'lib/aspera/rest.rb', line 151

def call(call_data)
  raise "Hash call parameter is required (#{call_data.class})" unless call_data.is_a?(Hash)
  Log.log.debug("accessing #{call_data[:subpath]}".red.bold.bg_green)
  call_data[:headers]||={}
  call_data[:headers]['User-Agent'] ||= @@user_agent
  call_data=@params.deep_merge(call_data)
  case call_data[:auth][:type]
  when :none
    # no auth
  when :basic
    Log.log.debug("using Basic auth")
    basic_auth_data=[call_data[:auth][:username],call_data[:auth][:password]]
  when :oauth2
    call_data[:headers]['Authorization']=oauth_token unless call_data[:headers].has_key?('Authorization')
  when :url
    call_data[:url_params]||={}
    call_data[:auth][:url_creds].each do |key, value|
      call_data[:url_params][key]=value
    end
  else raise "unsupported auth type: [#{call_data[:auth][:type]}]"
  end
  # TODO: shall we percent encode subpath (spaces) test with access key delete with space in id
  # URI.escape()
  uri=self.class.build_uri("#{@params[:base_url]}#{call_data[:subpath].nil? ? '' : '/'}#{call_data[:subpath]}",call_data[:url_params])
  Log.log.debug("URI=#{uri}")
  begin
    # instanciate request object based on string name
    req=Object::const_get('Net::HTTP::'+call_data[:operation].capitalize).new(uri.request_uri)
  rescue NameError => e
    raise "unsupported operation : #{call_data[:operation]}"
  end
  if call_data.has_key?(:json_params) and !call_data[:json_params].nil? then
    req.body=JSON.generate(call_data[:json_params])
    Log.dump('body JSON data',call_data[:json_params])
    #Log.log.debug("body JSON data=#{JSON.pretty_generate(call_data[:json_params])}")
    req['Content-Type'] = 'application/json'
    #call_data[:headers]['Accept']='application/json'
  end
  if call_data.has_key?(:www_body_params) then
    req.body=URI.encode_www_form(call_data[:www_body_params])
    Log.log.debug("body www data=#{req.body.chomp}")
    req['Content-Type'] = 'application/x-www-form-urlencoded'
  end
  if call_data.has_key?(:text_body_params) then
    req.body=call_data[:text_body_params]
    Log.log.debug("body data=#{req.body.chomp}")
  end
  # set headers
  if call_data.has_key?(:headers) then
    call_data[:headers].keys.each do |key|
      req[key] = call_data[:headers][key]
    end
  end
  # :type = :basic
  req.basic_auth(*basic_auth_data) unless basic_auth_data.nil?

  Log.log.debug("call_data = #{call_data}")
  result={:http=>nil}
  begin
    # we try the call, and will retry only if oauth, as we can, first with refresh, and then re-auth if refresh is bad
    oauth_tries ||= 2
    Log.log.debug("send request")
    http_session.request(req) do |response|
      result[:http] = response
      if call_data.has_key?(:save_to_file)
        total_size=result[:http]['Content-Length'].to_i
        progress=ProgressBar.create(
        :format     => '%a %B %p%% %r KB/sec %e',
        :rate_scale => lambda{|rate|rate/1024},
        :title      => 'progress',
        :total      => total_size)
        Log.log.debug("before write file")
        target_file=call_data[:save_to_file]
        # override user's path to path in header
        if !response['Content-Disposition'].nil? and m=response['Content-Disposition'].match(/filename="([^"]+)"/)
          target_file=m[1]
        end
        # download with temp filename
        target_file_tmp=target_file+'.http.partial'
        Log.log.debug("saving to: #{target_file}")
        File.open(target_file_tmp, "wb") do |file|
          result[:http].read_body do |fragment|
            file.write(fragment)
            new_process=progress.progress+fragment.length
            new_process = total_size if new_process > total_size
            progress.progress=new_process
          end
        end
        # rename at the end
        File.rename(target_file_tmp, target_file)
        progress=nil
      end
    end
    # sometimes there is a ITF8 char (e.g. (c) )
    result[:http].body.force_encoding("UTF-8") if result[:http].body.is_a?(String)
    Log.log.debug("result: body=#{result[:http].body}")
    result_mime=(result[:http]['Content-Type']||'text/plain').split(';').first
    case result_mime
    when 'application/json','application/vnd.api+json'
      result[:data]=JSON.parse(result[:http].body) rescue nil
    else #when 'text/plain'
      result[:data]=result[:http].body
    end
    Log.dump("result: parsed: #{result_mime}",result[:data])
    Log.log.debug("result: code=#{result[:http].code}")
    RestErrorAnalyzer.instance.raiseOnError(req,result)
  rescue RestCallError => e
    # not authorized: oauth token expired
    if @params[:not_auth_codes].include?(result[:http].code.to_s) and call_data[:auth][:type].eql?(:oauth2)
      begin
        # try to use refresh token
        req['Authorization']=oauth_token(refresh: true)
      rescue RestCallError => e
        Log.log.error("refresh failed".bg_red)
        # regenerate a brand new token
        req['Authorization']=oauth_token
      end
      Log.log.debug("using new token=#{call_data[:headers]['Authorization']}")
      retry unless (oauth_tries -= 1).zero?
    end # if
    # raise exception if could not retry and not return error in result
    raise e unless call_data[:return_error]
  end
  Log.log.debug("result=#{result}")
  return result

end

#cancel(subpath) ⇒ Object



300
301
302
# File 'lib/aspera/rest.rb', line 300

def cancel(subpath)
  return call({:operation=>'CANCEL',:subpath=>subpath,:headers=>{'Accept'=>'application/json'}})
end

#create(subpath, params, encoding = :json_params) ⇒ Object

Parameters:

  • encoding (defaults to: :json_params)

    : one of: :json_params, :url_params



284
285
286
# File 'lib/aspera/rest.rb', line 284

def create(subpath,params,encoding=:json_params)
  return call({:operation=>'POST',:subpath=>subpath,:headers=>{'Accept'=>'application/json'},encoding=>params})
end

#delete(subpath) ⇒ Object



296
297
298
# File 'lib/aspera/rest.rb', line 296

def delete(subpath)
  return call({:operation=>'DELETE',:subpath=>subpath,:headers=>{'Accept'=>'application/json'}})
end

#oauth_token(options = {}) ⇒ Object



134
135
136
137
# File 'lib/aspera/rest.rb', line 134

def oauth_token(options={})
  raise "ERROR: not Oauth" unless @oauth.is_a?(Oauth)
  return @oauth.get_authorization(options)
end

#read(subpath, args = nil) ⇒ Object



288
289
290
# File 'lib/aspera/rest.rb', line 288

def read(subpath,args=nil)
  return call({:operation=>'GET',:subpath=>subpath,:headers=>{'Accept'=>'application/json'},:url_params=>args})
end

#update(subpath, params) ⇒ Object



292
293
294
# File 'lib/aspera/rest.rb', line 292

def update(subpath,params)
  return call({:operation=>'PUT',:subpath=>subpath,:headers=>{'Accept'=>'application/json'},:json_params=>params})
end