lib/autoproj/ops/atomic_write.rb in autoproj-2.16.0 vs lib/autoproj/ops/atomic_write.rb in autoproj-2.17.0

- old
+ new

@@ -1,37 +1,74 @@ +# frozen_string_literal: true + +require "tempfile" +require "fileutils" + module Autoproj module Ops + # 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 + # # Shamelessly stolen from ActiveSupport - def self.atomic_write(file_name, temp_dir = Dir.tmpdir) - require "tempfile" unless defined?(Tempfile) - require "fileutils" unless defined?(FileUtils) + def self.atomic_write(file_name, temp_dir = File.dirname(file_name)) + Tempfile.open(".#{File.basename(file_name)}", temp_dir) do |temp_file| + temp_file.binmode + yield temp_file + temp_file.close - temp_file = Tempfile.new(File.basename(file_name), temp_dir) - yield temp_file - temp_file.flush - begin temp_file.fsync - rescue NotImplementedError - end - temp_file.close + old_stat = begin + # Get original file permissions + File.stat(file_name) + rescue Errno::ENOENT + # If not possible, probe which are the default permissions in the + # destination directory. + probe_stat_in(File.dirname(file_name)) + end - begin - # Get original file permissions - old_stat = File.stat(file_name) - rescue Errno::ENOENT - # No old permissions, write a temp file to determine the defaults - check_name = File.join( - File.dirname(file_name), ".permissions_check.#{Thread.current.object_id}.#{Process.pid}.#{rand(1000000)}" - ) - File.open(check_name, "w") {} - old_stat = File.stat(check_name) - File.unlink(check_name) + if old_stat + # Set correct permissions on new file + begin + File.chown(old_stat.uid, old_stat.gid, temp_file.path) + # This operation will affect filesystem ACL's + File.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 + File.rename(temp_file.path, file_name) end + end - # Overwrite original file with temp file - FileUtils.mv(temp_file.path, file_name) + # Private utility method. + def self.probe_stat_in(dir) # :nodoc: + basename = [ + ".permissions_check", + Thread.current.object_id, + Process.pid, + rand(1000000) + ].join(".") - # Set correct permissions on new file - File.chown(old_stat.uid, old_stat.gid, file_name) - File.chmod(old_stat.mode, file_name) + file_name = File.join(dir, basename) + FileUtils.touch(file_name) + File.stat(file_name) + rescue Errno::ENOENT + file_name = nil + ensure + FileUtils.rm_f(file_name) if file_name end end end