#!/usr/bin/env ruby #-- # Archive::Tar::Minitar 0.5.1 # Copyright © 2004 Mauricio Julio Fernández Pradier and Austin Ziegler # # This program is based on and incorporates parts of RPA::Package from # rpa-base (lib/rpa/package.rb and lib/rpa/util.rb) by Mauricio and has been # adapted to be more generic by Austin. # # It is licensed under the GNU General Public Licence or Ruby's licence. # # $Id: minitar.rb,v 1.3 2004/09/22 17:47:43 austin Exp $ #++ module Archive; end module Archive::Tar; end # = Archive::Tar::PosixHeader # Implements the POSIX tar header as a Ruby class. The structure of # the POSIX tar header is: # # struct tarfile_entry_posix # { // pack/unpack # char name[100]; // ASCII (+ Z unless filled) a100/Z100 # char mode[8]; // 0 padded, octal, null a8 /A8 # char uid[8]; // ditto a8 /A8 # char gid[8]; // ditto a8 /A8 # char size[12]; // 0 padded, octal, null a12 /A12 # char mtime[12]; // 0 padded, octal, null a12 /A12 # char checksum[8]; // 0 padded, octal, null, space a8 /A8 # char typeflag[1]; // see below a /a # char linkname[100]; // ASCII + (Z unless filled) a100/Z100 # char magic[6]; // "ustar\0" a6 /A6 # char version[2]; // "00" a2 /A2 # char uname[32]; // ASCIIZ a32 /Z32 # char gname[32]; // ASCIIZ a32 /Z32 # char devmajor[8]; // 0 padded, octal, null a8 /A8 # char devminor[8]; // 0 padded, octal, null a8 /A8 # char prefix[155]; // ASCII (+ Z unless filled) a155/Z155 # }; # # The +typeflag+ may be one of the following known values: # # "0":: Regular file. NULL should be treated as a synonym, for # compatibility purposes. # "1":: Hard link. # "2":: Symbolic link. # "3":: Character device node. # "4":: Block device node. # "5":: Directory. # "6":: FIFO node. # "7":: Reserved. # # POSIX indicates that "A POSIX-compliant implementation must treat any # unrecognized typeflag value as a regular file." class Archive::Tar::PosixHeader FIELDS = %w(name mode uid gid size mtime checksum typeflag linkname) + %w(magic version uname gname devmajor devminor prefix) FIELDS.each { |field| attr_reader field.intern } HEADER_PACK_FORMAT = "a100a8a8a8a12a12a7aaa100a6a2a32a32a8a8a155" HEADER_UNPACK_FORMAT = "Z100A8A8A8A12A12A8aZ100A6A2Z32Z32A8A8Z155" # Creates a new PosixHeader from a data stream. def self.new_from_stream(stream) data = stream.read(512) fields = data.unpack(HEADER_UNPACK_FORMAT) name = fields.shift mode = fields.shift.oct uid = fields.shift.oct gid = fields.shift.oct size = fields.shift.oct mtime = fields.shift.oct checksum = fields.shift.oct typeflag = fields.shift linkname = fields.shift magic = fields.shift version = fields.shift.oct uname = fields.shift gname = fields.shift devmajor = fields.shift.oct devminor = fields.shift.oct prefix = fields.shift empty = (data == "\0" * 512) new(:name => name, :mode => mode, :uid => uid, :gid => gid, :size => size, :mtime => mtime, :checksum => checksum, :typeflag => typeflag, :magic => magic, :version => version, :uname => uname, :gname => gname, :devmajor => devmajor, :devminor => devminor, :prefix => prefix, :empty => empty) end # Creates a new PosixHeader. A PosixHeader cannot be created unless the # #name, #size, #prefix, and #mode are provided. def initialize(vals) unless vals[:name] && vals[:size] && vals[:prefix] && vals[:mode] raise ArgumentError end vals[:mtime] ||= 0 vals[:checksum] ||= "" vals[:typeflag] ||= "0" vals[:magic] ||= "ustar" vals[:version] ||= "00" FIELDS.each do |field| instance_variable_set("@#{field}", vals[field.intern]) end @empty = vals[:empty] end def empty? @empty end def to_s update_checksum header(@checksum) end # Update the checksum field. def update_checksum hh = header(" " * 8) @checksum = oct(calculate_checksum(hh), 6) end private def oct(num, len) if num.nil? "\0" * (len + 1) else "%0#{len}o" % num end end def calculate_checksum(hdr) hdr.unpack("C*").inject { |aa, bb| aa + bb } end def header(chksum) arr = [name, oct(mode, 7), oct(uid, 7), oct(gid, 7), oct(size, 11), oct(mtime, 11), chksum, " ", typeflag, linkname, magic, version, uname, gname, oct(devmajor, 7), oct(devminor, 7), prefix] str = arr.pack(HEADER_PACK_FORMAT) str + "\0" * ((512 - str.size) % 512) end end require 'fileutils' require 'find' # = Archive::Tar::Minitar 0.5.1 # Archive::Tar::Minitar is a pure-Ruby library and command-line # utility that provides the ability to deal with POSIX tar(1) archive # files. The implementation is based heavily on Mauricio Fernández's # implementation in rpa-base, but has been reorganised to promote # reuse in other projects. # # This tar class performs a subset of all tar (POSIX tape archive) # operations. We can only deal with typeflags 0, 1, 2, and 5 (see # Archive::Tar::PosixHeader). All other typeflags will be treated as # normal files. # # NOTE::: support for typeflags 1 and 2 is not yet implemented in this # version. # # This release is version 0.5.1. The library can only handle files and # directories at this point. A future version will be expanded to # handle symbolic links and hard links in a portable manner. The # command line utility, minitar, can only create archives, extract # from archives, and list archive contents. # # == Synopsis # Using this library is easy. The simplest case is: # # require 'zlib' # require 'archive/tar/minitar' # include Archive::Tar # # # Packs everything that matches Find.find('tests') # File.open('test.tar', 'wb') { |tar| Minitar.pack('tests', tar) } # # Unpacks 'test.tar' to 'x', creating 'x' if necessary. # Minitar.unpack('test.tar', 'x') # # A gzipped tar can be written with: # # tgz = Zlib::GzipWriter.new(File.open('test.tgz', 'wb')) # # Warning: tgz will be closed! # Minitar.pack('tests', tgz) # # tgz = Zlib::GzipReader.new(File.open('test.tgz', 'rb')) # # Warning: tgz will be closed! # Minitar.unpack(tgz, 'x') # # As the case above shows, one need not write to a file. However, it # will sometimes require that one dive a little deeper into the API, # as in the case of StringIO objects. Note that I'm not providing a # block with Minitar::Output, as Minitar::Output#close automatically # closes both the Output object and the wrapped data stream object. # # begin # sgz = Zlib::GzipWriter.new(StringIO.new("")) # tar = Output.new(sgz) # Find.find('tests') do |entry| # Minitar.pack_file(entry, tar) # end # ensure # # Closes both tar and sgz. # tar.close # end # # == Copyright # Copyright 2004 Mauricio Julio Fernández Pradier and Austin Ziegler # # This program is based on and incorporates parts of RPA::Package from # rpa-base (lib/rpa/package.rb and lib/rpa/util.rb) by Mauricio and # has been adapted to be more generic by Austin. # # 'minitar' contains an adaptation of Ruby/ProgressBar by Satoru # Takabayashi , copyright © 2001 - 2004. # # This program is free software. It may be redistributed and/or # modified under the terms of the GPL version 2 (or later) or Ruby's # licence. module Archive::Tar::Minitar VERSION = "0.5.1" # The exception raised when a wrapped data stream class is expected to # respond to #rewind or #pos but does not. class NonSeekableStream < StandardError; end # The exception raised when a block is required for proper operation of # the method. class BlockRequired < ArgumentError; end # The exception raised when operations are performed on a stream that has # previously been closed. class ClosedStream < StandardError; end # The exception raised when a filename exceeds 256 bytes in length, # the maximum supported by the standard Tar format. class FileNameTooLong < StandardError; end # The exception raised when a data stream ends before the amount of data # expected in the archive's PosixHeader. class UnexpectedEOF < StandardError; end # The class that writes a tar format archive to a data stream. class Writer # A stream wrapper that can only be written to. Any attempt to read # from this restricted stream will result in a NameError being thrown. class RestrictedStream def initialize(anIO) @io = anIO end def write(data) @io.write(data) end end # A RestrictedStream that also has a size limit. class BoundedStream < Archive::Tar::Minitar::Writer::RestrictedStream # The exception raised when the user attempts to write more data to # a BoundedStream than has been allocated. class FileOverflow < RuntimeError; end # The maximum number of bytes that may be written to this data # stream. attr_reader :limit # The current total number of bytes written to this data stream. attr_reader :written def initialize(io, limit) @io = io @limit = limit @written = 0 end def write(data) raise FileOverflow if (data.size + @written) > @limit @io.write(data) @written += data.size data.size end end # With no associated block, +Writer::open+ is a synonym for # +Writer::new+. If the optional code block is given, it will be # passed the new _writer_ as an argument and the Writer object will # automatically be closed when the block terminates. In this instance, # +Writer::open+ returns the value of the block. def self.open(anIO) writer = Writer.new(anIO) return writer unless block_given? begin res = yield writer ensure writer.close end res end # Creates and returns a new Writer object. def initialize(anIO) @io = anIO @closed = false end # Adds a file to the archive as +name+. +opts+ must contain the # following values: # # :mode:: The Unix file permissions mode value. # :size:: The size, in bytes. # # +opts+ may contain the following values: # # :uid: The Unix file owner user ID number. # :gid: The Unix file owner group ID number. # :mtime:: The *integer* modification time value. # # It will not be possible to add more than opts[:size] bytes # to the file. def add_file_simple(name, opts = {}) # :yields BoundedStream: raise Archive::Tar::Minitar::BlockRequired unless block_given? raise Archive::Tar::ClosedStream if @closed name, prefix = split_name(name) header = { :name => name, :mode => opts[:mode], :mtime => opts[:mtime], :size => opts[:size], :gid => opts[:gid], :uid => opts[:uid], :prefix => prefix } header = Archive::Tar::PosixHeader.new(header).to_s @io.write(header) os = BoundedStream.new(@io, opts[:size]) yield os # FIXME: what if an exception is raised in the block? min_padding = opts[:size] - os.written @io.write("\0" * min_padding) remainder = (512 - (opts[:size] % 512)) % 512 @io.write("\0" * remainder) end # Adds a file to the archive as +name+. +opts+ must contain the # following value: # # :mode:: The Unix file permissions mode value. # # +opts+ may contain the following values: # # :uid: The Unix file owner user ID number. # :gid: The Unix file owner group ID number. # :mtime:: The *integer* modification time value. # # The file's size will be determined from the amount of data written # to the stream. # # For #add_file to be used, the Archive::Tar::Minitar::Writer must be # wrapping a stream object that is seekable (e.g., it responds to # #pos=). Otherwise, #add_file_simple must be used. # # +opts+ may be modified during the writing to the stream. def add_file(name, opts = {}) # :yields RestrictedStream, +opts+: raise Archive::Tar::Minitar::BlockRequired unless block_given? raise Archive::Tar::Minitar::ClosedStream if @closed raise Archive::Tar::Minitar::NonSeekableStream unless @io.respond_to?(:pos=) name, prefix = split_name(name) init_pos = @io.pos @io.write("\0" * 512) # placeholder for the header yield RestrictedStream.new(@io), opts # FIXME: what if an exception is raised in the block? size = @io.pos - (init_pos + 512) remainder = (512 - (size % 512)) % 512 @io.write("\0" * remainder) final_pos = @io.pos @io.pos = init_pos header = { :name => name, :mode => opts[:mode], :mtime => opts[:mtime], :size => size, :gid => opts[:gid], :uid => opts[:uid], :prefix => prefix } header = Archive::Tar::PosixHeader.new(header).to_s @io.write(header) @io.pos = final_pos end # Creates a directory in the tar. def mkdir(name, opts = {}) raise ClosedStream if @closed name, prefix = split_name(name) header = { :name => name, :mode => opts[:mode], :typeflag => "5", :size => 0, :gid => opts[:gid], :uid => opts[:uid], :mtime => opts[:mtime], :prefix => prefix } header = Archive::Tar::PosixHeader.new(header).to_s @io.write(header) nil end # Passes the #flush method to the wrapped stream, used for buffered # streams. def flush raise ClosedStream if @closed @io.flush if @io.respond_to?(:flush) end # Closes the Writer. def close return if @closed @io.write("\0" * 1024) @closed = true end private def split_name(name) raise FileNameTooLong if name.size > 256 if name.size <= 100 prefix = "" else parts = name.split(/\//) newname = parts.pop nxt = "" loop do nxt = parts.pop break if newname.size + 1 + nxt.size > 100 newname = "#{nxt}/#{newname}" end prefix = (parts + [nxt]).join("/") name = newname raise FileNameTooLong if name.size > 100 || prefix.size > 155 end return name, prefix end end # The class that reads a tar format archive from a data stream. The data # stream may be sequential or random access, but certain features only work # with random access data streams. class Reader # This marks the EntryStream closed for reading without closing the # actual data stream. module InvalidEntryStream def read(len = nil); raise ClosedStream; end def getc; raise ClosedStream; end def rewind; raise ClosedStream; end end # EntryStreams are pseudo-streams on top of the main data stream. class EntryStream Archive::Tar::PosixHeader::FIELDS.each do |field| attr_reader field.intern end def initialize(header, anIO) @io = anIO @name = header.name @mode = header.mode @uid = header.uid @gid = header.gid @size = header.size @mtime = header.mtime @checksum = header.checksum @typeflag = header.typeflag @linkname = header.linkname @magic = header.magic @version = header.version @uname = header.uname @gname = header.gname @devmajor = header.devmajor @devminor = header.devminor @prefix = header.prefix @read = 0 @orig_pos = @io.pos end # Reads +len+ bytes (or all remaining data) from the entry. Returns # +nil+ if there is no more data to read. def read(len = nil) return nil if @read >= @size len ||= @size - @read max_read = [len, @size - @read].min ret = @io.read(max_read) @read += ret.size ret end # Reads one byte from the entry. Returns +nil+ if there is no more data # to read. def getc return nil if @read >= @size ret = @io.getc @read += 1 if ret ret end # Returns +true+ if the entry represents a directory. def directory? @typeflag == "5" end alias_method :directory, :directory? # Returns +true+ if the entry represents a plain file. def file? @typeflag == "0" end alias_method :file, :file? # Returns +true+ if the current read pointer is at the end of the # EntryStream data. def eof? @read >= @size end # Returns the current read pointer in the EntryStream. def pos @read end # Sets the current read pointer to the beginning of the EntryStream. def rewind raise NonSeekableStream unless @io.respond_to?(:pos=) @io.pos = @orig_pos @read = 0 end def bytes_read @read end # Returns the full and proper name of the entry. def full_name if @prefix != "" File.join(@prefix, @name) else @name end end # Closes the entry. def close invalidate end private def invalidate extend InvalidEntryStream end end # With no associated block, +Reader::open+ is a synonym for # +Reader::new+. If the optional code block is given, it will be passed # the new _writer_ as an argument and the Reader object will # automatically be closed when the block terminates. In this instance, # +Reader::open+ returns the value of the block. def self.open(anIO) reader = Reader.new(anIO) return reader unless block_given? begin res = yield reader ensure reader.close end res end # Creates and returns a new Reader object. def initialize(anIO) @io = anIO @init_pos = anIO.pos end # Iterates through each entry in the data stream. def each(&block) each_entry(&block) end # Resets the read pointer to the beginning of data stream. Do not call # this during a #each or #each_entry iteration. This only works with # random access data streams that respond to #rewind and #pos. def rewind if @init_pos == 0 raise NonSeekableStream unless @io.respond_to?(:rewind) @io.rewind else raise NonSeekableStream unless @io.respond_to?(:pos=) @io.pos = @init_pos end end # Iterates through each entry in the data stream. def each_entry loop do return if @io.eof? header = Archive::Tar::PosixHeader.new_from_stream(@io) return if header.empty? entry = EntryStream.new(header, @io) size = entry.size yield entry skip = (512 - (size % 512)) % 512 if @io.respond_to?(:seek) # avoid reading... @io.seek(size - entry.bytes_read, IO::SEEK_CUR) else pending = size - entry.bytes_read while pending > 0 bread = @io.read([pending, 4096].min).size raise UnexpectedEOF if @io.eof? pending -= bread end end @io.read(skip) # discard trailing zeros # make sure nobody can use #read, #getc or #rewind anymore entry.close end end def close end end # Wraps a Archive::Tar::Minitar::Reader with convenience methods and # wrapped stream management; Input only works with random access data # streams. See Input::new for details. class Input include Enumerable # With no associated block, +Input::open+ is a synonym for # +Input::new+. If the optional code block is given, it will be passed # the new _writer_ as an argument and the Input object will # automatically be closed when the block terminates. In this instance, # +Input::open+ returns the value of the block. def self.open(input) stream = Input.new(input) return stream unless block_given? begin res = yield stream ensure stream.close end res end # Creates a new Input object. If +input+ is a stream object that responds # to #read), then it will simply be wrapped. Otherwise, one will be # created and opened using Kernel#open. When Input#close is called, the # stream object wrapped will be closed. def initialize(input) if input.respond_to?(:read) @io = input else @io = open(input, "rb") end @tarreader = Archive::Tar::Minitar::Reader.new(@io) end # Iterates through each entry and rewinds to the beginning of the stream # when finished. def each(&block) @tarreader.each { |entry| yield entry } ensure @tarreader.rewind end # Extracts the current +entry+ to +destdir+. If a block is provided, it # yields an +action+ Symbol, the full name of the file being extracted # (+name+), and a Hash of statistical information (+stats+). # # The +action+ will be one of: # :dir:: The +entry+ is a directory. # :file_start:: The +entry+ is a file; the extract of the # file is just beginning. # :file_progress:: Yielded every 4096 bytes during the extract # of the +entry+. # :file_done:: Yielded when the +entry+ is completed. # # The +stats+ hash contains the following keys: # :current:: The current total number of bytes read in the # +entry+. # :currinc:: The current number of bytes read in this read # cycle. # :entry:: The entry being extracted; this is a # Reader::EntryStream, with all methods thereof. def extract_entry(destdir, entry) # :yields action, name, stats: stats = { :current => 0, :currinc => 0, :entry => entry } if entry.directory? dest = File.join(destdir, entry.full_name) yield :dir, entry.full_name, stats if block_given? if Archive::Tar::Minitar.dir?(dest) begin FileUtils.chmod(entry.mode, dest) rescue Exception nil end else FileUtils.mkdir_p(dest, :mode => entry.mode) FileUtils.chmod(entry.mode, dest) end fsync_dir(dest) fsync_dir(File.join(dest, "..")) return else # it's a file destdir = File.join(destdir, File.dirname(entry.full_name)) FileUtils.mkdir_p(destdir, :mode => 0755) destfile = File.join(destdir, File.basename(entry.full_name)) FileUtils.chmod(0600, destfile) rescue nil # Errno::ENOENT yield :file_start, entry.full_name, stats if block_given? File.open(destfile, "wb", entry.mode) do |os| loop do data = entry.read(4096) break unless data stats[:currinc] = os.write(data) stats[:current] += stats[:currinc] yield :file_progress, entry.full_name, stats if block_given? end os.fsync end FileUtils.chmod(entry.mode, destfile) fsync_dir(File.dirname(destfile)) fsync_dir(File.join(File.dirname(destfile), "..")) yield :file_done, entry.full_name, stats if block_given? end end # Returns the Reader object for direct access. def tar @tarreader end # Closes the Reader object and the wrapped data stream. def close @io.close @tarreader.close end private def fsync_dir(dirname) # make sure this hits the disc dir = open(dirname, 'rb') dir.fsync rescue # ignore IOError if it's an unpatched (old) Ruby nil ensure dir.close if dir rescue nil end end # Wraps a Archive::Tar::Minitar::Writer with convenience methods and # wrapped stream management; Output only works with random access data # streams. See Output::new for details. class Output # With no associated block, +Output::open+ is a synonym for # +Output::new+. If the optional code block is given, it will be passed # the new _writer_ as an argument and the Output object will # automatically be closed when the block terminates. In this instance, # +Output::open+ returns the value of the block. def self.open(output) stream = Output.new(output) return stream unless block_given? begin res = yield stream ensure stream.close end res end # Creates a new Output object. If +output+ is a stream object that # responds to #read), then it will simply be wrapped. Otherwise, one will # be created and opened using Kernel#open. When Output#close is called, # the stream object wrapped will be closed. def initialize(output) if output.respond_to?(:write) @io = output else @io = ::File.open(output, "wb") end @tarwriter = Archive::Tar::Minitar::Writer.new(@io) end # Returns the Writer object for direct access. def tar @tarwriter end # Closes the Writer object and the wrapped data stream. def close @tarwriter.close @io.close end end class << self # Tests if +path+ refers to a directory. Fixes an apparently # corrupted stat() call on Windows. def dir?(path) File.directory?((path[-1] == ?/) ? path : "#{path}/") end # A convenience method for wrapping Archive::Tar::Minitar::Input.open # (mode +r+) and Archive::Tar::Minitar::Output.open (mode +w+). No other # modes are currently supported. def open(dest, mode = "r", &block) case mode when "r" Input.open(dest, &block) when "w" Output.open(dest, &block) else raise "Unknown open mode for Archive::Tar::Minitar.open." end end # A convenience method to packs the file provided. +entry+ may either be # a filename (in which case various values for the file (see below) will # be obtained from File#stat(entry) or a Hash with the fields: # # :name:: The filename to be packed into the tarchive. # *REQUIRED*. # :mode:: The mode to be applied. # :uid:: The user owner of the file. (Ignored on Windows.) # :gid:: The group owner of the file. (Ignored on Windows.) # :mtime:: The modification Time of the file. # # During packing, if a block is provided, #pack_file yields an +action+ # Symol, the full name of the file being packed, and a Hash of # statistical information, just as with # Archive::Tar::Minitar::Input#extract_entry. # # The +action+ will be one of: # :dir:: The +entry+ is a directory. # :file_start:: The +entry+ is a file; the extract of the # file is just beginning. # :file_progress:: Yielded every 4096 bytes during the extract # of the +entry+. # :file_done:: Yielded when the +entry+ is completed. # # The +stats+ hash contains the following keys: # :current:: The current total number of bytes read in the # +entry+. # :currinc:: The current number of bytes read in this read # cycle. # :name:: The filename to be packed into the tarchive. # *REQUIRED*. # :mode:: The mode to be applied. # :uid:: The user owner of the file. (+nil+ on Windows.) # :gid:: The group owner of the file. (+nil+ on Windows.) # :mtime:: The modification Time of the file. def pack_file(entry, outputter) #:yields action, name, stats: outputter = outputter.tar if outputter.kind_of?(Archive::Tar::Minitar::Output) stats = {} if entry.kind_of?(Hash) name = entry[:name] entry.each { |kk, vv| stats[kk] = vv unless vv.nil? } else name = entry end name = name.sub(%r{\./}, '') stat = File.stat(name) stats[:mode] ||= stat.mode stats[:mtime] ||= stat.mtime stats[:size] = stat.size if RUBY_PLATFORM =~ /win32/ stats[:uid] = nil stats[:gid] = nil else stats[:uid] ||= stat.uid stats[:gid] ||= stat.gid end case when File.file?(name) outputter.add_file_simple(name, stats) do |os| stats[:current] = 0 yield :file_start, name, stats if block_given? File.open(name, "rb") do |ff| until ff.eof? stats[:currinc] = os.write(ff.read(4096)) stats[:current] += stats[:currinc] yield :file_progress, name, stats if block_given? end end yield :file_done, name, stats if block_given? end when dir?(name) yield :dir, name, stats if block_given? outputter.mkdir(name, stats) else raise "Don't yet know how to pack this type of file." end end # A convenience method to pack files specified by +src+ into +dest+. If # +src+ is an Array, then each file detailed therein will be packed into # the resulting Archive::Tar::Minitar::Output stream; if +recurse_dirs+ # is true, then directories will be recursed. # # If +src+ is an Array, it will be treated as the argument to Find.find; # all files matching will be packed. def pack(src, dest, recurse_dirs = true, &block) Output.open(dest) do |outp| if src.kind_of?(Array) src.each do |entry| pack_file(entry, outp, &block) if dir?(entry) and recurse_dirs Dir["#{entry}/**/**"].each do |ee| pack_file(ee, outp, &block) end end end else Find.find(src) do |entry| pack_file(entry, outp, &block) end end end end # A convenience method to unpack files from +src+ into the directory # specified by +dest+. Only those files named explicitly in +files+ # will be extracted. def unpack(src, dest, files = [], &block) Input.open(src) do |inp| if File.exist?(dest) and (not dir?(dest)) raise "Can't unpack to a non-directory." elsif not File.exist?(dest) FileUtils.mkdir_p(dest) end inp.each do |entry| if files.empty? or files.include?(entry.full_name) inp.extract_entry(dest, entry, &block) end end end end end end