lib/sprout/archive_unpacker.rb in sprout-0.7.246-darwin vs lib/sprout/archive_unpacker.rb in sprout-1.0.0.pre

- old
+ new

@@ -1,235 +1,151 @@ +require 'zip/zip' +require 'archive/tar/minitar' module Sprout - class ArchiveUnpackerError < StandardError #:nodoc: - end - # Unpack downloaded files from a variety of common archive types. - # This library should efficiently extract archived files - # on OS X, Win XP, Vista, DOS, Cygwin, and Linux. - # - # It will attempt to infer the archive type by standard mime-type file - # extensions, but if there is a file with no extension, the unpack_archive - # method can be provided with an @archive_type symbol argument that is one - # of the following values: - # :exe - # :zip - # :targz - # :gzip - # :swc - # :rb - # :dmg - class ArchiveUnpacker #:nodoc: - include Archive::Tar - - def unpack_archive(file_name, dir, force=false, archive_type=nil) - archive_type ||= inferred_archive_type(file_name) - suffix = suffix_for_archive_type(archive_type) - - unpacked = unpacked_file_name(file_name, dir, suffix) - if(File.exists?(unpacked) && force) - FileUtils.rm_rf(unpacked) - end - - if(!File.exists?(unpacked)) - case archive_type.to_s - when 'zip' - unpack_zip(file_name, dir) - when 'targz' - unpack_targz(file_name, dir) - when 'tbz2' - unpack_tbz2(file_name, dir) - when 'dmg' - unpack_dmg(file_name, dir) - when 'exe' - FileUtils.mkdir_p(dir) - FileUtils.move(file_name, dir) - when 'swc' || 'rb' - return - else - raise ArchiveUnpackerError.new("ArchiveUnpacker does not know how to unpack files of type: #{archive_type} for file_name: #{file_name}") - end - end + # Given a source, destination and type (or ability to infer it), + # unpack downloaded archives. + class ArchiveUnpacker + + # 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) + + raise Sprout::Errors::UnknownArchiveType.new("Unsupported or unknown archive type encountered with: #{archive}") end - - def unpack_zip(zip_file, dir) - # Avoid the rubyzip Segmentation Fault bug - # at least on os x... - if(RUBY_PLATFORM =~ /darwin/) - # Unzipping on OS X - FileUtils.makedirs(dir) - zip_dir = File.expand_path(File.dirname(zip_file)) - zip_name = File.basename(zip_file) - output = File.expand_path(dir) - # puts ">> zip_dir: #{zip_dir} zip_name: #{zip_name} output: #{output}" - %x(cd #{zip_dir};unzip #{zip_name} -d #{output}) - else - retries = 0 - begin - retries += 1 - Zip::ZipFile::open(zip_file) do |zf| - zf.each do |e| - fpath = File.join(dir, e.name) - FileUtils.mkdir_p(File.dirname(fpath)) - # Disgusting, Gross Hack to fix DOS/Ruby Bug - # That makes the zip library throw a ZipDestinationFileExistsError - # When the zip archive includes two files whose names - # differ only by extension. - # This bug actually appears in the File.exists? implementation - # throwing false positives! - # If you're going to use this code, be sure you extract - # into a new, empty directory as existing files will be - # clobbered... - begin - if(File.exists?(fpath) && !File.directory?(fpath)) - hackpath = fpath + 'hack' - zf.extract(e, hackpath) - File.copy(hackpath, fpath) - File.delete(hackpath) - else - zf.extract(e, fpath) - end - rescue NotImplementedError => ni_err - puts "[WARNING] #{ni_err} for: #{e}" - end - end - end - rescue StandardError => err - FileUtils.rm_rf(dir) - if(retries < 3) - FileUtils.makedirs(dir) - retry - end - raise err + + # Unpack zip archives on any platform. + def unpack_zip archive, destination, clobber=nil + validate archive, destination + + 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 - - def unpacked_file_name(file, dir, suffix=nil) - basename = File.basename(file, suffix) - path = File.expand_path(dir) - return File.join(path, basename) - end - def unpack_tbz2(tgz_file, dir) - unpack_tar_zip tgz_file, dir, 'tar.bz2' - end + # Unpack tar.gz or .tgz files on any platform. + def unpack_tgz archive, destination, clobber=nil + validate archive, destination - def unpack_targz(tgz_file, dir) - unpack_tar_zip tgz_file, dir, 'tar.gz' - end - - def unpack_tar_zip(tgz_file, dir, ext) - if(!File.exists?(dir)) - FileUtils.makedirs(dir) + 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(tgz_file, 'rb')) - Minitar.unpack(tar, dir) - + + Archive::Tar::Minitar.unpack(tar, destination) + # Recurse and unpack gzipped children (Adobe did this double - # gzip with the Linux FlashPlayer for some reason) - Dir.glob("#{dir}/**/*.#{ext}").each do |child| - if(child != tgz_file && dir != File.dirname(child)) - unpack_targz(child, File.dirname(child)) + # 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 - - # This is actually not unpacking the FlashPlayer - # Binary file as expected... - # OSX is treated the player binary as if it is - # a regular text file, but if it is copied manually, - # the command works fine!? - def unpack_dmg(dmg_file, dir) - # 1) Mount the dmg in place - # 2) Recursively Copy its contents to asproject_home - # 3) Unmount the dmg - if(mounted_path.nil?) - raise StandardError.new('DMG file downloaded, but the RemoteFileTask needs a mounted_path in order to mount it') - end - if(!File.exists?(full_mounted_path)) - system("hdiutil mount #{dmg_file}") + # 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 - - begin - mounted_target = File.join(full_mounted_path, extracted_file) - - # Copy the DMG contents using system copy rather than ruby utils - # Because OS X does something special with .app files that the - # Ruby FileUtils and File classes break... - from = mounted_target -# from = File.join(full_mounted_path, extracted_file) - to = File.join(@user.downloads, @name.to_s, extracted_file) - FileUtils.makedirs(File.dirname(to)) - - if(File.exists?(from)) - `ditto '#{from}' '#{to}'` - end - rescue StandardError => e - if(File.exists?(full_mounted_path)) - system("hdiutil unmount -force \"#{full_mounted_path}\"") - end - end + FileUtils.mkdir_p destination + FileUtils.cp_r file, destination + + destination end - - def suffix_for_archive_type(type) - if(type == :targz) - return '.tar.gz' - else - return ".#{type.to_s}" - end + + # 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 - - def inferred_archive_type(file_name) - if is_zip?(file_name) - return :zip - elsif is_targz?(file_name) - return :targz - elsif is_gzip?(file_name) - return :gz - elsif is_swc?(file_name) - return :swc - elsif is_rb?(file_name) - return :rb - elsif is_dmg?(file_name) - return :dmg - elsif is_exe?(file_name) - return :exe - else - return nil - 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 - - def is_zip?(file) - return (file.split('.').pop == 'zip') + + def is_exe? archive, type=nil + type == :exe || !archive.match(/\.exe$/).nil? end - def is_targz?(file) - parts = file.split('.') - part = parts.pop - return (part == 'tgz' || part == 'gz' && parts.pop == 'tar') + def is_swc? archive, type=nil + type == :swc || !archive.match(/\.swc$/).nil? end - - def is_gzip?(file) - return (file.split('.').pop == 'gz') + + def is_rb? archive, type=nil + type == :rb || !archive.match(/\.rb$/).nil? end - - def is_swc?(file) - return (file.split('.').pop == 'swc') + + private + + def is_copyable? archive + (is_exe?(archive) || is_swc?(archive) || is_rb?(archive)) end - - def is_rb?(file) - return (file.split('.').pop == 'rb') + + def should_unpack_tgz? dir, clobber=nil + return !directory_has_children?(dir) || clobber == :clobber + end - - def is_dmg?(file) - return (file.split('.').pop == 'dmg') - end - def is_exe?(file) - return (file.split('.').pop == 'exe') + def directory_has_children? dir + (Dir.entries(dir) - ['.', '..']).size > 0 end + + def validate archive, destination + validate_archive archive + validate_destination destination + 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 + + 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 + + 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.message + end + end + end + end + end end +