Class: Bundler::CompactIndexClient::CacheFile

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

Overview

write cache files in a way that is robust to concurrent modifications if digests are given, the checksums will be verified

Defined Under Namespace

Classes: ClosedError, DigestMismatchError, Error

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(original_path, &block) ⇒ CacheFile

Returns a new instance of CacheFile.



49
50
51
52
53
54
55
56
57
58
59
# File 'lib/bundler/compact_index_client/cache_file.rb', line 49

def initialize(original_path, &block)
  @original_path = original_path
  @perm = original_path.file? ? original_path.stat.mode : DEFAULT_FILE_MODE
  @path = original_path.sub(/$/, ".#{$$}.tmp")
  return unless block_given?
  begin
    yield self
  ensure
    close
  end
end

Instance Attribute Details

#original_pathObject (readonly)

Returns the value of attribute original_path.



47
48
49
# File 'lib/bundler/compact_index_client/cache_file.rb', line 47

def original_path
  @original_path
end

#pathObject (readonly)

Returns the value of attribute path.



47
48
49
# File 'lib/bundler/compact_index_client/cache_file.rb', line 47

def path
  @path
end

Class Method Details

.copy(path, &block) ⇒ Object

Initialize with a copy of the original file, then yield the instance.



24
25
26
27
28
29
30
31
32
33
34
35
36
# File 'lib/bundler/compact_index_client/cache_file.rb', line 24

def self.copy(path, &block)
  new(path) do |file|
    file.initialize_digests

    SharedHelpers.filesystem_access(path, :read) do
      path.open("rb") do |s|
        file.open {|f| IO.copy_stream(s, f) }
      end
    end

    yield file
  end
end

.write(path, data, digests = nil) ⇒ Object

Write data to a temp file, then replace the original file with it verifying the digests if given.



39
40
41
42
43
44
45
# File 'lib/bundler/compact_index_client/cache_file.rb', line 39

def self.write(path, data, digests = nil)
  return unless data
  new(path) do |file|
    file.digests = digests
    file.write(data)
  end
end

Instance Method Details

#append(data) ⇒ Object

Returns false without appending when no digests since appending is too error prone to do without digests.



109
110
111
112
113
# File 'lib/bundler/compact_index_client/cache_file.rb', line 109

def append(data)
  return false unless digests?
  open("a") {|f| f.write data }
  verify && commit
end

#closeObject

Remove the temp file without replacing the original file. The file is permanently closed.



146
147
148
149
150
# File 'lib/bundler/compact_index_client/cache_file.rb', line 146

def close
  return if @closed
  FileUtils.remove_file(path) if @path&.file?
  @closed = true
end

#commitObject

Replace the original file with the temp file without verifying digests. The file is permanently closed.

Raises:



136
137
138
139
140
141
142
# File 'lib/bundler/compact_index_client/cache_file.rb', line 136

def commit
  raise ClosedError, "Cannot commit closed file" if @closed
  SharedHelpers.filesystem_access(original_path, :write) do
    FileUtils.mv(path, original_path)
  end
  @closed = true
end

#commit!Object



121
122
123
124
# File 'lib/bundler/compact_index_client/cache_file.rb', line 121

def commit!
  verify || raise(DigestMismatchError.new(@base64digests, @expected_digests))
  commit
end

#digests=(expected_digests) ⇒ Object

set the digests that will be verified at the end



77
78
79
80
81
82
83
84
85
86
87
# File 'lib/bundler/compact_index_client/cache_file.rb', line 77

def digests=(expected_digests)
  @expected_digests = expected_digests

  if @expected_digests.nil?
    @digests = nil
  elsif @digests
    @digests = @digests.slice(*@expected_digests.keys)
  else
    initialize_digests(@expected_digests.keys)
  end
end

#digests?Boolean

Returns:

  • (Boolean)


94
95
96
# File 'lib/bundler/compact_index_client/cache_file.rb', line 94

def digests?
  @digests&.any?
end

#initialize_digests(keys = nil) ⇒ Object

initialize the digests using CompactIndexClient::SUPPORTED_DIGESTS, or a subset based on keys.



66
67
68
69
# File 'lib/bundler/compact_index_client/cache_file.rb', line 66

def initialize_digests(keys = nil)
  @digests = keys ? SUPPORTED_DIGESTS.slice(*keys) : SUPPORTED_DIGESTS.dup
  @digests.transform_values! {|algo_class| SharedHelpers.digest(algo_class).new }
end

#md5Object

remove this method when we stop generating md5 digests for legacy etags



90
91
92
# File 'lib/bundler/compact_index_client/cache_file.rb', line 90

def md5
  @digests && @digests["md5"]
end

#open(write_mode = "wb", perm = @perm, &block) ⇒ Object

Open the temp file for writing, reusing original permissions, yielding the IO object.

Raises:



99
100
101
102
103
104
105
106
# File 'lib/bundler/compact_index_client/cache_file.rb', line 99

def open(write_mode = "wb", perm = @perm, &block)
  raise ClosedError, "Cannot reopen closed file" if @closed
  SharedHelpers.filesystem_access(path, :write) do
    path.open(write_mode, perm) do |f|
      yield digests? ? Gem::Package::DigestIO.new(f, @digests) : f
    end
  end
end

#reset_digestsObject

reset the digests so they don’t contain any previously read data



72
73
74
# File 'lib/bundler/compact_index_client/cache_file.rb', line 72

def reset_digests
  @digests&.each_value(&:reset)
end

#sizeObject



61
62
63
# File 'lib/bundler/compact_index_client/cache_file.rb', line 61

def size
  path.size
end

#verifyObject

Verify the digests, returning true on match, false on mismatch.



127
128
129
130
131
132
# File 'lib/bundler/compact_index_client/cache_file.rb', line 127

def verify
  return true unless @expected_digests && digests?
  @base64digests = @digests.transform_values!(&:base64digest)
  @digests = nil
  @base64digests.all? {|algo, digest| @expected_digests[algo] == digest }
end

#write(data) ⇒ Object



115
116
117
118
119
# File 'lib/bundler/compact_index_client/cache_file.rb', line 115

def write(data)
  reset_digests
  open {|f| f.write data }
  commit!
end