# class Bundler::CompactIndexClient::Updater

### Public Class Methods

new(fetcher) click to toggle source

    # File lib/bundler/compact_index_client/updater.rb, line 23
    def initialize(fetcher)
      @fetcher = fetcher
      require "tmpdir"
    end

### Public Instance Methods

checksum_for_file(path) click to toggle source

    # File lib/bundler/compact_index_client/updater.rb, line 105
    def checksum_for_file(path)
      return nil unless path.file?
      # This must use IO.read instead of Digest.file().hexdigest
      # because we need to preserve \n line endings on windows when calculating
      # the checksum
      SharedHelpers.filesystem_access(path, :read) do
        SharedHelpers.digest(:MD5).hexdigest(IO.read(path))
      end
    end

etag_for(path) click to toggle source

    # File lib/bundler/compact_index_client/updater.rb, line 92
    def etag_for(path)
      sum = checksum_for_file(path)
      sum ? %("#{sum}") : nil
    end

slice_body(body, range) click to toggle source

    # File lib/bundler/compact_index_client/updater.rb, line 97
    def slice_body(body, range)
      if body.respond_to?(:byteslice)
        body.byteslice(range)
      else # pre-1.9.3
        body.unpack("@#{range.first}a#{range.end + 1}").first
      end
    end

update(local_path, remote_path, retrying = nil) click to toggle source

    # File lib/bundler/compact_index_client/updater.rb, line 28
    def update(local_path, remote_path, retrying = nil)
      headers = {}

      Dir.mktmpdir("bundler-compact-index-") do |local_temp_dir|
        local_temp_path = Pathname.new(local_temp_dir).join(local_path.basename)

        # first try to fetch any new bytes on the existing file
        if retrying.nil? && local_path.file?
          SharedHelpers.filesystem_access(local_temp_path) do
            FileUtils.cp local_path, local_temp_path
          end
          headers["If-None-Match"] = etag_for(local_temp_path)
          headers["Range"] =
            if local_temp_path.size.nonzero?
              # Subtract a byte to ensure the range won't be empty.
              # Avoids 416 (Range Not Satisfiable) responses.
              "bytes=#{local_temp_path.size - 1}-"
            else
              "bytes=#{local_temp_path.size}-"
            end
        else
          # Fastly ignores Range when Accept-Encoding: gzip is set
          headers["Accept-Encoding"] = "gzip"
        end

        response = @fetcher.call(remote_path, headers)
        return nil if response.is_a?(Net::HTTPNotModified)

        content = response.body
        if response["Content-Encoding"] == "gzip"
          content = Zlib::GzipReader.new(StringIO.new(content)).read
        end

        SharedHelpers.filesystem_access(local_temp_path) do
          if response.is_a?(Net::HTTPPartialContent) && local_temp_path.size.nonzero?
            local_temp_path.open("a") {|f| f << slice_body(content, 1..-1) }
          else
            local_temp_path.open("w") {|f| f << content }
          end
        end

        response_etag = (response["ETag"] || "").gsub(%r{\AW/}, "")
        if etag_for(local_temp_path) == response_etag
          SharedHelpers.filesystem_access(local_path) do
            FileUtils.mv(local_temp_path, local_path)
          end
          return nil
        end

        if retrying
          raise MisMatchedChecksumError.new(remote_path, response_etag, etag_for(local_temp_path))
        end

        update(local_path, remote_path, :retrying)
      end
    rescue Errno::EACCES
      raise Bundler::PermissionError,
        "Bundler does not have write access to create a temp directory " \
        "within #{Dir.tmpdir}. Bundler must have write access to your " \
        "systems temp directory to function properly. "
    rescue Zlib::GzipFile::Error
      raise Bundler::HTTPError
    end