There is some complex logic around replacing an existing file safely, and it
is much better that we provide a central, and easy to use, method for
achieving that in a consistent way.

Also, added a tiny stub test to verify that the method does the bare minimum
of what it says on the box.
---
 lib/puppet/util.rb |   46 ++++++++++++++++++++++++++++++++++++++++++++++
 spec/unit/util.rb  |   26 ++++++++++++++++++++++++++
 2 files changed, 72 insertions(+), 0 deletions(-)
 create mode 100755 spec/unit/util.rb

diff --git a/lib/puppet/util.rb b/lib/puppet/util.rb
index 503c02b..45f4284 100644
--- a/lib/puppet/util.rb
+++ b/lib/puppet/util.rb
@@ -2,6 +2,7 @@
 
 require 'puppet/util/monkey_patches'
 require 'sync'
+require 'tempfile'
 require 'puppet/external/lock'
 
 module Puppet
@@ -444,6 +445,51 @@ module Util
         end
     end
     module_function :secure_open
+
+    def replace_file (file, mode = 0600, &block)
+        raise Puppet::DevError,"replace_file requires a block" unless 
block_given?
+        # Theoretically we should have an absolute path anyway, but let us be
+        # forgiving to the developers, shall we?
+        dir = File.dirname(File.expand_path(file))
+        tempfile = Tempfile.new(file, dir)
+
+        # OK, now allow the caller to write the content of the file...
+        yield tempfile
+
+        # Adjust the mode of the file on disk before we replace the old file,
+        # to ensure that processes which respond immediately don't fail.
+        tempfile.chmod(mode)
+
+        # Now, make sure the data (which includes the mode) is safe on disk.
+        #
+        # Note: fsync may not be implemented by Ruby on all platforms, but
+        # there is absolutely no recovery path if we detect that.  So, we just
+        # ignore the return code.
+        #
+        # However, don't be fooled: that is accepting that we are running in
+        # an unsafe fashion.  If you are porting to a new platform don't stub
+        # that out.
+        tempfile.flush
+        tempfile.fsync
+
+        # ...and then actually replace the original file.  this *must* occur
+        # before tempfile is closed, or the class will delete the data on disk.
+        File.rename(tempfile.path, file)
+
+        # Ideally, we would now fsync the directory as well, but Ruby doesn't
+        # have support for that, and it doesn't matter /that/ much...
+
+        # Close the file handle to stop anyone doing anything *too* silly
+        # happening, such as further writes to the file object we yielded if
+        # it got stashed away outside the scope or something.
+        #
+        # By "silly" I mean "not actually atomic", and "able to lose data"
+        tempfile.close
+
+        # Return something true, and possibly useful.
+        return file
+    end
+    module_function :replace_file
 end
 end
 
diff --git a/spec/unit/util.rb b/spec/unit/util.rb
new file mode 100755
index 0000000..1b662e8
--- /dev/null
+++ b/spec/unit/util.rb
@@ -0,0 +1,26 @@
+#!/usr/bin/env ruby
+
+require File.dirname(__FILE__) + '/../spec_helper'
+
+require 'tempfile'
+
+describe Puppet::Util do
+    it "should have a 'replace_file' method" do
+        Puppet::Util.should respond_to(:specificity)
+    end
+
+    it "should replace a file when replace_file is used" do
+        # Create a target file to replace.
+        target = Tempfile.new("replace-me")
+        target.print "replace-me\n"
+        target.fsync            # flush that to disk...
+
+        # Now, replace it...
+        replace_file(target.path, 0600) { |f|
+            f.print "replacement\n"
+        }
+
+        # Finally, check that actually worked.
+        File.new(target.path).readline.must == "replacement\n"
+    end
+end
-- 
1.7.0.4

-- 
You received this message because you are subscribed to the Google Groups 
"Puppet Developers" group.
To post to this group, send email to [email protected].
To unsubscribe from this group, send email to 
[email protected].
For more options, visit this group at 
http://groups.google.com/group/puppet-dev?hl=en.

Reply via email to