Use ActiveSupport File.atomic_write

nd delete our own implementation.
This commit is contained in:
Mike McQuaid 2018-09-17 15:23:17 +01:00
parent 4d1de3334c
commit ead23d1f4c
No known key found for this signature in database
GPG Key ID: 48A898132FD8EE70
4 changed files with 77 additions and 38 deletions

View File

@ -162,45 +162,13 @@ class Pathname
# NOTE always overwrites
def atomic_write(content)
require "tempfile"
tf = Tempfile.new(basename.to_s, dirname)
begin
tf.binmode
tf.write(content)
begin
old_stat = stat
rescue Errno::ENOENT
old_stat = default_stat
Dir.mktmpdir(".d", dirname) do |tmpdir|
File.atomic_write(self, tmpdir) do |file|
file.write(content)
end
uid = Process.uid
gid = Process.groups.delete(old_stat.gid) { Process.gid }
begin
tf.chown(uid, gid)
tf.chmod(old_stat.mode)
rescue Errno::EPERM # rubocop:disable Lint/HandleExceptions
end
# Close the file before renaming to prevent the error: Device or resource busy
# Affects primarily NFS.
tf.close
File.rename(tf.path, self)
ensure
tf.close!
end
end
def default_stat
sentinel = parent.join(".brew.#{Process.pid}.#{rand(Time.now.to_i)}")
sentinel.open("w") {}
sentinel.stat
ensure
sentinel.unlink
end
private :default_stat
# @private
def cp_path_sub(pattern, replacement)
raise "#{self} does not exist" unless exist?

View File

@ -132,6 +132,7 @@ require "extend/predicable"
require "extend/string"
require "active_support/core_ext/object/blank"
require "active_support/core_ext/hash/deep_merge"
require "active_support/core_ext/file/atomic"
require "constants"
require "exceptions"

View File

@ -105,14 +105,14 @@ describe Pathname do
it "preserves permissions" do
File.open(file, "w", 0100777) {}
file.atomic_write("CONTENT")
expect(file.stat.mode).to eq(0100777 & ~File.umask)
expect(file.stat.mode.to_s(8)).to eq((0100777 & ~File.umask).to_s(8))
end
it "preserves default permissions" do
file.atomic_write("CONTENT")
sentinel = file.parent.join("sentinel")
sentinel = file.dirname.join("sentinel")
touch sentinel
expect(file.stat.mode).to eq(sentinel.stat.mode)
expect(file.stat.mode.to_s(8)).to eq(sentinel.stat.mode.to_s(8))
end
end

View File

@ -0,0 +1,70 @@
# frozen_string_literal: true
require "fileutils"
class File
# Write to a file atomically. Useful for situations where you don't
# want other processes or threads to see half-written files.
#
# File.atomic_write('important.file') do |file|
# file.write('hello')
# end
#
# This method needs to create a temporary file. By default it will create it
# in the same directory as the destination file. If you don't like this
# behavior you can provide a different directory but it must be on the
# same physical filesystem as the file you're trying to write.
#
# File.atomic_write('/data/something.important', '/data/tmp') do |file|
# file.write('hello')
# end
def self.atomic_write(file_name, temp_dir = dirname(file_name))
require "tempfile" unless defined?(Tempfile)
Tempfile.open(".#{basename(file_name)}", temp_dir) do |temp_file|
temp_file.binmode
return_val = yield temp_file
temp_file.close
old_stat = if exist?(file_name)
# Get original file permissions
stat(file_name)
elsif temp_dir != dirname(file_name)
# If not possible, probe which are the default permissions in the
# destination directory.
probe_stat_in(dirname(file_name))
end
if old_stat
# Set correct permissions on new file
begin
chown(old_stat.uid, old_stat.gid, temp_file.path)
# This operation will affect filesystem ACL's
chmod(old_stat.mode, temp_file.path)
rescue Errno::EPERM, Errno::EACCES
# Changing file ownership failed, moving on.
end
end
# Overwrite original file with temp file
rename(temp_file.path, file_name)
return_val
end
end
# Private utility method.
def self.probe_stat_in(dir) #:nodoc:
basename = [
".permissions_check",
Thread.current.object_id,
Process.pid,
rand(1000000)
].join(".")
file_name = join(dir, basename)
FileUtils.touch(file_name)
stat(file_name)
ensure
FileUtils.rm_f(file_name) if file_name
end
end