require ‘zip/zip’ require ‘sprout/version’ require ‘archive/tar/minitar’
## # Given a source, destination and archive type (or ability to infer it), # unpack the provided archive. # # unpacker = Sprout::ArchiveUnpacker.new # unpacker.unpack “Foo.zip”, “unpacked/” # class Sprout::ArchiveUnpacker
## # Unpack the provided +archive+ into the provided +destination+. # # If a +type+ is not provided, a type will be inferred from the file name suffix. # # @param archive [File] Path to the archive that will be unpacked (or copied) # @param destination [Path] Path to the folder where unpacked files should be placed (or copied). # @param type [Symbol] The type of the archive in cases where it can't be inferred # from the name. Acceptable values are: :zip, :tgz, :swc, :exe or :rb # @param clobber [Boolean] If the destination already contains the expected file(s), # the unpacker will not run unless +clobber+ is true. # @return [String] path to the unpacked files (usually same as destination). # @raise Sprout::Errors::UnknownArchiveType If the archive type cannot be inferred and a valid type is not provided. def unpack archive, destination, type=nil, clobber=nil return unpack_zip(archive, destination, clobber) if is_zip?(archive, type) return unpack_tgz(archive, destination, clobber) if is_tgz?(archive, type) # This is definitely debatable, should we copy the file even if it's # not an archive that we're about to unpack? # If so, why would we only do this with some subset of file types? # Opinions welcome here... return copy_file(archive, destination, clobber) if is_copyable?(archive) raise Sprout::Errors::UnknownArchiveType.new("Unsupported or unknown archive type encountered with: #{archive}") end ## # Unpack zip archives on any platform using whatever strategy is most # efficient and reliable. # # @param archive [File] Path to the archive that will be unpacked. # @param destination [Path] Path to the folder where unpacked files should be placed. # @param clobber [Boolean] If the destination already contains the expected file(s), # the unpacker will not run unless +clobber+ is true. # @return [File] the file or directory that was created. def unpack_zip archive, destination, clobber=nil validate archive, destination ## # As it turns out, the Rubyzip library corrupts # binary files (like the Flash Player) on OSX and is also # horribly slow for large archives (like the ~120MB Flex SDK) # on all platforms. if is_darwin? unpack_zip_on_darwin archive, destination, clobber else Zip::ZipFile.open archive do |zipfile| zipfile.each do |entry| next if entry.name =~ /__MACOSX/ or entry.name =~ /\.DS_Store/ unpack_zip_entry entry, destination, clobber end end end end ## # Return true if we're on a Darwin native system (OSX). # @return [Boolean] def is_darwin? Sprout.current_system.is_a?(Sprout::System::OSXSystem) end ## # Optimization for zip files on OSX. Uses the native # 'unzip' utility which is much faster (and more reliable) # than Ruby for large archives (like the Flex SDK) and # binaries that Ruby corrupts (like the Flash Player). # # @return [File] the file or directory that was created. def unpack_zip_on_darwin archive, destination, clobber # Unzipping on OS X FileUtils.makedirs destination zip_dir = File. File.dirname(archive) zip_name = File.basename archive output = File. destination # puts ">> zip_dir: #{zip_dir} zip_name: #{zip_name} output: #{output}" %x(cd #{zip_dir};unzip #{zip_name} -d #{output}) end ## # Unpack tar.gz or .tgz files on any platform. # # @return [File] the file or directory that was created. def unpack_tgz archive, destination, clobber=nil validate archive, destination tar = Zlib::GzipReader.new(File.open(archive, 'rb')) if(!should_unpack_tgz?(destination, clobber)) raise Sprout::Errors::DestinationExistsError.new "Unable to unpack #{archive} into #{destination} without explicit :clobber argument" end Archive::Tar::Minitar.unpack(tar, destination) # Recurse and unpack gzipped children (Adobe did this double # gzip with the Linux FlashPlayer for some weird reason) ["#{destination}/**/*.tgz", "#{destination}/**/*.tar.gz"].each do |pattern| Dir.glob(pattern).each do |child| if(child != archive && dir != File.dirname(child)) unpack_tgz(child, File.dirname(child)) end end end end ## # Rather than unpacking, safely copy the file from one location # to another. # This method is generally used when .exe files are downloaded # directly. # # @return [File] the file or directory that was created. def copy_file file, destination, clobber=nil validate file, destination target = File.( File.join(destination, File.basename(file)) ) if(File.exists?(target) && clobber != :clobber) raise Sprout::Errors::DestinationExistsError.new "Unable to copy #{file} to #{target} because target already exists and we were not asked to :clobber it" end FileUtils.mkdir_p destination FileUtils.cp_r file, destination destination end ## # Returns true if the provided file name looks like a zip file or the +type+ argument is +:zip+. # @return [Boolean] def is_zip? archive, type=nil type == :zip || !archive.match(/\.zip$/).nil? end ## # Return true if the provided file name looks like a tar.gz file or the +type+ argument is +:tgz+. # @return [Boolean] def is_tgz? archive, type=nil type == :tgz || !archive.match(/\.tgz$/).nil? || !archive.match(/\.tar.gz$/).nil? end ## # Return true if the downloaded archive is a .exe file or the +type+ argument is +:exe+. # @return [Boolean] def is_exe? archive, type=nil type == :exe || !archive.match(/\.exe$/).nil? end ## # Return true if the downloaded archive is a .swc file or the +type+ argument is +:swc+. # @return [Boolean] def is_swc? archive, type=nil type == :swc || !archive.match(/\.swc$/).nil? end ## # Return true if the downloaded archive is a .rb file or the +type+ argument is +:rb+. # @return [Boolean] def is_rb? archive, type=nil type == :rb || !archive.match(/\.rb$/).nil? end private ## # Return true if the provided archive should be copied as-is, rather # than being unpacked first. # @return [Boolean] def is_copyable? archive (is_exe?(archive) || is_swc?(archive) || is_rb?(archive)) end ## # Return true if the tgz should be unpacked. # @return [Boolean] def should_unpack_tgz? dir, clobber=nil return !directory_has_children?(dir) || clobber == :clobber end ## # Return true if the provided directory has one or more chidren. # @return [Boolean] def directory_has_children? dir (Dir.entries(dir) - ['.', '..']).size > 0 end ## # @return [Boolean] true if the +archive+ and +destination+ exist. # @raise Sprout::Errors::ArchiveUnpackerError if the +archive+ or +destination+ don't exist. def validate archive, destination = "Archive could not be found at: #{archive}" raise Sprout::Errors::ArchiveUnpackerError.new() unless valid_path?(archive) = "Destination could not be found at: #{destination}" raise Sprout::Errors::ArchiveUnpackerError.new() unless valid_path?(destination) true end ## # @return [Boolean] true if the provided +path+ is not nil and exists on disk. def valid_path? path !path.nil? && File.exists?(path) end ## # Unpack an entry from a zip archive. This is an _inconvenience_ method # thanks to the way Ruby zip handles zip archives. def unpack_zip_entry entry, destination, clobber # Ensure hidden mac files don't get written to disk: path = File.join destination, entry.name if entry.directory? # If an archive has empty directories: FileUtils.mkdir_p path elsif entry.file? # On Windows, we don't get the entry for # each parent directory: FileUtils.mkdir_p File.dirname(path) begin entry.extract path rescue Zip::ZipDestinationFileExistsError => zip_dest_error if(clobber == :clobber) FileUtils.rm_rf path entry.extract path else raise Sprout::Errors::DestinationExistsError.new zip_dest_error. end end end end
end