diff --git a/Library/Homebrew/extend/pathname.rb b/Library/Homebrew/extend/pathname.rb index 7362191231..71c6443dd0 100644 --- a/Library/Homebrew/extend/pathname.rb +++ b/Library/Homebrew/extend/pathname.rb @@ -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? diff --git a/Library/Homebrew/global.rb b/Library/Homebrew/global.rb index 1976fcdb94..8319ed9bc0 100644 --- a/Library/Homebrew/global.rb +++ b/Library/Homebrew/global.rb @@ -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" diff --git a/Library/Homebrew/test/pathname_spec.rb b/Library/Homebrew/test/pathname_spec.rb index c9bf25eeb5..4b5f4305d3 100644 --- a/Library/Homebrew/test/pathname_spec.rb +++ b/Library/Homebrew/test/pathname_spec.rb @@ -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 diff --git a/Library/Homebrew/vendor/bundle-standalone/ruby/2.3.0/gems/activesupport-5.2.1/lib/active_support/core_ext/file/atomic.rb b/Library/Homebrew/vendor/bundle-standalone/ruby/2.3.0/gems/activesupport-5.2.1/lib/active_support/core_ext/file/atomic.rb new file mode 100644 index 0000000000..8e288833b6 --- /dev/null +++ b/Library/Homebrew/vendor/bundle-standalone/ruby/2.3.0/gems/activesupport-5.2.1/lib/active_support/core_ext/file/atomic.rb @@ -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