summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLuke Kanies <luke@madstop.com>2007-10-22 15:24:50 -0500
committerLuke Kanies <luke@madstop.com>2007-10-22 15:24:50 -0500
commit393a3e8743f503543ad34a874e9296d684b0d49b (patch)
treed05b94cb9424b0b9a41a2bf3a9b541e0af26d8b5
parentaab157e77bf471269ef0403576eca0e77e6f51ec (diff)
downloadpuppet-393a3e8743f503543ad34a874e9296d684b0d49b.tar.gz
puppet-393a3e8743f503543ad34a874e9296d684b0d49b.tar.xz
puppet-393a3e8743f503543ad34a874e9296d684b0d49b.zip
Adding a Fileset class for managing sets of files. This
is the new server-side for file recursion, and I'll next be hooking it to the fileserving 'search' methods. This is basically a mechanism for abstracting that search functionality into a single class.
-rw-r--r--lib/puppet/file_serving/fileset.rb138
-rwxr-xr-xspec/unit/file_serving/fileset.rb230
2 files changed, 368 insertions, 0 deletions
diff --git a/lib/puppet/file_serving/fileset.rb b/lib/puppet/file_serving/fileset.rb
new file mode 100644
index 000000000..7f7b9fc2d
--- /dev/null
+++ b/lib/puppet/file_serving/fileset.rb
@@ -0,0 +1,138 @@
+#
+# Created by Luke Kanies on 2007-10-22.
+# Copyright (c) 2007. All rights reserved.
+
+require 'find'
+require 'puppet/file_serving'
+require 'puppet/file_serving/metadata'
+
+# Operate recursively on a path, returning a set of file paths.
+class Puppet::FileServing::Fileset
+ attr_reader :path, :ignore, :links
+ attr_accessor :recurse
+
+ # Find our collection of files. This is different from the
+ # normal definition of find in that we support specific levels
+ # of recursion, which means we need to know when we're going another
+ # level deep, which Find doesn't do.
+ def find
+ files = perform_recursion
+
+ # Now strip off the leading path, so each file becomes relative, and remove
+ # any slashes that might end up at the beginning of the path.
+ result = files.collect { |file| file.sub(%r{^#{@path}/*}, '') }
+
+ # And add the path itself.
+ result.unshift(".")
+
+ result
+ end
+
+ # Should we ignore this path?
+ def ignore?(path)
+ # 'detect' normally returns the found result, whereas we just want true/false.
+ ! @ignore.detect { |pattern| File.fnmatch?(pattern, path) }.nil?
+ end
+
+ def ignore=(values)
+ values = [values] unless values.is_a?(Array)
+ @ignore = values
+ end
+
+ def initialize(path, options = {})
+ raise ArgumentError.new("Fileset paths must be fully qualified") unless path =~ /^#{::File::SEPARATOR}/
+
+ @path = path
+
+ # Set our defaults.
+ @ignore = []
+ @links = :manage
+ @recurse = false
+
+ options.each do |option, value|
+ method = option.to_s + "="
+ begin
+ send(method, value)
+ rescue NoMethodError
+ raise ArgumentError, "Invalid option '%s'" % option
+ end
+ end
+
+ raise ArgumentError.new("Fileset paths must exist") unless stat = stat(path)
+ end
+
+ def links=(links)
+ links = links.intern if links.is_a?(String)
+ raise(ArgumentError, "Invalid :links value '%s'" % links) unless [:manage, :follow].include?(links)
+ @links = links
+ @stat_method = links == :manage ? :lstat : :stat
+ end
+
+ # Should we recurse further? This is basically a single
+ # place for all of the logic around recursion.
+ def recurse?(depth)
+ # If recurse is true, just return true
+ return true if self.recurse == true
+
+ # Return false if the value is false or zero.
+ return false if [false, 0].include?(self.recurse)
+
+ # Return true if our current depth is less than the allowed recursion depth.
+ return true if self.recurse.is_a?(Fixnum) and depth <= self.recurse
+
+ # Else, return false.
+ return false
+ end
+
+ private
+
+ # Pull the recursion logic into one place. It's moderately hairy, and this
+ # allows us to keep the hairiness apart from what we do with the files.
+ def perform_recursion
+ # Start out with just our base directory.
+ current_dirs = [@path]
+
+ next_dirs = []
+
+ depth = 1
+
+ result = []
+ return result unless recurse?(depth)
+
+ while dir_path = current_dirs.shift or ((depth += 1) and recurse?(depth) and current_dirs = next_dirs and next_dirs = [] and dir_path = current_dirs.shift)
+ next unless stat = stat(dir_path)
+ next unless stat.directory?
+
+ Dir.entries(dir_path).each do |file_path|
+ next if [".", ".."].include?(file_path)
+
+ # Note that this also causes matching directories not
+ # to be recursed into.
+ next if ignore?(file_path)
+
+ # Add it to our list of files to return
+ result << File.join(dir_path, file_path)
+
+ # And to our list of files/directories to iterate over.
+ next_dirs << File.join(dir_path, file_path)
+ end
+ end
+
+ return result
+ end
+
+ # Stat a given file, using the links-appropriate method.
+ def stat(path)
+ unless defined?(@stat_method)
+ @stat_method = self.links == :manage ? :lstat : :stat
+ end
+
+ begin
+ return File.send(@stat_method, path)
+ rescue
+ # If this happens, it is almost surely because we're
+ # trying to manage a link to a file that does not exist.
+ return nil
+ end
+ end
+end
diff --git a/spec/unit/file_serving/fileset.rb b/spec/unit/file_serving/fileset.rb
new file mode 100755
index 000000000..518d7298d
--- /dev/null
+++ b/spec/unit/file_serving/fileset.rb
@@ -0,0 +1,230 @@
+#!/usr/bin/env ruby
+
+require File.dirname(__FILE__) + '/../../spec_helper'
+
+require 'puppet/file_serving/fileset'
+
+describe Puppet::FileServing::Fileset, " when initializing" do
+ it "should require a path" do
+ proc { Puppet::FileServing::Fileset.new }.should raise_error(ArgumentError)
+ end
+
+ it "should fail if its path is not fully qualified" do
+ proc { Puppet::FileServing::Fileset.new("some/file") }.should raise_error(ArgumentError)
+ end
+
+ it "should fail if its path does not exist" do
+ File.expects(:lstat).with("/some/file").returns nil
+ proc { Puppet::FileServing::Fileset.new("/some/file") }.should raise_error(ArgumentError)
+ end
+
+ it "should accept a 'recurse' option" do
+ File.expects(:lstat).with("/some/file").returns stub("stat")
+ set = Puppet::FileServing::Fileset.new("/some/file", :recurse => true)
+ set.recurse.should be_true
+ end
+
+ it "should accept an 'ignore' option" do
+ File.expects(:lstat).with("/some/file").returns stub("stat")
+ set = Puppet::FileServing::Fileset.new("/some/file", :ignore => ".svn")
+ set.ignore.should == [".svn"]
+ end
+
+ it "should accept a 'links' option" do
+ File.expects(:lstat).with("/some/file").returns stub("stat")
+ set = Puppet::FileServing::Fileset.new("/some/file", :links => :manage)
+ set.links.should == :manage
+ end
+
+ it "should fail if 'links' is set to anything other than :manage or :follow" do
+ proc { Puppet::FileServing::Fileset.new("/some/file", :links => :whatever) }.should raise_error(ArgumentError)
+ end
+
+ it "should default to 'false' for recurse" do
+ File.expects(:lstat).with("/some/file").returns stub("stat")
+ Puppet::FileServing::Fileset.new("/some/file").recurse.should == false
+ end
+
+ it "should default to an empty ignore list" do
+ File.expects(:lstat).with("/some/file").returns stub("stat")
+ Puppet::FileServing::Fileset.new("/some/file").ignore.should == []
+ end
+
+ it "should default to :manage for links" do
+ File.expects(:lstat).with("/some/file").returns stub("stat")
+ Puppet::FileServing::Fileset.new("/some/file").links.should == :manage
+ end
+end
+
+describe Puppet::FileServing::Fileset, " when determining whether to recurse" do
+ before do
+ @path = "/my/path"
+ File.expects(:lstat).with(@path).returns stub("stat")
+ @fileset = Puppet::FileServing::Fileset.new(@path)
+ end
+
+ it "should always recurse if :recurse is set to 'true'" do
+ @fileset.recurse = true
+ @fileset.recurse?(0).should be_true
+ end
+
+ it "should never recurse if :recurse is set to 'false'" do
+ @fileset.recurse = false
+ @fileset.recurse?(-1).should be_false
+ end
+
+ it "should recurse if :recurse is set to an integer and the current depth is less than that integer" do
+ @fileset.recurse = 1
+ @fileset.recurse?(0).should be_true
+ end
+
+ it "should recurse if :recurse is set to an integer and the current depth is equal to that integer" do
+ @fileset.recurse = 1
+ @fileset.recurse?(1).should be_true
+ end
+
+ it "should not recurse if :recurse is set to an integer and the current depth is great than that integer" do
+ @fileset.recurse = 1
+ @fileset.recurse?(2).should be_false
+ end
+
+ it "should not recurse if :recurse is set to 0" do
+ @fileset.recurse = 0
+ @fileset.recurse?(-1).should be_false
+ end
+end
+
+describe Puppet::FileServing::Fileset, " when recursing" do
+ before do
+ @path = "/my/path"
+ File.expects(:lstat).with(@path).returns stub("stat", :directory? => true)
+ @fileset = Puppet::FileServing::Fileset.new(@path)
+
+ @dirstat = stub 'dirstat', :directory? => true
+ @filestat = stub 'filestat', :directory? => false
+ end
+
+ def mock_dir_structure(path, stat_method = :lstat)
+ File.stubs(stat_method).with(path).returns(@dirstat)
+ Dir.stubs(:entries).with(path).returns(%w{one two .svn CVS})
+
+ # Keep track of the files we're stubbing.
+ @files = %w{.}
+
+ %w{one two .svn CVS}.each do |subdir|
+ @files << subdir # relative path
+ subpath = File.join(path, subdir)
+ File.stubs(stat_method).with(subpath).returns(@dirstat)
+ Dir.stubs(:entries).with(subpath).returns(%w{.svn CVS file1 file2})
+ %w{file1 file2 .svn CVS}.each do |file|
+ @files << File.join(subdir, file) # relative path
+ File.stubs(stat_method).with(File.join(subpath, file)).returns(@filestat)
+ end
+ end
+ end
+
+ it "should recurse through the whole file tree if :recurse is set to 'true'" do
+ mock_dir_structure(@path)
+ @fileset.stubs(:recurse?).returns(true)
+ @fileset.find.sort.should == @files.sort
+ end
+
+ it "should not recurse if :recurse is set to 'false'" do
+ mock_dir_structure(@path)
+ @fileset.stubs(:recurse?).returns(false)
+ @fileset.find.should == %w{.}
+ end
+
+ # It seems like I should stub :recurse? here, or that I shouldn't stub the
+ # examples above, but...
+ it "should recurse to the level set if :recurse is set to an integer" do
+ mock_dir_structure(@path)
+ @fileset.recurse = 1
+ @fileset.find.should == %w{. one two .svn CVS}
+ end
+
+ it "should ignore the '.' and '..' directories in subdirectories" do
+ mock_dir_structure(@path)
+ @fileset.recurse = true
+ @fileset.find.sort.should == @files.sort
+ end
+
+ it "should ignore files that match a single pattern in the ignore list" do
+ mock_dir_structure(@path)
+ @fileset.recurse = true
+ @fileset.ignore = ".svn"
+ @fileset.find.find { |file| file.include?(".svn") }.should be_nil
+ end
+
+ it "should ignore files that match any of multiple patterns in the ignore list" do
+ mock_dir_structure(@path)
+ @fileset.recurse = true
+ @fileset.ignore = %w{.svn CVS}
+ @fileset.find.find { |file| file.include?(".svn") or file.include?("CVS") }.should be_nil
+ end
+
+ it "should use File.stat if :links is set to :follow" do
+ mock_dir_structure(@path, :stat)
+ @fileset.recurse = true
+ @fileset.links = :follow
+ @fileset.find.sort.should == @files.sort
+ end
+
+ it "should use File.lstat if :links is set to :manage" do
+ mock_dir_structure(@path, :lstat)
+ @fileset.recurse = true
+ @fileset.links = :manage
+ @fileset.find.sort.should == @files.sort
+ end
+end
+
+describe Puppet::FileServing::Fileset, " when following links that point to missing files" do
+ before do
+ @path = "/my/path"
+ File.expects(:lstat).with(@path).returns stub("stat", :directory? => true)
+ @fileset = Puppet::FileServing::Fileset.new(@path)
+ @fileset.links = :follow
+ @fileset.recurse = true
+
+ @stat = stub 'stat', :directory? => true
+
+ File.expects(:stat).with(@path).returns(@stat)
+ File.expects(:stat).with(File.join(@path, "mylink")).raises(Errno::ENOENT)
+ Dir.stubs(:entries).with(@path).returns(["mylink"])
+ end
+
+ it "should not fail" do
+ proc { @fileset.find }.should_not raise_error
+ end
+
+ it "should still manage the link" do
+ @fileset.find.sort.should == %w{. mylink}.sort
+ end
+end
+
+describe Puppet::FileServing::Fileset, " when ignoring" do
+ before do
+ @path = "/my/path"
+ File.expects(:lstat).with(@path).returns stub("stat", :directory? => true)
+ @fileset = Puppet::FileServing::Fileset.new(@path)
+ end
+
+ it "should use ruby's globbing to determine what files should be ignored" do
+ @fileset.ignore = ".svn"
+ File.expects(:fnmatch?).with(".svn", "my_file")
+ @fileset.ignore?("my_file")
+ end
+
+ it "should ignore files whose paths match a single provided ignore value" do
+ @fileset.ignore = ".svn"
+ File.stubs(:fnmatch?).with(".svn", "my_file").returns true
+ @fileset.ignore?("my_file").should be_true
+ end
+
+ it "should ignore files whose paths match any of multiple provided ignore values" do
+ @fileset.ignore = [".svn", "CVS"]
+ File.stubs(:fnmatch?).with(".svn", "my_file").returns false
+ File.stubs(:fnmatch?).with("CVS", "my_file").returns true
+ @fileset.ignore?("my_file").should be_true
+ end
+end