class Gem::AtomicFileWriter
Public Class Methods
Write to a file atomically. Useful for situations where you don’t want other processes or threads to see half-written files.
# File lib/rubygems/util/atomic_file_writer.rb, line 14 def self.open(file_name) # Vendored, because activating the securerandom default gem here pins it for # the rest of the process and conflicts with gems that need a newer one. require_relative "../vendored_securerandom" old_stat = begin File.stat(file_name) rescue SystemCallError nil end # Names can't be longer than 255B tmp_suffix = ".tmp.#{Gem::SecureRandom.hex}" dirname = File.dirname(file_name) basename = File.basename(file_name) base_slice = byteslice_at_char_boundary(basename, 254 - tmp_suffix.bytesize) tmp_path = File.join(dirname, ".#{base_slice}#{tmp_suffix}") # The temporary name is longer than the final one, so on Windows a # writable destination can still map to a path beyond the 260-character # MAX_PATH limit. Trim the random suffix first, keeping at least 8 hex # characters to avoid collisions, then shorten the basename if that is not # enough. Recomputing the basename from the trimmed suffix would just let # it grow back, so trim the already-sliced basename instead. if tmp_path.length >= 260 && Gem.win_platform? overflow = tmp_path.length - 259 trim = [tmp_suffix.bytesize - (".tmp.".bytesize + 8), overflow].min tmp_suffix = tmp_suffix.byteslice(0, tmp_suffix.bytesize - trim) overflow -= trim base_slice = byteslice_at_char_boundary(base_slice, [base_slice.bytesize - overflow, 0].max) if overflow > 0 tmp_path = File.join(dirname, ".#{base_slice}#{tmp_suffix}") end flags = File::RDWR | File::CREAT | File::EXCL | File::BINARY flags |= File::SHARE_DELETE if defined?(File::SHARE_DELETE) renamed = false temp_file = File.open(tmp_path, flags) begin temp_file.binmode if old_stat # Set correct permissions on new file begin temp_file.chown(old_stat.uid, old_stat.gid) # This operation will affect filesystem ACL's temp_file.chmod(old_stat.mode) rescue Errno::EPERM, Errno::EACCES # Changing file ownership failed, moving on. end end return_val = yield temp_file # Any data still buffered is handed to the filesystem on close, so a # failing flush must surface while the destination is still intact. # That means closing the temporary file before the rename. Note this # does not fsync, so the write is atomic but not crash durable. temp_file.close File.rename(tmp_path, file_name) renamed = true return_val ensure unless renamed begin temp_file.close rescue StandardError nil ensure # The unlink runs from an ensure so that a non-StandardError raised by # the close above, a second Ctrl-C for instance, still reaches it. An # interrupt landing on the unlink itself is not covered. begin File.unlink(tmp_path) rescue StandardError nil end end end end end