1
0
Fork 0
mirror of https://github.com/ruby/ruby.git synced 2022-11-09 12:17:21 -05:00
ruby--ruby/lib/bundler/compact_index_client/updater.rb
David Rodríguez 76de7a92b9 [rubygems/rubygems] Fix misleading error if compact index cannot be copied
Previously if `~/.bundle/cache/compact_index/rubygems.org.*/version`
were owned by root with read-only access, `bundle install` would fail
with a misleading error message. For example:

```
There was an error while trying to write to `/tmp/bundler-compact-index-20220711-1823-npllre/versions`. It is
likely that you need to grant write permissions for that path.
```

This happened because the EACCESS error was caught by
`SharedHelpers.filesystem_access`, which makes it look like the target
directory is at fault instead of the source.

We can't simply drop this guard because that causes the opposite
problem: the permission error appears to come from the source instead of
the target, since `CompactIndexClient::Cache#lines` also wraps read
access errors.

Instead, bring a minimal implementation of `FileUtils.cp` and nest calls
to `SharedHelpers.filesystem_access` properly.

https://github.com/rubygems/rubygems/commit/320822c070

Co-authored-by: Stan Hu <stanhu@gmail.com>
2022-07-14 15:06:09 +09:00

116 lines
3.6 KiB
Ruby

# frozen_string_literal: true
require_relative "../vendored_fileutils"
module Bundler
class CompactIndexClient
class Updater
class MisMatchedChecksumError < Error
def initialize(path, server_checksum, local_checksum)
@path = path
@server_checksum = server_checksum
@local_checksum = local_checksum
end
def message
"The checksum of /#{@path} does not match the checksum provided by the server! Something is wrong " \
"(local checksum is #{@local_checksum.inspect}, was expecting #{@server_checksum.inspect})."
end
end
def initialize(fetcher)
@fetcher = fetcher
require_relative "../vendored_tmpdir"
end
def update(local_path, remote_path, retrying = nil)
headers = {}
Bundler::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?
copy_file local_path, local_temp_path
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
end
response = @fetcher.call(remote_path, headers)
return nil if response.is_a?(Net::HTTPNotModified)
content = response.body
etag = (response["ETag"] || "").gsub(%r{\AW/}, "")
correct_response = 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) }
etag_for(local_temp_path) == etag
else
local_temp_path.open("wb") {|f| f << content }
etag.length.zero? || etag_for(local_temp_path) == etag
end
end
if correct_response
SharedHelpers.filesystem_access(local_path) do
FileUtils.mv(local_temp_path, local_path)
end
return nil
end
if retrying
raise MisMatchedChecksumError.new(remote_path, etag, etag_for(local_temp_path))
end
update(local_path, remote_path, :retrying)
end
rescue Zlib::GzipFile::Error
raise Bundler::HTTPError
end
def etag_for(path)
sum = checksum_for_file(path)
sum ? %("#{sum}") : nil
end
def slice_body(body, range)
body.byteslice(range)
end
def checksum_for_file(path)
return nil unless path.file?
# This must use File.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(File.read(path))
end
end
private
def copy_file(source, dest)
SharedHelpers.filesystem_access(source, :read) do
File.open(source, "r") do |s|
SharedHelpers.filesystem_access(dest, :write) do
File.open(dest, "wb", s.stat.mode) do |f|
IO.copy_stream(s, f)
end
end
end
end
end
end
end
end