123456789_123456789_123456789_123456789_123456789_

Class: Gem::CompactIndexClient::Updater

Relationships & Source Files
Namespace Children
Exceptions:
Inherits: Object
Defined in: lib/rubygems/compact_index_client/updater.rb

Overview

Updates the cached files on disk, keeping them in sync with the server using conditional requests (ETag) and ranged requests where possible.

Class Method Summary

Instance Method Summary

Constructor Details

.new(fetcher) ⇒ Updater

[ GitHub ]

  
# File 'lib/rubygems/compact_index_client/updater.rb', line 16

def initialize(fetcher)
  @fetcher = fetcher
end

Instance Method Details

#append(remote_path, local_path, etag_path) (private)

[ GitHub ]

  
# File 'lib/rubygems/compact_index_client/updater.rb', line 30

def append(remote_path, local_path, etag_path)
  return false unless local_path.file? && local_path.size.nonzero?

  CacheFile.copy(local_path) do |file|
    etag = etag_path.read.tap(&:chomp!) if etag_path.file?

    # Subtract a byte to ensure the range won't be empty.
    # Avoids 416 (Range Not Satisfiable) responses.
    response = @fetcher.call(remote_path, request_headers(etag, file.size - 1))
    break true if response.is_a?(Gem::Net::HTTPNotModified)

    file.digests = parse_digests(response)
    # server may ignore Range and return the full response
    if response.is_a?(Gem::Net::HTTPPartialContent)
      tail = response.body.byteslice(1..-1)
      break false unless tail && file.append(tail)
    else
      file.write(response.body)
    end
    CacheFile.write(etag_path, etag_from_response(response))
    true
  end
end

#byte_sequence(value) (private)

Unwrap surrounding colons (byte sequence) The wrapping characters must be matched or we return nil. Also handles quotes because right now rubygems.org sends them.

[ GitHub ]

  
# File 'lib/rubygems/compact_index_client/updater.rb', line 99

def byte_sequence(value)
  return if value.delete_prefix!(":") && !value.delete_suffix!(":")
  return if value.delete_prefix!('"') && !value.delete_suffix!('"')
  value
end

#etag_from_response(response) (private)

[ GitHub ]

  
# File 'lib/rubygems/compact_index_client/updater.rb', line 70

def etag_from_response(response)
  return unless response["ETag"]
  etag = response["ETag"].delete_prefix("W/")
  return if etag.delete_prefix!('"') && !etag.delete_suffix!('"')
  etag
end

#parse_digests(response) (private)

Unwraps and returns a Hash of digest algorithms and base64 values according to RFC 8941 Structured Field Values for HTTP. https://www.rfc-editor.org/rfc/rfc8941#name-parsing-a-byte-sequence Ignores unsupported algorithms.

[ GitHub ]

  
# File 'lib/rubygems/compact_index_client/updater.rb', line 81

def parse_digests(response)
  return unless header = response["Repr-Digest"] || response["Digest"]
  digests = {}
  header.split(",") do |param|
    algorithm, value = param.split("=", 2)
    algorithm.strip!
    algorithm.downcase!
    next unless SUPPORTED_DIGESTS.key?(algorithm)
    next unless value
    next unless value = byte_sequence(value)
    digests[algorithm] = value
  end
  digests.empty? ? nil : digests
end

#replace(remote_path, local_path, etag_path) (private)

request without range header to get the full file or a 304 Not Modified

[ GitHub ]

  
# File 'lib/rubygems/compact_index_client/updater.rb', line 55

def replace(remote_path, local_path, etag_path)
  etag = etag_path.read.tap(&:chomp!) if etag_path.file?
  response = @fetcher.call(remote_path, request_headers(etag))
  return true if response.is_a?(Gem::Net::HTTPNotModified)
  CacheFile.write(local_path, response.body, parse_digests(response))
  CacheFile.write(etag_path, etag_from_response(response))
end

#request_headers(etag, range_start = nil) (private)

[ GitHub ]

  
# File 'lib/rubygems/compact_index_client/updater.rb', line 63

def request_headers(etag, range_start = nil)
  headers = {}
  headers["Range"] = "bytes=#{range_start}-" if range_start
  headers["If-None-Match"] = %("#{etag}") if etag
  headers
end

#update(remote_path, local_path, etag_path)

[ GitHub ]

  
# File 'lib/rubygems/compact_index_client/updater.rb', line 20

def update(remote_path, local_path, etag_path)
  append(remote_path, local_path, etag_path) || replace(remote_path, local_path, etag_path)
rescue CacheFile::DigestMismatchError => e
  raise MismatchedChecksumError.new(remote_path, e.message)
rescue Zlib::GzipFile::Error
  raise Error, "invalid gzip response while fetching /#{remote_path}"
end