Use ActiveSupport File.atomic_write
nd delete our own implementation.
This commit is contained in:
parent
4d1de3334c
commit
ead23d1f4c
@ -162,45 +162,13 @@ class Pathname
|
|||||||
|
|
||||||
# NOTE always overwrites
|
# NOTE always overwrites
|
||||||
def atomic_write(content)
|
def atomic_write(content)
|
||||||
require "tempfile"
|
Dir.mktmpdir(".d", dirname) do |tmpdir|
|
||||||
tf = Tempfile.new(basename.to_s, dirname)
|
File.atomic_write(self, tmpdir) do |file|
|
||||||
begin
|
file.write(content)
|
||||||
tf.binmode
|
|
||||||
tf.write(content)
|
|
||||||
|
|
||||||
begin
|
|
||||||
old_stat = stat
|
|
||||||
rescue Errno::ENOENT
|
|
||||||
old_stat = default_stat
|
|
||||||
end
|
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
|
||||||
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
|
# @private
|
||||||
def cp_path_sub(pattern, replacement)
|
def cp_path_sub(pattern, replacement)
|
||||||
raise "#{self} does not exist" unless exist?
|
raise "#{self} does not exist" unless exist?
|
||||||
|
@ -132,6 +132,7 @@ require "extend/predicable"
|
|||||||
require "extend/string"
|
require "extend/string"
|
||||||
require "active_support/core_ext/object/blank"
|
require "active_support/core_ext/object/blank"
|
||||||
require "active_support/core_ext/hash/deep_merge"
|
require "active_support/core_ext/hash/deep_merge"
|
||||||
|
require "active_support/core_ext/file/atomic"
|
||||||
|
|
||||||
require "constants"
|
require "constants"
|
||||||
require "exceptions"
|
require "exceptions"
|
||||||
|
@ -105,14 +105,14 @@ describe Pathname do
|
|||||||
it "preserves permissions" do
|
it "preserves permissions" do
|
||||||
File.open(file, "w", 0100777) {}
|
File.open(file, "w", 0100777) {}
|
||||||
file.atomic_write("CONTENT")
|
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
|
end
|
||||||
|
|
||||||
it "preserves default permissions" do
|
it "preserves default permissions" do
|
||||||
file.atomic_write("CONTENT")
|
file.atomic_write("CONTENT")
|
||||||
sentinel = file.parent.join("sentinel")
|
sentinel = file.dirname.join("sentinel")
|
||||||
touch 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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -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
|
Loading…
x
Reference in New Issue
Block a user