class Bundler::CompactIndexClient::Updater
Public Class Methods
# File bundler/compact_index_client/updater.rb, line 12 def initialize(fetcher) @fetcher = fetcher end
Public Instance Methods
# 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
# 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? etag ||= generate_etag(etag_path, file) # Remove this after 2.5.0 has been out for a while. # 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
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 108 def byte_sequence(value) return if value.delete_prefix!(":") && !value.delete_suffix!(":") return if value.delete_prefix!('"') && !value.delete_suffix!('"') value end
# File bundler/compact_index_client/updater.rb, line 66 def etag_for_request(etag_path) etag_path.read.tap(&:chomp!) if etag_path.file? end
# File bundler/compact_index_client/updater.rb, line 80 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
When first releasing this opaque etag feature, we want to generate the old MD5 etag based on the content of the file. After that it will always use the saved opaque etag. This transparently saves existing users with good caches from updating a bunch of files. Remove this behavior after 2.5.0 has been out for a while.
# File bundler/compact_index_client/updater.rb, line 74 def generate_etag(etag_path, file) etag = file.md5.hexdigest CacheFile.write(etag_path, etag) etag end
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 91 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
request without range header to get the full file or a 304 Not Modified
# File bundler/compact_index_client/updater.rb, line 51 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
# File bundler/compact_index_client/updater.rb, line 59 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