lib/sprout/archive_unpacker.rb in sprout-1.0.32.pre vs lib/sprout/archive_unpacker.rb in sprout-1.0.35.pre

- old
+ new

@@ -1,175 +1,241 @@ require 'zip/zip' +require 'sprout/version' require 'archive/tar/minitar' -module Sprout +## +# 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 - # Given a source, destination and type (or ability to infer it), - # unpack downloaded archives. - class 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) - # Figure out what kind of archive you have from the file name, - # and unpack it using the appropriate scheme. - 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) - # 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 - 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 - # Unpack zip archives on any platform. - # - # In case you're wondering... Ruby sucks... - # This code corrupts the FlashPlayer executable - # on OSX but if the file is manually unpacked, - # it works fine. - # - def unpack_zip archive, destination, clobber=nil - validate archive, destination - - 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/ + ## + # 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 + end - def is_darwin? - Sprout.current_system.is_a?(Sprout::System::OSXSystem) - 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 - def unpack_zip_on_darwin archive, destination, clobber - # Unzipping on OS X - FileUtils.makedirs destination - zip_dir = File.expand_path File.dirname(archive) - zip_name = File.basename archive - output = File.expand_path destination - # puts ">> zip_dir: #{zip_dir} zip_name: #{zip_name} output: #{output}" - %x(cd #{zip_dir};unzip #{zip_name} -d #{output}) - 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.expand_path File.dirname(archive) + zip_name = File.basename archive + output = File.expand_path 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. - def unpack_tgz archive, destination, clobber=nil - validate archive, destination + ## + # 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 + 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) + 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 + # 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. - def copy_file file, destination, clobber=nil - validate file, destination - target = File.expand_path( 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 + ## + # 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.expand_path( 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 - # Return true if the provided file name looks like a zip file. - def is_zip? archive, type=nil - type == :zip || !archive.match(/\.zip$/).nil? - end + destination + end - # Return true if the provided file name looks like a tar.gz file. - def is_tgz? archive, type=nil - type == :tgz || !archive.match(/\.tgz$/).nil? || !archive.match(/\.tar.gz$/).nil? - 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 - def is_exe? archive, type=nil - type == :exe || !archive.match(/\.exe$/).nil? - end - - def is_swc? archive, type=nil - type == :swc || !archive.match(/\.swc$/).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 - def is_rb? archive, type=nil - type == :rb || !archive.match(/\.rb$/).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 - private + ## + # 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 - def is_copyable? archive - (is_exe?(archive) || is_swc?(archive) || is_rb?(archive)) - 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 - def should_unpack_tgz? dir, clobber=nil - return !directory_has_children?(dir) || clobber == :clobber + private - end + ## + # 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 - def directory_has_children? dir - (Dir.entries(dir) - ['.', '..']).size > 0 - 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 - def validate archive, destination - validate_archive archive - validate_destination destination - end + end - def validate_archive archive - message = "Archive could not be found at: #{archive}" - raise Sprout::Errors::ArchiveUnpackerError.new(message) if archive.nil? || !File.exists?(archive) - 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 - def validate_destination path - message = "Archive destination could not be found at: #{path}" - raise Sprout::Errors::ArchiveUnpackerError.new(message) if path.nil? || !File.exists?(path) - 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_message = "Archive could not be found at: #{archive}" + raise Sprout::Errors::ArchiveUnpackerError.new(archive_message) unless valid_path?(archive) + destination_message = "Destination could not be found at: #{destination}" + raise Sprout::Errors::ArchiveUnpackerError.new(destination_message) unless valid_path?(destination) + true + end - def unpack_zip_entry entry, destination, clobber - # Ensure hidden mac files don't get written to disk: - path = File.join destination, entry.name + ## + # @return [Boolean] true if the provided +path+ is not nil and exists on disk. + def valid_path? path + !path.nil? && File.exists?(path) + end - 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 + ## + # 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 - 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.message - end + else + raise Sprout::Errors::DestinationExistsError.new zip_dest_error.message end end end - end end