Preview Ruby 4.1 is a preview. It can change before the final release. Switch to Ruby 4.0 →
class method
Gem::AtomicFileWriter.open
open(file_name) { |temp_file| ... }
lib/rubygems/util/atomic_file_writer.rb
View on GitHub ↗
# File tmp/rubies/ruby-4.1.0-preview1/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
Write to a file atomically. Useful for situations where you don’t want other processes or threads to see half-written files.
See also
.byteslice_at_char_boundary