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, files_original_path = request_to_checksum_and_path( request ) dir_path = path_for(request.options[:bucket_path], checksum) file_path = ::File.join(dir_path, 'contents') return nil unless ::File.exists?(file_path) return nil unless path_match(dir_path, files_original_path) if request.options[:diff_with] hash_protocol = sumtype(checksum) 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 = IO.binread(file_path) Puppet.info "FileBucket read #{checksum}" model.new(contents) end end def head(request) checksum, files_original_path = request_to_checksum_and_path(request) dir_path = path_for(request.options[:bucket_path], checksum) ::File.exists?(::File.join(dir_path, 'contents')) and path_match(dir_path, files_original_path) end def save( request ) instance = request.instance checksum, files_original_path = request_to_checksum_and_path(request) save_to_disk(instance, files_original_path) instance.to_s end private def path_match(dir_path, files_original_path) return true unless files_original_path # if no path was provided, it's a match paths_path = ::File.join(dir_path, 'paths') return false unless ::File.exists?(paths_path) ::File.open(paths_path) do |f| f.each_line do |line| return true if line.chomp == files_original_path end end return false end def save_to_disk( bucket_file, files_original_path ) filename = path_for(bucket_file.bucket_path, bucket_file.checksum_data, 'contents') dir_path = path_for(bucket_file.bucket_path, bucket_file.checksum_data) paths_path = ::File.join(dir_path, 'paths') # 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?(dir_path) Puppet::Util.withumask(0007) do ::FileUtils.mkdir_p(dir_path) end end Puppet.info "FileBucket adding #{bucket_file.checksum}" # Write the file to disk. Puppet::Util.withumask(0007) do ::File.open(filename, ::File::WRONLY|::File::CREAT, 0440) do |of| of.binmode of.print bucket_file.contents end ::File.open(paths_path, ::File::WRONLY|::File::CREAT, 0640) do |of| # path will be written below end end end unless path_match(dir_path, files_original_path) ::File.open(paths_path, 'a') do |f| f.puts(files_original_path) end end end def request_to_checksum_and_path( request ) checksum_type, checksum, path = request.key.split(/\//, 3) if path == '' # Treat "md5//" like "md5/" path = nil end raise "Unsupported checksum type #{checksum_type.inspect}" if checksum_type != 'md5' raise "Invalid checksum #{checksum.inspect}" if checksum !~ /^[0-9a-f]{32}$/ [checksum, path] end 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 # 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) disk_contents = IO.binread(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}" else Puppet.info "FileBucket got a duplicate file #{bucket_file.checksum}" end end end end