197 lines
		
	
	
		
			4.5 KiB
		
	
	
	
		
			Ruby
		
	
	
	
			
		
		
	
	
			197 lines
		
	
	
		
			4.5 KiB
		
	
	
	
		
			Ruby
		
	
	
	
# frozen_string_literal: true
 | 
						|
 | 
						|
##
 | 
						|
# This class is compatible with IO class (https://ruby-doc.org/core-2.3.1/IO.html)
 | 
						|
# source: https://gitlab.com/snippets/1685610
 | 
						|
module Gitlab
 | 
						|
  class HttpIO
 | 
						|
    BUFFER_SIZE = 128.kilobytes
 | 
						|
 | 
						|
    InvalidURLError = Class.new(StandardError)
 | 
						|
    FailedToGetChunkError = Class.new(StandardError)
 | 
						|
 | 
						|
    attr_reader :uri, :size
 | 
						|
    attr_reader :tell
 | 
						|
    attr_reader :chunk, :chunk_range
 | 
						|
 | 
						|
    alias_method :pos, :tell
 | 
						|
 | 
						|
    def initialize(url, size)
 | 
						|
      raise InvalidURLError unless ::Gitlab::UrlSanitizer.valid?(url)
 | 
						|
 | 
						|
      @uri = URI(url)
 | 
						|
      @size = size
 | 
						|
      @tell = 0
 | 
						|
    end
 | 
						|
 | 
						|
    def close
 | 
						|
      # no-op
 | 
						|
    end
 | 
						|
 | 
						|
    def binmode
 | 
						|
      # no-op
 | 
						|
    end
 | 
						|
 | 
						|
    def binmode?
 | 
						|
      true
 | 
						|
    end
 | 
						|
 | 
						|
    def path
 | 
						|
      nil
 | 
						|
    end
 | 
						|
 | 
						|
    def url
 | 
						|
      @uri.to_s
 | 
						|
    end
 | 
						|
 | 
						|
    def seek(pos, where = IO::SEEK_SET)
 | 
						|
      new_pos =
 | 
						|
        case where
 | 
						|
        when IO::SEEK_END
 | 
						|
          size + pos
 | 
						|
        when IO::SEEK_SET
 | 
						|
          pos
 | 
						|
        when IO::SEEK_CUR
 | 
						|
          tell + pos
 | 
						|
        else
 | 
						|
          -1
 | 
						|
        end
 | 
						|
 | 
						|
      raise 'new position is outside of file' if new_pos < 0 || new_pos > size
 | 
						|
 | 
						|
      @tell = new_pos
 | 
						|
    end
 | 
						|
 | 
						|
    def eof?
 | 
						|
      tell == size
 | 
						|
    end
 | 
						|
 | 
						|
    def each_line
 | 
						|
      until eof?
 | 
						|
        line = readline
 | 
						|
        break if line.nil?
 | 
						|
 | 
						|
        yield(line)
 | 
						|
      end
 | 
						|
    end
 | 
						|
 | 
						|
    def read(length = nil, outbuf = nil)
 | 
						|
      out = []
 | 
						|
 | 
						|
      length ||= size - tell
 | 
						|
 | 
						|
      until length <= 0 || eof?
 | 
						|
        data = get_chunk
 | 
						|
        break if data.empty?
 | 
						|
 | 
						|
        chunk_bytes = [BUFFER_SIZE - chunk_offset, length].min
 | 
						|
        data_slice = data.byteslice(0, chunk_bytes)
 | 
						|
 | 
						|
        out << data_slice
 | 
						|
        @tell += data_slice.bytesize
 | 
						|
        length -= data_slice.bytesize
 | 
						|
      end
 | 
						|
 | 
						|
      out = out.join
 | 
						|
 | 
						|
      # If outbuf is passed, we put the output into the buffer. This supports IO.copy_stream functionality
 | 
						|
      if outbuf
 | 
						|
        outbuf.replace(out)
 | 
						|
      end
 | 
						|
 | 
						|
      out
 | 
						|
    end
 | 
						|
 | 
						|
    def readline
 | 
						|
      out = []
 | 
						|
 | 
						|
      until eof?
 | 
						|
        data = get_chunk
 | 
						|
        new_line = data.index("\n")
 | 
						|
 | 
						|
        if !new_line.nil?
 | 
						|
          out << data[0..new_line]
 | 
						|
          @tell += new_line + 1
 | 
						|
          break
 | 
						|
        else
 | 
						|
          out << data
 | 
						|
          @tell += data.bytesize
 | 
						|
        end
 | 
						|
      end
 | 
						|
 | 
						|
      out.join
 | 
						|
    end
 | 
						|
 | 
						|
    def write(data)
 | 
						|
      raise NotImplementedError
 | 
						|
    end
 | 
						|
 | 
						|
    def truncate(offset)
 | 
						|
      raise NotImplementedError
 | 
						|
    end
 | 
						|
 | 
						|
    def flush
 | 
						|
      raise NotImplementedError
 | 
						|
    end
 | 
						|
 | 
						|
    def present?
 | 
						|
      true
 | 
						|
    end
 | 
						|
 | 
						|
    private
 | 
						|
 | 
						|
    ##
 | 
						|
    # The below methods are not implemented in IO class
 | 
						|
    #
 | 
						|
    def in_range?
 | 
						|
      @chunk_range&.include?(tell)
 | 
						|
    end
 | 
						|
 | 
						|
    def get_chunk
 | 
						|
      unless in_range?
 | 
						|
        response = Net::HTTP.start(uri.hostname, uri.port, proxy_from_env: true, use_ssl: uri.scheme == 'https') do |http|
 | 
						|
          http.request(request)
 | 
						|
        end
 | 
						|
 | 
						|
        raise FailedToGetChunkError unless response.code == '200' || response.code == '206'
 | 
						|
 | 
						|
        @chunk = response.body.force_encoding(Encoding::BINARY)
 | 
						|
        @chunk_range = response.content_range
 | 
						|
 | 
						|
        ##
 | 
						|
        # Note: If provider does not return content_range, then we set it as we requested
 | 
						|
        # Provider: minio
 | 
						|
        # - When the file size is larger than requested Content-range, the Content-range is included in responses with Net::HTTPPartialContent 206
 | 
						|
        # - When the file size is smaller than requested Content-range, the Content-range is included in responses with Net::HTTPPartialContent 206
 | 
						|
        # Provider: AWS
 | 
						|
        # - When the file size is larger than requested Content-range, the Content-range is included in responses with Net::HTTPPartialContent 206
 | 
						|
        # - When the file size is smaller than requested Content-range, the Content-range is included in responses with Net::HTTPPartialContent 206
 | 
						|
        # Provider: GCS
 | 
						|
        # - When the file size is larger than requested Content-range, the Content-range is included in responses with Net::HTTPPartialContent 206
 | 
						|
        # - When the file size is smaller than requested Content-range, the Content-range is included in responses with Net::HTTPOK 200
 | 
						|
        @chunk_range ||= (chunk_start...(chunk_start + @chunk.bytesize))
 | 
						|
      end
 | 
						|
 | 
						|
      @chunk[chunk_offset..BUFFER_SIZE]
 | 
						|
    end
 | 
						|
 | 
						|
    def request
 | 
						|
      Net::HTTP::Get.new(uri, { 'accept-encoding' => nil }).tap do |request|
 | 
						|
        request.set_range(chunk_start, BUFFER_SIZE)
 | 
						|
      end
 | 
						|
    end
 | 
						|
 | 
						|
    def chunk_offset
 | 
						|
      tell % BUFFER_SIZE
 | 
						|
    end
 | 
						|
 | 
						|
    def chunk_start
 | 
						|
      (tell / BUFFER_SIZE) * BUFFER_SIZE
 | 
						|
    end
 | 
						|
 | 
						|
    def chunk_end
 | 
						|
      [chunk_start + BUFFER_SIZE, size].min
 | 
						|
    end
 | 
						|
  end
 | 
						|
end
 |