QutBioacoustics/baw-workers

View on GitHub
lib/baw-workers/api_communicator.rb

Summary

Maintainability
D
1 day
Test Coverage
module BawWorkers
  class ApiCommunicator

    attr_accessor :logger

    # Create a new BawWorkers::ApiCommunicator.
    # @param [Object] logger
    # @param [Object] login_details
    # @param [Object] endpoints
    # @return [BawWorkers::ApiCommunicator]
    def initialize(logger, login_details, endpoints)
      @logger = logger
      @login_details = login_details
      @endpoints = endpoints

      @class_name = self.class.name
    end

    def host;
      @login_details['host'];
    end

    def port;
      @login_details['port'];
    end

    def user;
      @login_details['user'];
    end

    def password;
      @login_details['password'];
    end

    def endpoint_login
      @endpoints['login']
    end

    def endpoint_audio_recording
      @endpoints['audio_recording']
    end

    def endpoint_audio_recording_create
      @endpoints['audio_recording_create']
    end

    def endpoint_audio_recording_uploader
      @endpoints['audio_recording_uploader']
    end

    def endpoint_audio_recording_update_status
      @endpoints['audio_recording_update_status']
    end

    def endpoint_analysis_jobs_item_update_status
      @endpoints['analysis_jobs_item_update_status']
    end

    # Send HTTP request.
    # @param [string] description
    # @param [Symbol] method
    # @param [String] host
    # @param [Integer] port
    # @param [string] endpoint
    # @param [Hash] security_info
    # @param [Hash] body
    # @return [Net::HTTP::Response] The response.
    def send_request(description, method, host, port, endpoint, security_info = {auth_token: nil, cookies: nil}, body = nil)

      case method
        when :get
          request = Net::HTTP::Get.new(endpoint)
        when :put
          request = Net::HTTP::Put.new(endpoint)
        when :post
          request = Net::HTTP::Post.new(endpoint)
        when :head
          request = Net::HTTP::Head.new(endpoint)
        when :patch
          request = Net::HTTP::Patch.new(endpoint)
        else
          fail ArgumentError, "Unrecognised HTTP method #{method}."
      end

      request['Content-Type'] = 'application/json'
      request['Accept'] = 'application/json'
      request['Authorization'] = "Token token=\"#{security_info[:auth_token]}\"" if security_info && security_info.include?(:auth_token) && !security_info[:auth_token].nil?

      # extract XSRF-TOKEN from cookie, and put into X-XSRF-TOKEN header
      if security_info && security_info.include?(:cookies) && !security_info[:cookies].nil?

        # include cookies if any were set
        request['Cookie'] = security_info[:cookies]

        cookie_strings = security_info[:cookies].split('; ')
        xsrf_cookie = nil
        key_string = 'XSRF-TOKEN='
        cookie_strings.each do |cookie_string|
          if cookie_string.start_with?(key_string)
            xsrf_cookie = cookie_string[key_string.size..-1]
            break
          end
        end

        unless xsrf_cookie.nil?
          url_decoded = URI.decode_www_form_component(xsrf_cookie)
          request['X-XSRF-TOKEN'] = url_decoded
        end

      end

      request.body = body.to_json unless body.blank?

      msg = "#{method} '#{description}'. Url: #{host}:#{port}#{endpoint}"

      response = nil

      begin
        #res = Net::HTTP::Proxy('127.0.0.1', '8888').start(host, port) do |http|

        use_ssl = BawWorkers::Settings.endpoints.use_ssl.to_s
        use_ssl = use_ssl.downcase == 'true' ? true : false

        res = Net::HTTP.start(host, port, use_ssl: use_ssl) do |http|
          response = http.request(request)

          @logger.debug(@class_name) {
            "[HTTP] Sent request for #{msg}, Body: #{request.body}, Headers: #{request.to_hash}"
          }

        end
      rescue Timeout::Error => te
        @logger.error(@class_name) {
          "[HTTP] Request TIMED OUT for #{msg}, Body: #{request.body}, Headers: #{request.to_hash}, Error: #{te}\nBacktrace: #{te.backtrace.join("\n")}"
        }
        raise te
      rescue StandardError => e
        @logger.error(@class_name) {
          "[HTTP] Request for #{msg}, Body: #{request.body}, Headers: #{request.to_hash}, Error: #{e}\nBacktrace: #{e.backtrace.join("\n")}"
        }
        fail e
      end

      @logger.info(@class_name) {
        "[HTTP] Received response for #{msg}, Code: #{response.code}, Body: #{response.body}, Headers: #{response.to_hash}"
      }

      response
    end

    # Request an auth token (using an existing token if available).
    # @return [Hash]
    def request_login
      login_response = send_request('Login request', :post, host, port, endpoint_login, nil, {email: user, password: password})

      # get cookies
      # from http://stackoverflow.com/a/9320190/31567
      all_cookies = login_response.get_fields('set-cookie')

      cookies = nil

      if all_cookies && all_cookies.respond_to?(:each)
        cookies_array = []
        all_cookies.each { |cookie|
          cookies_array.push(cookie.split('; ')[0])
        }
        cookies = cookies_array.join('; ')
      end

      if login_response.code.to_i == 200 && !login_response.body.blank?
        @logger.info(@class_name) {
          '[HTTP] Got auth token in response body.'
        }
        json_resp = JSON.parse(login_response.body)

        {
            auth_token: json_resp['data']['auth_token'],
            cookies: cookies
        }
      else
        @logger.error(@class_name) {
          '[HTTP] Problem requesting auth token.'
        }

        {
            auth_token: nil,
            cookies: cookies
        }
      end
    end

    # Update audio recording metadata
    def update_audio_recording_details(description, file_to_process, audio_recording_id, update_hash, security_info)
      endpoint = endpoint_audio_recording.gsub(':id', audio_recording_id.to_s)
      response = send_request("Update audio recording metadata - #{description}", :put, host, port, endpoint, security_info, update_hash)
      msg = "Code #{response.code}, Id: #{audio_recording_id}, Hash: '#{update_hash}', File: '#{file_to_process}'"

      if response.code.to_i == 200 || response.code.to_i == 204
        @logger.info(@class_name) {
          "[HTTP] Audio recording metadata update '#{description}' succeeded. #{msg}"
        }
        true
      else
        @logger.error(@class_name) {
          "[HTTP] Audio recording metadata update '#{description}' failed. #{msg}"
        }
        false
      end
    end

    # Check that uploader_id has access to project_id.
    # @param [string] project_id
    # @param [string] site_id
    # @param [string] uploader_id
    # @param [Hash] security_info
    # @return [Boolean]
    def check_uploader_project_access(project_id, site_id, uploader_id, security_info)
      if security_info
        if uploader_check_success?(project_id, site_id, uploader_id, security_info)
          @logger.info(@class_name) {
            "[HTTP] Uploader with id #{uploader_id} has access to project id #{project_id}."
          }
          true
        else
          @logger.error(@class_name) {
            "[HTTP] Uploader id #{uploader_id} does not have required permissions for project id #{project_id}."
          }
          false
        end
      else
        @logger.error(@class_name) {
          "[HTTP] No auth token given so cannot check uploader with id #{uploader_id}  access to project id #{project_id}."
        }
        false
      end
    end

    # Send request to check project access.
    # @param [string] project_id
    # @param [string] site_id
    # @param [string] uploader_id
    # @param [Hash] security_info
    # @return [Boolean] true if uploader_id has access to project_id
    def uploader_check_success?(project_id, site_id, uploader_id, security_info)
      endpoint = endpoint_audio_recording_uploader
                     .gsub(':project_id', project_id.to_s)
                     .gsub(':site_id', site_id.to_s)
                     .gsub(':uploader_id', uploader_id.to_s)

      check_uploader_response = send_request('Check uploader id', :get, host, port, endpoint, security_info)
      check_uploader_response.code.to_i == 204
    end

    # Create a new audio recording.
    # @param [String] file_to_process
    # @param [Integer] project_id
    # @param [Integer] site_id
    # @param [Hash] audio_info_hash
    # @param [Hash] security_info
    # @return [Hash] response and response json
    def create_new_audio_recording(file_to_process, project_id, site_id, audio_info_hash, security_info)
      endpoint = endpoint_audio_recording_create
                     .gsub(':project_id', project_id.to_s)
                     .gsub(':site_id', site_id.to_s)

      msg = "Project: #{project_id}, Site: #{site_id}, File: #{file_to_process}, Params: #{audio_info_hash}"

      response = send_request('Create audio recording', :post, host, port, endpoint, security_info, audio_info_hash)
      if response.code.to_i == 201
        response_json = JSON.parse(response.body)
        @logger.info(@class_name) {
          "[HTTP] Created new audio recording. Id: #{response_json['data']['id']}, #{msg}"
        }
        {response: response, response_json: response_json}
      else
        @logger.error(@class_name) {
          "[HTTP] Problem creating new audio recording. #{msg}"
        }
        {response: response, response_json: nil}
      end
    end

    # Update audio recording status.
    # @param [String] description
    # @param [String] file_to_process
    # @param [Integer] audio_recording_id
    # @param [Hash] update_hash
    # @param [Hash] security_info
    # @return [Boolean] successful?
    def update_audio_recording_status(description, file_to_process, audio_recording_id, update_hash, security_info)
      endpoint = endpoint_audio_recording_update_status.gsub(':id', audio_recording_id.to_s)
      response = send_request("Update audio recording status - #{description}", :put, host, port, endpoint, security_info, update_hash)
      msg = "'#{description}'. Code #{response.code}, File: '#{file_to_process}', Id: #{audio_recording_id}, Hash: '#{update_hash}'"
      if response.code.to_i == 200 || response.code.to_i == 204
        @logger.info(@class_name) {
          "[HTTP] Audio recording status updated for #{msg}"
        }
        true
      else
        @logger.error(@class_name) {
          "[HTTP] Problem updating audio recording status for #{msg}"
        }
        false
      end
    end

    # Get the status of an analysis job item
    # @param [Integer] analysis_job_id
    # @param [Integer] audio_recording_id
    # @return [Hash] a hash containing :response,: response_json, and :status
    def get_analysis_jobs_item_status(analysis_job_id, audio_recording_id, security_info)
      endpoint = endpoint_analysis_jobs_item_update_status
                     .gsub(':audio_recording_id', audio_recording_id.to_s)
                     .gsub(':analysis_job_id', analysis_job_id.to_s)

      response = send_request(
          "Getting analysis job item status",
          :get,
          host,
          port,
          endpoint,
          security_info
      )

      msg = "analysis_job_id=#{analysis_job_id}, audio_recording_id=#{audio_recording_id}"

      if response.code.to_i == 200
        response_json = response.body.blank? ? nil : JSON.parse(response.body)
        status = response_json.nil? ? nil : response_json['data']['status']
        @logger.info(@class_name) {
          "[HTTP] Retrieved status for analysis job item: #{status}, #{msg}"
        }
        {response: response, failed: false, response_json: response_json, status: status}
      else
        @logger.error(@class_name) {
          "[HTTP] Problem retrieving status for analysis job item: #{msg}"
        }
        {response: response, failed: true, response_json: nil, status: nil}
      end
    end

    # Update the status of an analysis job item
    # @param [Integer] analysis_job_id
    # @param [Integer] audio_recording_id
    # @return [Hash] a hash containing :response,: response_json, and :status
    # @param [Symbol] status
    def update_analysis_jobs_item_status(analysis_job_id, audio_recording_id, status, security_info)
      approved_statuses = [:working, :successful, :failed, :timed_out, :cancelled]

      raise "Cannot set status to `#{status}`" unless approved_statuses.include?(status)

      endpoint = endpoint_analysis_jobs_item_update_status
                     .gsub(':audio_recording_id', audio_recording_id.to_s)
                     .gsub(':analysis_job_id', analysis_job_id.to_s)
      response = send_request(
          "Update analysis job item status - to `#{status}`",
          :put,
          host,
          port,
          endpoint,
          security_info,
          {
              "status": status
          }
      )

      msg = "analysis_job_id=#{analysis_job_id}, audio_recording_id=#{audio_recording_id}"

      if response.code.to_i == 200 || response.code.to_i == 204
        response_json = response.body.blank? ? nil : JSON.parse(response.body)

        @logger.info(@class_name) {
          "[HTTP] Audio recording status updated for #{msg}"
        }
        {
            response: response,
            failed: false,
            response_json: response_json,
            status: response_json.nil? ? nil : response_json['data']['status']
        }
      else
        @logger.error(@class_name) {
          "[HTTP] Problem updating audio recording status for #{msg}"
        }
        {response: response, failed: true, response_json: nil, status: nil}
      end
    end

  end
end