Class: Bundler::CompactIndexClient::Updater

Inherits:
Object
  • Object
show all
Defined in:
lib/bundler/compact_index_client/updater.rb

Defined Under Namespace

Classes: MisMatchedChecksumError

Instance Method Summary collapse

Constructor Details

#initialize(fetcher) ⇒ Updater

Returns a new instance of Updater.


23
24
25
26
# File 'lib/bundler/compact_index_client/updater.rb', line 23

def initialize(fetcher)
  @fetcher = fetcher
  require "tmpdir"
end

Instance Method Details

#checksum_for_file(path) ⇒ Object


101
102
103
104
105
106
107
108
109
# File 'lib/bundler/compact_index_client/updater.rb', line 101

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) ⇒ Object


92
93
94
95
# 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) ⇒ Object


97
98
99
# File 'lib/bundler/compact_index_client/updater.rb', line 97

def slice_body(body, range)
  body.byteslice(range)
end

#update(local_path, remote_path, retrying = nil) ⇒ Object


28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# 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