diff --git a/lib/puppet/file_bucket/dipper.rb b/lib/puppet/file_bucket/dipper.rb index dbfcdcd43..b012a8681 100644 --- a/lib/puppet/file_bucket/dipper.rb +++ b/lib/puppet/file_bucket/dipper.rb @@ -1,99 +1,99 @@ require 'puppet/file_bucket' require 'puppet/file_bucket/file' require 'puppet/indirector/request' class Puppet::FileBucket::Dipper # This is a transitional implementation that uses REST # to access remote filebucket files. attr_accessor :name # Create our bucket client def initialize(hash = {}) # Emulate the XMLRPC client server = hash[:Server] port = hash[:Port] || Puppet[:masterport] environment = Puppet[:environment] if hash.include?(:Path) @local_path = hash[:Path] @rest_path = nil else @local_path = nil @rest_path = "https://#{server}:#{port}/#{environment}/file_bucket_file/" end end def local? !! @local_path end # Back up a file to our bucket def backup(file) raise(ArgumentError, "File #{file} does not exist") unless ::File.exist?(file) contents = ::File.read(file) begin - file_bucket_file = Puppet::FileBucket::File.new(contents, :bucket_path => @local_path, :path => absolutize_path(file) ) + file_bucket_file = Puppet::FileBucket::File.new(contents, :bucket_path => @local_path) dest_path = "#{@rest_path}#{file_bucket_file.name}" file_bucket_file.save(dest_path) return file_bucket_file.checksum_data rescue => detail puts detail.backtrace if Puppet[:trace] raise Puppet::Error, "Could not back up #{file}: #{detail}" end end # Retrieve a file by sum. def getfile(sum) source_path = "#{@rest_path}md5/#{sum}" file_bucket_file = Puppet::FileBucket::File.find(source_path, :bucket_path => @local_path) raise Puppet::Error, "File not found" unless file_bucket_file file_bucket_file.to_s end # Restore the file def restore(file,sum) restore = true if FileTest.exists?(file) cursum = Digest::MD5.hexdigest(::File.read(file)) # if the checksum has changed... # this might be extra effort if cursum == sum restore = false end end if restore if newcontents = getfile(sum) tmp = "" newsum = Digest::MD5.hexdigest(newcontents) changed = nil if FileTest.exists?(file) and ! FileTest.writable?(file) changed = ::File.stat(file).mode ::File.chmod(changed | 0200, file) end ::File.open(file, ::File::WRONLY|::File::TRUNC|::File::CREAT) { |of| of.print(newcontents) } ::File.chmod(changed, file) if changed else Puppet.err "Could not find file with checksum #{sum}" return nil end return newsum else return nil end end private def absolutize_path( path ) require 'pathname' Pathname.new(path).realpath end end diff --git a/lib/puppet/file_bucket/file.rb b/lib/puppet/file_bucket/file.rb index 96fd8e225..08c0329f1 100644 --- a/lib/puppet/file_bucket/file.rb +++ b/lib/puppet/file_bucket/file.rb @@ -1,136 +1,56 @@ require 'puppet/file_bucket' require 'puppet/indirector' require 'puppet/util/checksums' +require 'digest/md5' class Puppet::FileBucket::File - include Puppet::Util::Checksums - # This class handles the abstract notion of a file in a filebucket. # There are mechanisms to save and load this file locally and remotely in puppet/indirector/filebucketfile/* # There is a compatibility class that emulates pre-indirector filebuckets in Puppet::FileBucket::Dipper extend Puppet::Indirector require 'puppet/file_bucket/file/indirection_hooks' indirects :file_bucket_file, :terminus_class => :file, :extend => Puppet::FileBucket::File::IndirectionHooks - attr :path, true - attr :paths, true - attr :contents, true - attr :checksum_type - attr :bucket_path, true - - def self.default_checksum_type - "md5" - end + attr :contents + attr :bucket_path def initialize( contents, options = {} ) - @bucket_path = options[:bucket_path] - @path = options[:path] - @paths = options[:paths] || [] - - @checksum = options[:checksum] - @checksum_type = options[:checksum_type] - - self.contents = contents - - yield(self) if block_given? - - validate! - end + raise ArgumentError if !contents.is_a?(String) + @contents = contents - def validate! - validate_checksum_type!(checksum_type) - validate_checksum!(checksum) if checksum + @bucket_path = options.delete(:bucket_path) + raise ArgumentError if options != {} end - def contents=(str) - raise "You may not change the contents of a FileBucket File" if @contents - validate_content!(str) - @contents = str + def checksum_type + 'md5' end def checksum - return @checksum if @checksum - @checksum = calculate_checksum if contents - @checksum - end - - def checksum=(checksum) - validate_checksum!(checksum) - @checksum = checksum - end - - def checksum_type=( new_checksum_type ) - @checksum = nil - @checksum_type = new_checksum_type - end - - def checksum_type - unless @checksum_type - if @checksum - @checksum_type = sumtype(checksum) - else - @checksum_type = self.class.default_checksum_type - end - end - @checksum_type + "{#{checksum_type}}#{checksum_data}" end def checksum_data - sumdata(checksum) + @checksum_data ||= Digest::MD5.hexdigest(contents) end def to_s contents end def name - [checksum_type, checksum_data, path].compact.join('/') - end - - def name=(name) - data = name.split('/',3) - self.path = data.pop - @checksum_type = nil - self.checksum = "{#{data[0]}}#{data[1]}" - end - - def conflict_check? - true + "#{checksum_type}/#{checksum_data}" end def self.from_s( contents ) self.new( contents ) end def to_pson - hash = { "contents" => contents } - hash["path"] = @path if @path - hash.to_pson + { "contents" => contents }.to_pson end def self.from_pson( pson ) - self.new( pson["contents"], :path => pson["path"] ) - end - - private - - def calculate_checksum - "{#{checksum_type}}" + send(checksum_type, contents) - end - - def validate_content!(content) - raise ArgumentError, "Contents must be a string" if content and ! content.is_a?(String) - end - - def validate_checksum!(new_checksum) - newtype = sumtype(new_checksum) - - unless sumdata(new_checksum) == (calc_sum = send(newtype, contents)) - raise Puppet::Error, "Checksum #{new_checksum} does not match contents #{calc_sum}" - end - end - - def validate_checksum_type!(type) - raise ArgumentError, "Invalid checksum type #{type}" unless respond_to?(type) + self.new( pson["contents"] ) end end diff --git a/lib/puppet/indirector/file_bucket_file/file.rb b/lib/puppet/indirector/file_bucket_file/file.rb index 9d9cee793..38e0be6e9 100644 --- a/lib/puppet/indirector/file_bucket_file/file.rb +++ b/lib/puppet/indirector/file_bucket_file/file.rb @@ -1,155 +1,106 @@ require 'puppet/indirector/code' require 'puppet/file_bucket/file' require 'puppet/util/checksums' require 'fileutils' module Puppet::FileBucketFile class File < Puppet::Indirector::Code include Puppet::Util::Checksums desc "Store files in a directory set based on their checksums." def initialize Puppet.settings.use(:filebucket) end def find( request ) - checksum, path = request_to_checksum_and_path( request ) - file = find_by_checksum( checksum, request.options ) + checksum = request_to_checksum( request ) + file_path = path_for(request.options[:bucket_path], checksum, 'contents') - if file && request.options[:diff_with] + return nil unless ::File.exists?(file_path) + + if request.options[:diff_with] hash_protocol = sumtype(checksum) - file2 = find_by_checksum( "{#{hash_protocol}}#{request.options[:diff_with]}", request.options ) - raise "could not find diff_with #{request.options[:diff_with]}" unless file2 - return `diff #{path_for(file).inspect}/contents #{path_for(file2).inspect}/contents` + file2_path = path_for(request.options[:bucket_path], request.options[:diff_with], 'contents') + raise "could not find diff_with #{request.options[:diff_with]}" unless ::File.exists?(file2_path) + return `diff #{file_path.inspect} #{file2_path.inspect}` + else + contents = ::File.read file_path + Puppet.info "FileBucket read #{checksum}" + model.new(contents) end + end - file + def head(request) + checksum = request_to_checksum(request) + file_path = path_for(request.options[:bucket_path], checksum, 'contents') + ::File.exists?(file_path) end def save( request ) - checksum, path = request_to_checksum_and_path( request ) - instance = request.instance - instance.checksum = checksum if checksum - instance.path = path if path save_to_disk(instance) instance.to_s end private - def find_by_checksum( checksum, options ) - model.new( nil, :checksum => checksum ) do |bucket_file| - bucket_file.bucket_path = options[:bucket_path] - filename = contents_path_for( bucket_file ) - - return nil if ! ::File.exist? filename - - begin - contents = ::File.read filename - Puppet.info "FileBucket read #{bucket_file.checksum}" - rescue RuntimeError => e - raise Puppet::Error, "file could not be read: #{e.message}" - end - - if ::File.exist?(paths_path_for( bucket_file) ) - ::File.open(paths_path_for( bucket_file) ) do |f| - bucket_file.paths = f.readlines.map { |l| l.chomp } - end - end - - bucket_file.contents = contents - end - end - def save_to_disk( bucket_file ) - # If the file already exists, just return the md5 sum. - if ::File.exist?(contents_path_for( bucket_file) ) + filename = path_for(bucket_file.bucket_path, bucket_file.checksum_data, 'contents') + dirname = path_for(bucket_file.bucket_path, bucket_file.checksum_data) + + # If the file already exists, do nothing. + if ::File.exist?(filename) verify_identical_file!(bucket_file) else # Make the directories if necessary. - unless ::File.directory?( path_for( bucket_file) ) + unless ::File.directory?(dirname) Puppet::Util.withumask(0007) do - ::FileUtils.mkdir_p( path_for( bucket_file) ) + ::FileUtils.mkdir_p(dirname) end end - Puppet.info "FileBucket adding #{bucket_file.path} as #{bucket_file.checksum}" + Puppet.info "FileBucket adding #{bucket_file.checksum}" # Write the file to disk. Puppet::Util.withumask(0007) do - ::File.open(contents_path_for(bucket_file), ::File::WRONLY|::File::CREAT, 0440) do |of| + ::File.open(filename, ::File::WRONLY|::File::CREAT, 0440) do |of| of.print bucket_file.contents end end end - - save_path_to_paths_file(bucket_file) - bucket_file.checksum_data end - def request_to_checksum_and_path( request ) - return [request.key, nil] if checksum?(request.key) - - checksum_type, checksum, path = request.key.split(/\//, 3) - return(checksum_type.to_s == "" ? nil : [ "{#{checksum_type}}#{checksum}", path ]) + def request_to_checksum( request ) + checksum_type, checksum = request.key.split(/\//, 2) + raise "Unsupported checksum type #{checksum_type.inspect}" if checksum_type != 'md5' + raise "Invalid checksum #{checksum.inspect}" if checksum !~ /^[0-9a-f]{32}$/ + checksum end - def path_for(bucket_file, subfile = nil) - bucket_path = bucket_file.bucket_path || Puppet[:bucketdir] - digest = bucket_file.checksum_data + def path_for(bucket_path, digest, subfile = nil) + bucket_path ||= Puppet[:bucketdir] dir = ::File.join(digest[0..7].split("")) basedir = ::File.join(bucket_path, dir, digest) return basedir unless subfile ::File.join(basedir, subfile) end - def contents_path_for(bucket_file) - path_for(bucket_file, "contents") - end - - def paths_path_for(bucket_file) - path_for(bucket_file, "paths") - end - - def content_check? - true - end - # If conflict_check is enabled, verify that the passed text is # the same as the text in our file. def verify_identical_file!(bucket_file) - return unless content_check? - disk_contents = ::File.read(contents_path_for(bucket_file)) + disk_contents = ::File.read(path_for(bucket_file.bucket_path, bucket_file.checksum_data, 'contents')) # If the contents don't match, then we've found a conflict. # Unlikely, but quite bad. if disk_contents != bucket_file.contents - raise Puppet::FileBucket::BucketError, "Got passed new contents for sum #{bucket_file.checksum}", caller + raise Puppet::FileBucket::BucketError, "Got passed new contents for sum #{bucket_file.checksum}" else - Puppet.info "FileBucket got a duplicate file #{bucket_file.path} (#{bucket_file.checksum})" + Puppet.info "FileBucket got a duplicate file #{bucket_file.checksum}" end end - - def save_path_to_paths_file(bucket_file) - return unless bucket_file.path - - # check for dupes - if ::File.exist?(paths_path_for( bucket_file) ) - ::File.open(paths_path_for( bucket_file) ) do |f| - return if f.readlines.collect { |l| l.chomp }.include?(bucket_file.path) - end - end - - # if it's a new file, or if our path isn't in the file yet, add it - ::File.open(paths_path_for(bucket_file), ::File::WRONLY|::File::CREAT|::File::APPEND) do |of| - of.puts bucket_file.path - end - end - end end diff --git a/spec/unit/file_bucket/dipper_spec.rb b/spec/unit/file_bucket/dipper_spec.rb index 86ad01f83..730e10792 100755 --- a/spec/unit/file_bucket/dipper_spec.rb +++ b/spec/unit/file_bucket/dipper_spec.rb @@ -1,72 +1,72 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' require 'pathname' require 'puppet/file_bucket/dipper' describe Puppet::FileBucket::Dipper do include PuppetSpec::Files def make_tmp_file(contents) file = tmpfile("file_bucket_file") File.open(file, 'w') { |f| f.write(contents) } file end it "should fail in an informative way when there are failures backing up to the server" do @dipper = Puppet::FileBucket::Dipper.new(:Path => "/my/bucket") file = make_tmp_file('contents') Puppet::FileBucket::File.any_instance.expects(:save).raises ArgumentError lambda { @dipper.backup(file) }.should raise_error(Puppet::Error) end it "should backup files to a local bucket" do @dipper = Puppet::FileBucket::Dipper.new(:Path => "/my/bucket") file = make_tmp_file('my contents') checksum = Digest::MD5.hexdigest('my contents') Puppet::FileBucket::File.any_instance.expects(:save) @dipper.backup(file).should == checksum end it "should retrieve files from a local bucket" do @dipper = Puppet::FileBucket::Dipper.new(:Path => "/my/bucket") checksum = Digest::MD5.hexdigest('my contents') Puppet::FileBucket::File.expects(:find).with{|x,opts| x == "md5/#{checksum}" }.returns(Puppet::FileBucket::File.new('my contents')) @dipper.getfile(checksum).should == 'my contents' end it "should backup files to a remote server" do @dipper = Puppet::FileBucket::Dipper.new(:Server => "puppetmaster", :Port => "31337") file = make_tmp_file('my contents') checksum = Digest::MD5.hexdigest('my contents') real_path = Pathname.new(file).realpath - Puppet::FileBucket::File.any_instance.expects(:save).with("https://puppetmaster:31337/production/file_bucket_file/md5/#{checksum}/#{real_path}") + Puppet::FileBucket::File.any_instance.expects(:save).with("https://puppetmaster:31337/production/file_bucket_file/md5/#{checksum}") @dipper.backup(file).should == checksum end it "should retrieve files from a remote server" do @dipper = Puppet::FileBucket::Dipper.new(:Server => "puppetmaster", :Port => "31337") checksum = Digest::MD5.hexdigest('my contents') Puppet::FileBucket::File.expects(:find).with{|x,opts| x == "https://puppetmaster:31337/production/file_bucket_file/md5/#{checksum}" }.returns(Puppet::FileBucket::File.new('my contents')) @dipper.getfile(checksum).should == "my contents" end end diff --git a/spec/unit/file_bucket/file_spec.rb b/spec/unit/file_bucket/file_spec.rb index 3ad70c203..82063c2e3 100644 --- a/spec/unit/file_bucket/file_spec.rb +++ b/spec/unit/file_bucket/file_spec.rb @@ -1,230 +1,150 @@ #!/usr/bin/env ruby require ::File.dirname(__FILE__) + '/../../spec_helper' require 'puppet/file_bucket/file' require 'digest/md5' require 'digest/sha1' describe Puppet::FileBucket::File do + include PuppetSpec::Files + before do # this is the default from spec_helper, but it keeps getting reset at odd times - Puppet[:bucketdir] = "/dev/null/bucket" + @bucketdir = tmpdir('bucket') + Puppet[:bucketdir] = @bucketdir @digest = "4a8ec4fa5f01b4ab1a0ab8cbccb709f0" @checksum = "{md5}4a8ec4fa5f01b4ab1a0ab8cbccb709f0" - @dir = '/dev/null/bucket/4/a/8/e/c/4/f/a/4a8ec4fa5f01b4ab1a0ab8cbccb709f0' + @dir = File.join(@bucketdir, '4/a/8/e/c/4/f/a/4a8ec4fa5f01b4ab1a0ab8cbccb709f0') @contents = "file contents" end it "should have a to_s method to return the contents" do Puppet::FileBucket::File.new(@contents).to_s.should == @contents end - it "should calculate the checksum type from the passed in checksum" do - Puppet::FileBucket::File.new(@contents, :checksum => @checksum).checksum_type.should == "md5" - end - - it "should allow contents to be specified in a block" do - bucket = Puppet::FileBucket::File.new(nil) do |fb| - fb.contents = "content" - end - bucket.contents.should == "content" - end - it "should raise an error if changing content" do x = Puppet::FileBucket::File.new("first") proc { x.contents = "new" }.should raise_error end it "should require contents to be a string" do proc { Puppet::FileBucket::File.new(5) }.should raise_error(ArgumentError) end - it "should raise an error if setting contents to a non-string" do - proc do - Puppet::FileBucket::File.new(nil) do |x| - x.contents = 5 - end - end.should raise_error(ArgumentError) - end - it "should set the contents appropriately" do Puppet::FileBucket::File.new(@contents).contents.should == @contents end it "should default to 'md5' as the checksum algorithm if the algorithm is not in the name" do Puppet::FileBucket::File.new(@contents).checksum_type.should == "md5" end it "should calculate the checksum" do Puppet::FileBucket::File.new(@contents).checksum.should == @checksum end - it "should remove the old checksum value if the algorithm is changed" do - sum = Puppet::FileBucket::File.new(@contents) - sum.checksum.should_not be_nil - - newsum = Digest::SHA1.hexdigest(@contents).to_s - sum.checksum_type = :sha1 - sum.checksum.should == "{sha1}#{newsum}" - end - - it "should support specifying the checksum_type during initialization" do - sum = Puppet::FileBucket::File.new(@contents, :checksum_type => :sha1) - sum.checksum_type.should == :sha1 - end - - it "should fail when an unsupported checksum_type is used" do - proc { Puppet::FileBucket::File.new(@contents, :checksum_type => :nope) }.should raise_error(ArgumentError) - end - - it "should fail if given an checksum at initialization that does not match the contents" do - proc { Puppet::FileBucket::File.new(@contents, :checksum => "{md5}00000000000000000000000000000000") }.should raise_error(RuntimeError) - end - - it "should fail if assigned a checksum that does not match the contents" do - bucket = Puppet::FileBucket::File.new(@contents) - proc { bucket.checksum = "{md5}00000000000000000000000000000000" }.should raise_error(RuntimeError) - end - describe "when using back-ends" do it "should redirect using Puppet::Indirector" do Puppet::Indirector::Indirection.instance(:file_bucket_file).model.should equal(Puppet::FileBucket::File) end it "should have a :save instance method" do Puppet::FileBucket::File.new("mysum").should respond_to(:save) end it "should respond to :find" do Puppet::FileBucket::File.should respond_to(:find) end it "should respond to :destroy" do Puppet::FileBucket::File.should respond_to(:destroy) end end describe "when saving files" do it "should save the contents to the calculated path" do ::File.stubs(:directory?).with(@dir).returns(true) ::File.expects(:exist?).with("#{@dir}/contents").returns false mockfile = mock "file" mockfile.expects(:print).with(@contents) ::File.expects(:open).with("#{@dir}/contents", ::File::WRONLY|::File::CREAT, 0440).yields(mockfile) Puppet::FileBucket::File.new(@contents).save end it "should make any directories necessary for storage" do FileUtils.expects(:mkdir_p).with do |arg| ::File.umask == 0007 and arg == @dir end ::File.expects(:directory?).with(@dir).returns(false) ::File.expects(:open).with("#{@dir}/contents", ::File::WRONLY|::File::CREAT, 0440) ::File.expects(:exist?).with("#{@dir}/contents").returns false Puppet::FileBucket::File.new(@contents).save end - - it "should append the path to the paths file" do - remote_path = '/path/on/the/remote/box' - - ::File.expects(:directory?).with(@dir).returns(true) - ::File.expects(:open).with("#{@dir}/contents", ::File::WRONLY|::File::CREAT, 0440) - ::File.expects(:exist?).with("#{@dir}/contents").returns false - - mockfile = mock "file" - mockfile.expects(:puts).with('/path/on/the/remote/box') - ::File.expects(:exist?).with("#{@dir}/paths").returns false - ::File.expects(:open).with("#{@dir}/paths", ::File::WRONLY|::File::CREAT|::File::APPEND).yields mockfile - Puppet::FileBucket::File.new(@contents, :path => remote_path).save - - end - end - - it "should accept a path" do - remote_path = '/path/on/the/remote/box' - Puppet::FileBucket::File.new(@contents, :path => remote_path).path.should == remote_path end it "should return a url-ish name" do Puppet::FileBucket::File.new(@contents).name.should == "md5/4a8ec4fa5f01b4ab1a0ab8cbccb709f0" end it "should reject a url-ish name with an invalid checksum" do bucket = Puppet::FileBucket::File.new(@contents) lambda { bucket.name = "sha1/4a8ec4fa5f01b4ab1a0ab8cbccb709f0/new/path" }.should raise_error end - it "should accept a url-ish name" do - bucket = Puppet::FileBucket::File.new(@contents) - lambda { bucket.name = "sha1/034fa2ed8e211e4d20f20e792d777f4a30af1a93/new/path" }.should_not raise_error - bucket.checksum_type.should == "sha1" - bucket.checksum_data.should == '034fa2ed8e211e4d20f20e792d777f4a30af1a93' - bucket.path.should == "new/path" - end - - it "should return a url-ish name with a path" do - Puppet::FileBucket::File.new(@contents, :path => 'my/path').name.should == "md5/4a8ec4fa5f01b4ab1a0ab8cbccb709f0/my/path" - end - it "should convert the contents to PSON" do Puppet::FileBucket::File.new(@contents).to_pson.should == '{"contents":"file contents"}' end it "should load from PSON" do Puppet::FileBucket::File.from_pson({"contents"=>"file contents"}).contents.should == "file contents" end it "should save a file" do ::File.expects(:exist?).with("#{@dir}/contents").returns false ::File.expects(:directory?).with(@dir).returns false ::FileUtils.expects(:mkdir_p).with(@dir) ::File.expects(:open).with("#{@dir}/contents", ::File::WRONLY|::File::CREAT, 0440) bucketfile = Puppet::FileBucket::File.new(@contents) bucketfile.save end + def make_bucketed_file + FileUtils.mkdir_p(@dir) + File.open("#{@dir}/contents", 'w') { |f| f.write @contents } + end + describe "using the indirector's find method" do it "should return nil if a file doesn't exist" do - ::File.expects(:exist?).with("#{@dir}/contents").returns false - - bucketfile = Puppet::FileBucket::File.find("{md5}#{@digest}") + bucketfile = Puppet::FileBucket::File.find("md5/#{@digest}") bucketfile.should == nil end it "should find a filebucket if the file exists" do - ::File.expects(:exist?).with("#{@dir}/contents").returns true - ::File.expects(:exist?).with("#{@dir}/paths").returns false - ::File.expects(:read).with("#{@dir}/contents").returns @contents - - bucketfile = Puppet::FileBucket::File.find("{md5}#{@digest}") + make_bucketed_file + bucketfile = Puppet::FileBucket::File.find("md5/#{@digest}") bucketfile.should_not == nil end describe "using RESTish digest notation" do it "should return nil if a file doesn't exist" do - ::File.expects(:exist?).with("#{@dir}/contents").returns false - bucketfile = Puppet::FileBucket::File.find("md5/#{@digest}") bucketfile.should == nil end it "should find a filebucket if the file exists" do - ::File.expects(:exist?).with("#{@dir}/contents").returns true - ::File.expects(:exist?).with("#{@dir}/paths").returns false - ::File.expects(:read).with("#{@dir}/contents").returns @contents - + make_bucketed_file bucketfile = Puppet::FileBucket::File.find("md5/#{@digest}") bucketfile.should_not == nil end end end end diff --git a/spec/unit/indirector/file_bucket_file/file_spec.rb b/spec/unit/indirector/file_bucket_file/file_spec.rb index 7bf02b5b3..6057fd2e6 100755 --- a/spec/unit/indirector/file_bucket_file/file_spec.rb +++ b/spec/unit/indirector/file_bucket_file/file_spec.rb @@ -1,335 +1,180 @@ #!/usr/bin/env ruby require ::File.dirname(__FILE__) + '/../../../spec_helper' require 'puppet/indirector/file_bucket_file/file' describe Puppet::FileBucketFile::File do + include PuppetSpec::Files + it "should be a subclass of the Code terminus class" do Puppet::FileBucketFile::File.superclass.should equal(Puppet::Indirector::Code) end it "should have documentation" do Puppet::FileBucketFile::File.doc.should be_instance_of(String) end describe "non-stubbing tests" do include PuppetSpec::Files before do Puppet[:bucketdir] = tmpdir('bucketdir') end describe "when diffing files" do def save_bucket_file(contents) bucket_file = Puppet::FileBucket::File.new(contents) bucket_file.save bucket_file.checksum_data end it "should generate an empty string if there is no diff" do checksum = save_bucket_file("I'm the contents of a file") Puppet::FileBucket::File.find("md5/#{checksum}", :diff_with => checksum).should == '' end it "should generate a proper diff if there is a diff" do checksum1 = save_bucket_file("foo\nbar\nbaz") checksum2 = save_bucket_file("foo\nbiz\nbaz") diff = Puppet::FileBucket::File.find("md5/#{checksum1}", :diff_with => checksum2) diff.should == < biz HERE end it "should raise an exception if the hash to diff against isn't found" do checksum = save_bucket_file("whatever") bogus_checksum = "d1bf072d0e2c6e20e3fbd23f022089a1" lambda { Puppet::FileBucket::File.find("md5/#{checksum}", :diff_with => bogus_checksum) }.should raise_error "could not find diff_with #{bogus_checksum}" end it "should return nil if the hash to diff from isn't found" do checksum = save_bucket_file("whatever") bogus_checksum = "d1bf072d0e2c6e20e3fbd23f022089a1" Puppet::FileBucket::File.find("md5/#{bogus_checksum}", :diff_with => checksum).should == nil end end end describe "when initializing" do it "should use the filebucket settings section" do Puppet.settings.expects(:use).with(:filebucket) Puppet::FileBucketFile::File.new end end - describe "the find_by_checksum method" do - before do - # this is the default from spec_helper, but it keeps getting reset at odd times - Puppet[:bucketdir] = "/dev/null/bucket" - - @digest = "4a8ec4fa5f01b4ab1a0ab8cbccb709f0" - @checksum = "{md5}4a8ec4fa5f01b4ab1a0ab8cbccb709f0" - @dir = '/dev/null/bucket/4/a/8/e/c/4/f/a/4a8ec4fa5f01b4ab1a0ab8cbccb709f0' - - @contents = "file contents" - end - - it "should return nil if a file doesn't exist" do - ::File.expects(:exist?).with("#{@dir}/contents").returns false - - bucketfile = Puppet::FileBucketFile::File.new.send(:find_by_checksum, "{md5}#{@digest}", {}) - bucketfile.should == nil - end - - it "should find a filebucket if the file exists" do - ::File.expects(:exist?).with("#{@dir}/contents").returns true - ::File.expects(:exist?).with("#{@dir}/paths").returns false - ::File.expects(:read).with("#{@dir}/contents").returns @contents - - bucketfile = Puppet::FileBucketFile::File.new.send(:find_by_checksum, "{md5}#{@digest}", {}) - bucketfile.should_not == nil - end - - it "should load the paths" do - paths = ["path1", "path2"] - ::File.expects(:exist?).with("#{@dir}/contents").returns true - ::File.expects(:exist?).with("#{@dir}/paths").returns true - ::File.expects(:read).with("#{@dir}/contents").returns @contents - - mockfile = mock "file" - mockfile.expects(:readlines).returns( paths ) - ::File.expects(:open).with("#{@dir}/paths").yields mockfile - - Puppet::FileBucketFile::File.new.send(:find_by_checksum, "{md5}#{@digest}", {}).paths.should == paths - end - - end - describe "when retrieving files" do before :each do Puppet.settings.stubs(:use) @store = Puppet::FileBucketFile::File.new @digest = "70924d6fa4b2d745185fa4660703a5c0" - @sum = stub 'sum', :name => @digest - @dir = "/what/ever" + @bucket_dir = tmpdir("bucket") - Puppet.stubs(:[]).with(:bucketdir).returns(@dir) + Puppet.stubs(:[]).with(:bucketdir).returns(@bucket_dir) - @contents_path = '/what/ever/7/0/9/2/4/d/6/f/70924d6fa4b2d745185fa4660703a5c0/contents' - @paths_path = '/what/ever/7/0/9/2/4/d/6/f/70924d6fa4b2d745185fa4660703a5c0/paths' + @dir = "#{@bucket_dir}/7/0/9/2/4/d/6/f/70924d6fa4b2d745185fa4660703a5c0" + @contents_path = "#{@dir}/contents" - @request = stub 'request', :key => "md5/#{@digest}/remote/path", :options => {} + @request = Puppet::Indirector::Request.new(:indirection_name, :find, "md5/#{@digest}") end - it "should call find_by_checksum" do - @store.expects(:find_by_checksum).with{|x,opts| x == "{md5}#{@digest}"}.returns(false) - @store.find(@request) - end - - it "should look for the calculated path" do - ::File.expects(:exist?).with(@contents_path).returns(false) - @store.find(@request) + def make_bucketed_file + FileUtils.mkdir_p(@dir) + File.open(@contents_path, 'w') { |f| f.write @contents } end it "should return an instance of Puppet::FileBucket::File created with the content if the file exists" do - content = "my content" - bucketfile = stub 'bucketfile' - bucketfile.stubs(:bucket_path) - bucketfile.stubs(:bucket_path=) - bucketfile.stubs(:checksum_data).returns(@digest) - bucketfile.stubs(:checksum).returns(@checksum) - - bucketfile.expects(:contents=).with(content) - Puppet::FileBucket::File.expects(:new).with(nil, {:checksum => "{md5}#{@digest}"}).yields(bucketfile).returns(bucketfile) + @contents = "my content" + make_bucketed_file - ::File.expects(:exist?).with(@contents_path).returns(true) - ::File.expects(:exist?).with(@paths_path).returns(false) - ::File.expects(:read).with(@contents_path).returns(content) - - @store.find(@request).should equal(bucketfile) + bucketfile = @store.find(@request) + bucketfile.should be_a(Puppet::FileBucket::File) + bucketfile.contents.should == @contents end it "should return nil if no file is found" do - ::File.expects(:exist?).with(@contents_path).returns(false) @store.find(@request).should be_nil end - - it "should fail intelligently if a found file cannot be read" do - ::File.expects(:exist?).with(@contents_path).returns(true) - ::File.expects(:read).with(@contents_path).raises(RuntimeError) - proc { @store.find(@request) }.should raise_error(Puppet::Error) - end - - end - - describe "when determining file paths" do - before do - Puppet[:bucketdir] = '/dev/null/bucketdir' - @digest = 'DEADBEEFC0FFEE' - @bucket = stub_everything "bucket" - @bucket.expects(:checksum_data).returns(@digest) - end - - it "should use the value of the :bucketdir setting as the root directory" do - path = Puppet::FileBucketFile::File.new.send(:contents_path_for, @bucket) - path.should =~ %r{^/dev/null/bucketdir} - end - - it "should choose a path 8 directories deep with each directory name being the respective character in the filebucket" do - path = Puppet::FileBucketFile::File.new.send(:contents_path_for, @bucket) - dirs = @digest[0..7].split("").join(File::SEPARATOR) - path.should be_include(dirs) - end - - it "should use the full filebucket as the final directory name" do - path = Puppet::FileBucketFile::File.new.send(:contents_path_for, @bucket) - ::File.basename(::File.dirname(path)).should == @digest - end - - it "should use 'contents' as the actual file name" do - path = Puppet::FileBucketFile::File.new.send(:contents_path_for, @bucket) - ::File.basename(path).should == "contents" - end - - it "should use the bucketdir, the 8 sum character directories, the full filebucket, and 'contents' as the full file name" do - path = Puppet::FileBucketFile::File.new.send(:contents_path_for, @bucket) - path.should == ['/dev/null/bucketdir', @digest[0..7].split(""), @digest, "contents"].flatten.join(::File::SEPARATOR) - end end describe "when saving files" do before do # this is the default from spec_helper, but it keeps getting reset at odd times Puppet[:bucketdir] = "/dev/null/bucket" @digest = "4a8ec4fa5f01b4ab1a0ab8cbccb709f0" @checksum = "{md5}4a8ec4fa5f01b4ab1a0ab8cbccb709f0" @dir = '/dev/null/bucket/4/a/8/e/c/4/f/a/4a8ec4fa5f01b4ab1a0ab8cbccb709f0' @contents = "file contents" @bucket = stub "bucket file" @bucket.stubs(:bucket_path) @bucket.stubs(:checksum_data).returns(@digest) @bucket.stubs(:path).returns(nil) @bucket.stubs(:checksum).returns(nil) @bucket.stubs(:contents).returns("file contents") end it "should save the contents to the calculated path" do ::File.stubs(:directory?).with(@dir).returns(true) ::File.expects(:exist?).with("#{@dir}/contents").returns false mockfile = mock "file" mockfile.expects(:print).with(@contents) ::File.expects(:open).with("#{@dir}/contents", ::File::WRONLY|::File::CREAT, 0440).yields(mockfile) Puppet::FileBucketFile::File.new.send(:save_to_disk, @bucket) end it "should make any directories necessary for storage" do FileUtils.expects(:mkdir_p).with do |arg| ::File.umask == 0007 and arg == @dir end ::File.expects(:directory?).with(@dir).returns(false) ::File.expects(:open).with("#{@dir}/contents", ::File::WRONLY|::File::CREAT, 0440) ::File.expects(:exist?).with("#{@dir}/contents").returns false Puppet::FileBucketFile::File.new.send(:save_to_disk, @bucket) end end describe "when verifying identical files" do before do # this is the default from spec_helper, but it keeps getting reset at odd times Puppet[:bucketdir] = "/dev/null/bucket" @digest = "4a8ec4fa5f01b4ab1a0ab8cbccb709f0" @checksum = "{md5}4a8ec4fa5f01b4ab1a0ab8cbccb709f0" @dir = '/dev/null/bucket/4/a/8/e/c/4/f/a/4a8ec4fa5f01b4ab1a0ab8cbccb709f0' @contents = "file contents" @bucket = stub "bucket file" @bucket.stubs(:bucket_path) @bucket.stubs(:checksum).returns(@checksum) @bucket.stubs(:checksum_data).returns(@digest) @bucket.stubs(:path).returns(nil) @bucket.stubs(:contents).returns("file contents") end it "should raise an error if the files don't match" do File.expects(:read).with("#{@dir}/contents").returns("corrupt contents") lambda{ Puppet::FileBucketFile::File.new.send(:verify_identical_file!, @bucket) }.should raise_error(Puppet::FileBucket::BucketError) end it "should do nothing if the files match" do File.expects(:read).with("#{@dir}/contents").returns("file contents") Puppet::FileBucketFile::File.new.send(:verify_identical_file!, @bucket) end end - - - describe "when writing to the paths file" do - before do - Puppet[:bucketdir] = '/dev/null/bucketdir' - @digest = '70924d6fa4b2d745185fa4660703a5c0' - @bucket = stub_everything "bucket" - - @paths_path = '/dev/null/bucketdir/7/0/9/2/4/d/6/f/70924d6fa4b2d745185fa4660703a5c0/paths' - - @paths = [] - @bucket.stubs(:paths).returns(@paths) - @bucket.stubs(:checksum_data).returns(@digest) - end - - it "should create a file if it doesn't exist" do - @bucket.expects(:path).returns('path/to/save').at_least_once - File.expects(:exist?).with(@paths_path).returns(false) - file = stub "file" - file.expects(:puts).with('path/to/save') - File.expects(:open).with(@paths_path, ::File::WRONLY|::File::CREAT|::File::APPEND).yields(file) - - Puppet::FileBucketFile::File.new.send(:save_path_to_paths_file, @bucket) - end - - it "should append to a file if it exists" do - @bucket.expects(:path).returns('path/to/save').at_least_once - File.expects(:exist?).with(@paths_path).returns(true) - old_file = stub "file" - old_file.stubs(:readlines).returns [] - File.expects(:open).with(@paths_path).yields(old_file) - - file = stub "file" - file.expects(:puts).with('path/to/save') - File.expects(:open).with(@paths_path, ::File::WRONLY|::File::CREAT|::File::APPEND).yields(file) - - Puppet::FileBucketFile::File.new.send(:save_path_to_paths_file, @bucket) - end - - it "should not alter a file if it already contains the path" do - @bucket.expects(:path).returns('path/to/save').at_least_once - File.expects(:exist?).with(@paths_path).returns(true) - old_file = stub "file" - old_file.stubs(:readlines).returns ["path/to/save\n"] - File.expects(:open).with(@paths_path).yields(old_file) - - Puppet::FileBucketFile::File.new.send(:save_path_to_paths_file, @bucket) - end - - it "should do nothing if there is no path" do - @bucket.expects(:path).returns(nil).at_least_once - - Puppet::FileBucketFile::File.new.send(:save_path_to_paths_file, @bucket) - end - end - end