lib/minitar.rb in minitar-0.12.1 vs lib/minitar.rb in minitar-1.0.0

- old
+ new

@@ -1,12 +1,300 @@ -# coding: utf-8 +require "fileutils" +require "rbconfig" -require "archive/tar/minitar" +# == Synopsis +# +# Using minitar is easy. The simplest case is: +# +# require 'zlib' +# require 'minitar' +# +# # Packs everything that matches Find.find('tests'). +# # test.tar will automatically be closed by Minitar.pack. +# Minitar.pack('tests', File.open('test.tar', 'wb')) +# +# # Unpacks 'test.tar' to 'x', creating 'x' if necessary. +# Minitar.unpack('test.tar', 'x') +# +# A gzipped tar can be written with: +# +# # test.tgz will be closed automatically. +# Minitar.pack('tests', Zlib::GzipWriter.new(File.open('test.tgz', 'wb')) +# +# # test.tgz will be closed automatically. +# Minitar.unpack(Zlib::GzipReader.new(File.open('test.tgz', 'rb')), '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 +class Minitar + VERSION = "1.0.0".freeze # :nodoc: -if defined?(::Minitar) && ::Minitar != Archive::Tar::Minitar - warn <<-EOS -::Minitar is already defined. -This will conflict with future versions of minitar. - EOS -else - ::Minitar = Archive::Tar::Minitar + # The base class for any minitar error. + Error = Class.new(::StandardError) + # Raised when a wrapped data stream class is not seekable. + NonSeekableStream = Class.new(Error) + # The exception raised when operations are performed on a stream that has + # previously been closed. + ClosedStream = Class.new(Error) + # The exception raised when a filename exceeds 256 bytes in length, the + # maximum supported by the standard Tar format. + FileNameTooLong = Class.new(Error) + # The exception raised when a data stream ends before the amount of data + # expected in the archive's PosixHeader. + UnexpectedEOF = Class.new(StandardError) + # The exception raised when a file contains a relative path in secure mode + # (the default for this version). + SecureRelativePathError = Class.new(Error) + # The exception raised when a file contains an invalid Posix header. + InvalidTarStream = Class.new(Error) end + +class << Minitar + # Tests if +path+ refers to a directory. Fixes an apparently + # corrupted <tt>stat()</tt> call on Windows. + def dir?(path) + File.directory?((path[-1] == "/") ? path : "#{path}/") + end + + # A convenience method for wrapping Minitar::Input.open + # (mode +r+) and Minitar::Output.open (mode +w+). No other + # modes are currently supported. + def open(dest, mode = "r", &) + case mode + when "r" + Input.open(dest, &) + when "w" + Output.open(dest, &block) + else + raise "Unknown open mode for Minitar.open." + end + end + + def const_missing(c) # :nodoc: + case c + when :BlockRequired + warn "This constant has been removed." + const_set(:BlockRequired, Class.new(StandardError)) + else + super + end + end + + def windows? # :nodoc: + RbConfig::CONFIG["host_os"] =~ /^(mswin|mingw|cygwin)/ + end + + # A convenience method to pack the provided +data+ as a file named +entry+. +entry+ may + # either be a name or a Hash with the fields described below. When only a name is + # provided, or only some Hash fields are provided, the default values will apply. + # + # <tt>:name</tt>:: The filename to be packed into the archive. Required. + # <tt>:mode</tt>:: The mode to be applied. Defaults to 0o644 for files and 0o755 for + # directories. + # <tt>:uid</tt>:: The user owner of the file. Default is +nil+. + # <tt>:gid</tt>:: The group owner of the file. Default is +nil+. + # <tt>:mtime</tt>:: The modification Time of the file. Default is +Time.now+. + # + # If +data+ is +nil+, a directory will be created. Use an empty String for a normal + # empty file. + def pack_as_file(entry, data, outputter) # :yields action, name, stats: + if outputter.is_a?(Minitar::Output) + outputter = outputter.tar + end + + stats = { + gid: nil, + uid: nil, + mtime: Time.now, + size: data&.size || 0, + mode: data ? 0o644 : 0o755 + } + + if entry.is_a?(Hash) + name = entry.delete(:name) + entry.each_pair { stats[_1] = _2 unless _2.nil? } + else + name = entry + end + + if data.nil? # Create a directory + yield :dir, name, stats if block_given? + outputter.mkdir(name, stats) + else + outputter.add_file_simple(name, stats) do |os| + stats[:current] = 0 + yield :file_start, name, stats if block_given? + + StringIO.open(data, "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 + end + end + + # A convenience method to pack the file provided. +entry+ may either be a filename (in + # which case various values for the file (see below) will be obtained from + # <tt>File#stat(entry)</tt> or a Hash with the fields: + # + # <tt>:name</tt>:: The filename to be packed into the archive. Required. + # <tt>:mode</tt>:: The mode to be applied. + # <tt>:uid</tt>:: The user owner of the file. (Ignored on Windows.) + # <tt>:gid</tt>:: The group owner of the file. (Ignored on Windows.) + # <tt>:mtime</tt>:: 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 Minitar::Input#extract_entry. + # + # The +action+ will be one of: + # <tt>:dir</tt>:: The +entry+ is a directory. + # <tt>:file_start</tt>:: The +entry+ is a file; the extract of the + # file is just beginning. + # <tt>:file_progress</tt>:: Yielded every 4096 bytes during the extract + # of the +entry+. + # <tt>:file_done</tt>:: Yielded when the +entry+ is completed. + # + # The +stats+ hash contains the following keys: + # <tt>:current</tt>:: The current total number of bytes read in the + # +entry+. + # <tt>:currinc</tt>:: The current number of bytes read in this read + # cycle. + # <tt>:name</tt>:: The filename to be packed into the tarchive. + # *REQUIRED*. + # <tt>:mode</tt>:: The mode to be applied. + # <tt>:uid</tt>:: The user owner of the file. (+nil+ on Windows.) + # <tt>:gid</tt>:: The group owner of the file. (+nil+ on Windows.) + # <tt>:mtime</tt>:: The modification Time of the file. + def pack_file(entry, outputter) # :yields action, name, stats: + if outputter.is_a?(Minitar::Output) + outputter = outputter.tar + end + + stats = {} + + if entry.is_a?(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 windows? + stats[:uid] = nil + stats[:gid] = nil + else + stats[:uid] ||= stat.uid + stats[:gid] ||= stat.gid + end + + if 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 + elsif 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 Minitar::Output stream; if +recurse_dirs+ is + # true, then directories will be recursed. + # + # If +src+ is not an Array, it will be treated as the result of Find.find; + # all files matching will be packed. + def pack(src, dest, recurse_dirs = true, &block) + require "find" + Minitar::Output.open(dest) do |outp| + if src.is_a?(Array) + src.each do |entry| + if dir?(entry) && recurse_dirs + Find.find(entry) do |ee| + pack_file(ee, outp, &block) + end + else + pack_file(entry, outp, &block) + 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 = [], options = {}, &block) + Minitar::Input.open(src) do |inp| + if File.exist?(dest) && !dir?(dest) + raise "Can't unpack to a non-directory." + end + + FileUtils.mkdir_p(dest) unless File.exist?(dest) + + inp.each do |entry| + if files.empty? || files.include?(entry.full_name) + inp.extract_entry(dest, entry, options, &block) + end + end + end + end + + # Check whether +io+ can seek without errors. + def seekable?(io, methods = nil) + # The IO class throws an exception at runtime if we try to change + # position on a non-regular file. + if io.respond_to?(:stat) + io.stat.file? + else + # Duck-type the rest of this. + methods ||= [:pos, :pos=, :seek, :rewind] + methods = [methods] unless methods.is_a?(Array) + methods.all? { |m| io.respond_to?(m) } + end + end +end + +require "minitar/posix_header" +require "minitar/input" +require "minitar/output"