require 'fileutils' require 'find' require 'minitar' require 'tempfile' require 'uri' require 'zlib' require 'r10k/settings' require 'r10k/settings/mixin' require 'r10k/util/platform' require 'r10k/util/cacheable' require 'r10k/util/downloader' module R10K class Tarball include R10K::Settings::Mixin include R10K::Util::Cacheable include R10K::Util::Downloader def_setting_attr :proxy # Defaults to global proxy setting def_setting_attr :cache_root, R10K::Util::Cacheable.default_cachedir # @!attribute [rw] name # @return [String] The tarball's name attr_accessor :name # @!attribute [rw] source # @return [String] The tarball's source attr_accessor :source # @!attribute [rw] checksum # @return [String] The tarball's expected sha256 digest attr_accessor :checksum # @param name [String] The name of the tarball content # @param source [String] The source for the tarball content # @param checksum [String] The sha256 digest of the tarball content def initialize(name, source, checksum: nil) @name = name @source = source @checksum = checksum # At this time, the only checksum type supported is sha256. In the future, # we may decide to support other algorithms if a use case arises. TBD. checksum_algorithm = :SHA256 end # @return [String] Directory. Where the cache_basename file will be created. def cache_dirname File.join(settings[:cache_root], 'tarball') end # The final cache_path should match one of the templates: # # - {cachedir}/{checksum}.tar.gz # - {cachedir}/{source}.tar.gz # # @return [String] File. The full file path the tarball will be cached to. def cache_path File.join(cache_dirname, cache_basename) end # @return [String] The basename of the tarball cache file. def cache_basename if checksum.nil? sanitized_dirname(source) + '.tar.gz' else checksum + '.tar.gz' end end # Extract the cached tarball to the target directory. # # @param target_dir [String] Where to unpack the tarball def unpack(target_dir) file = File.open(cache_path, 'rb') reader = Zlib::GzipReader.new(file) begin Minitar.unpack(reader, target_dir) ensure reader.close end end # @param target_dir [String] The directory to check if is in sync with the # tarball content # @param ignore_untracked_files [Boolean] If true, consider the target # dir to be in sync as long as all tracked content matches. # # @return [Boolean] def insync?(target_dir, ignore_untracked_files: false) target_tree_entries = Find.find(target_dir).map(&:to_s) - [target_dir] each_tarball_entry do |entry| found = target_tree_entries.delete(File.join(target_dir, entry.full_name.chomp('/'))) return false if found.nil? next if entry.directory? return false unless file_digest(found) == reader_digest(entry) end if ignore_untracked_files # We wouldn't have gotten this far if there were discrepancies in # tracked content true else # If there are still files in target_tree_entries, then there is # untracked content present in the target tree. If not, we're in sync. target_tree_entries.empty? end end # Download the tarball from @source to @cache_path def get Tempfile.open(cache_basename) do |tempfile| tempfile.binmode src_uri = URI.parse(source) temp_digest = case src_uri.scheme when 'file', nil copy(src_uri.path, tempfile) when %r{^[a-z]$} # Windows drive letter copy(src_uri.to_s, tempfile) when %r{^https?$} download(src_uri, tempfile) else raise "Unexpected source scheme #{src_uri.scheme}" end # Verify the download unless (checksum == temp_digest) || checksum.nil? raise 'Downloaded file does not match checksum' end # Move the download to cache_path FileUtils::mkdir_p(cache_dirname) begin FileUtils.mv(tempfile.path, cache_path) rescue Errno::EACCES # It may be the case that permissions don't permit moving the file # into place, but do permit overwriting an existing in-place file. FileUtils.cp(tempfile.path, cache_path) end end end # Checks the cached tarball's digest against the expected checksum. Returns # false if no cached file is present. If the tarball has no expected # checksum, any cached file is assumed to be valid. # # @return [Boolean] def cache_valid? return false unless File.exist?(cache_path) return true if checksum.nil? checksum == file_digest(cache_path) end # List all of the files contained in the tarball and their paths. This is # useful for implementing R10K::Purgable # # @return [Array] A normalized list of file paths contained in the archive def paths names = Array.new each_tarball_entry { |entry| names << Pathname.new(entry).cleanpath.to_s } names - ['.'] end def cache_checksum raise R10K::Error, _("Cache not present at %{path}") % {path: cache_path} unless File.exist?(cache_path) file_digest(cache_path) end private def each_tarball_entry(&block) File.open(cache_path, 'rb') do |file| Zlib::GzipReader.wrap(file) do |reader| Archive::Tar::Minitar::Input.each_entry(reader) do |entry| yield entry end end end end end end