lib/azure_blob/http.rb



# frozen_string_literal: true

require_relative "errors"
require_relative "metadata"
require "net/http"
require "rexml"

module AzureBlob
  class Http # :nodoc:
    class Error < AzureBlob::Error
      attr_reader :body, :status
      def initialize(body: nil, status: nil)
        @body = body
        @status = status
      end

      def inspect
        @body
      end
    end
    class FileNotFoundError < Error; end
    class ForbidenError < Error; end
    class IntegrityError < Error; end

    include REXML

    def initialize(uri, headers = {}, signer: nil, metadata: {}, tags: {}, debug: false, raise_on_error: true)
      @raise_on_error = raise_on_error
      @date = Time.now.httpdate
      @uri = uri
      @signer = signer
      @headers = headers.merge(
        Metadata.new(metadata).headers,
        Tags.new(tags).headers,
      )

      sanitize_headers

      @http = Net::HTTP.new(uri.hostname, uri.port)
      @http.use_ssl = uri.port == 443
      @http.set_debug_output($stdout) if debug
    end

    def get
      sign_request("GET") if signer
      @response = http.start do |http|
        http.get(uri, headers)
      end
      raise_error  unless success?
      response.body
    end

    def put(content = "")
      sign_request("PUT") if signer
      @response = http.start do |http|
        http.put(uri, content, headers)
      end
      raise_error  unless success?
      true
    end

    def post(content = "")
      sign_request("POST") if signer
      @response = http.start do |http|
        http.post(uri, content, headers)
      end
      raise_error  unless success?
      response.body
    end

    def head
      sign_request("HEAD") if signer
      @response = http.start do |http|
        http.head(uri, headers)
      end
      raise_error  unless success?
      response
    end

    def delete
      sign_request("DELETE") if signer
      @response = http.start do |http|
        http.delete(uri, headers)
      end
      raise_error  unless success?
      response.body
    end

    def success?
      status < Net::HTTPSuccess
    end

    private

    ERROR_MAPPINGS = {
      Net::HTTPNotFound => FileNotFoundError,
      Net::HTTPForbidden => ForbidenError,
    }

    ERROR_CODE_MAPPINGS = {
      "Md5Mismatch" => IntegrityError,
    }

    def sanitize_headers
      headers[:"x-ms-version"] =  API_VERSION
      headers[:"x-ms-date"] = date
      headers[:"Content-Type"] = headers[:"Content-Type"].to_s
      headers[:"Content-Length"] = headers[:"Content-Length"]&.to_s
      headers[:"Content-MD5"] = nil if headers[:"Content-MD5"]&.empty?
      headers.reject! { |_, value| value.nil? }
    end

    def sign_request(method)
      headers[:Authorization] = signer.authorization_header(uri:, verb: method, headers:)
    end

    def raise_error
      return unless raise_on_error
      raise error_from_response.new(body: @response.body, status: @response.code&.to_i)
    end

    def status
      @status ||= Net::HTTPResponse::CODE_TO_OBJ[response.code]
    end

    def azure_error_code
      Document.new(response.body).get_elements("//Error/Code").first.get_text.to_s if response.body
    end

    def error_from_response
      ERROR_MAPPINGS[status] || ERROR_CODE_MAPPINGS[azure_error_code] || Error
    end

    attr_accessor :host, :http, :signer, :response, :headers, :uri, :date, :raise_on_error
  end
end