class Bundler::CompactIndexClient::Updater
Public Class Methods
new(fetcher)
click to toggle source
# File bundler/compact_index_client/updater.rb, line 12 def initialize(fetcher) @fetcher = fetcher end
Public Instance Methods
update(remote_path, local_path, etag_path)
click to toggle source
# File bundler/compact_index_client/updater.rb, line 16 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 Bundler::HTTPError end
Private Instance Methods
append(remote_path, local_path, etag_path)
click to toggle source
# File bundler/compact_index_client/updater.rb, line 26 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) break false unless file.append(response.body.byteslice(1..-1)) else file.write(response.body) end CacheFile.write(etag_path, etag_from_response(response)) true end end
byte_sequence(value)
click to toggle source
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.
# File bundler/compact_index_client/updater.rb, line 97 def byte_sequence(value) return if value.delete_prefix!(":") && !value.delete_suffix!(":") return if value.delete_prefix!('"') && !value.delete_suffix!('"') value end
etag_for_request(etag_path)
click to toggle source
# File bundler/compact_index_client/updater.rb, line 65 def etag_for_request(etag_path) etag_path.read.tap(&:chomp!) if etag_path.file? end
etag_from_response(response)
click to toggle source
# File bundler/compact_index_client/updater.rb, line 69 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)
click to toggle source
Unwraps and returns a Hash of digest algorithms and base64 values according to RFC 8941 Structured Field Values for HTTP. www.rfc-editor.org/rfc/rfc8941#name-parsing-a-byte-sequence Ignores unsupported algorithms.
# File bundler/compact_index_client/updater.rb, line 80 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 = byte_sequence(value) digests[algorithm] = value end digests.empty? ? nil : digests end
replace(remote_path, local_path, etag_path)
click to toggle source
request without range header to get the full file or a 304 Not Modified
# File bundler/compact_index_client/updater.rb, line 50 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)
click to toggle source
# File bundler/compact_index_client/updater.rb, line 58 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