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