#!/usr/bin/env ruby #-- # Archive::Tar::Baby 0.5.1 # Copyright © 2004 Mauricio Julio Fernández Pradier and Austin Ziegler # This is free software with ABSOLUTELY NO WARRANTY. # # 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. # # This file contains an adaptation of Ruby/ProgressBar by Satoru # Takabayashi , copyright © 2001 - 2004. # # It is licensed under the GNU General Public Licence or Ruby's licence. # # $Id: command.rb,v 1.4 2004/09/22 17:47:43 austin Exp $ #++ require 'zlib' # TODO: add # TODO: delete ??? require 'optparse' require 'ostruct' require 'fileutils' module Archive::Tar::Minitar::Command class ProgressBar VERSION = "0.8" attr_accessor :total attr_accessor :title def initialize (title, total, out = STDERR) @title = title @total = total @out = out @bar_width = 80 @bar_mark = "o" @current = 0 @previous = 0 @is_finished = false @start_time = Time.now @previous_time = @start_time @title_width = 14 @format = "%-#{@title_width}s %3d%% %s %s" @format_arguments = [:title, :percentage, :bar, :stat] show end private def convert_bytes (bytes) if bytes < 1024 sprintf("%6dB", bytes) elsif bytes < 1024 * 1000 # 1000kb sprintf("%5.1fKB", bytes.to_f / 1024) elsif bytes < 1024 * 1024 * 1000 # 1000mb sprintf("%5.1fMB", bytes.to_f / 1024 / 1024) else sprintf("%5.1fGB", bytes.to_f / 1024 / 1024 / 1024) end end def transfer_rate bytes_per_second = @current.to_f / (Time.now - @start_time) sprintf("%s/s", convert_bytes(bytes_per_second)) end def bytes convert_bytes(@current) end def format_time (t) t = t.to_i sec = t % 60 min = (t / 60) % 60 hour = t / 3600 sprintf("%02d:%02d:%02d", hour, min, sec); end # ETA stands for Estimated Time of Arrival. def eta if @current == 0 "ETA: --:--:--" else elapsed = Time.now - @start_time eta = elapsed * @total / @current - elapsed; sprintf("ETA: %s", format_time(eta)) end end def elapsed elapsed = Time.now - @start_time sprintf("Time: %s", format_time(elapsed)) end def stat if @is_finished then elapsed else eta end end def stat_for_file_transfer if @is_finished then sprintf("%s %s %s", bytes, transfer_rate, elapsed) else sprintf("%s %s %s", bytes, transfer_rate, eta) end end def eol if @is_finished then "\n" else "\r" end end def bar len = percentage * @bar_width / 100 sprintf("|%s%s|", @bar_mark * len, " " * (@bar_width - len)) end def percentage(value = nil) if @total.zero? 100 else (value || @current) * 100 / @total end end def title @title[0,(@title_width - 1)] + ":" end def get_width # FIXME: I don't know how portable it is. default_width = 80 # begin # tiocgwinsz = 0x5413 # data = [0, 0, 0, 0].pack("SSSS") # if @out.ioctl(tiocgwinsz, data) >= 0 then # rows, cols, xpixels, ypixels = data.unpack("SSSS") # if cols >= 0 then cols else default_width end # else # default_width # end # rescue Exception # default_width # end end def show arguments = @format_arguments.map {|method| send(method) } line = sprintf(@format, *arguments) width = get_width if line.length == width - 1 @out.print(line + eol) elsif line.length >= width @bar_width = [@bar_width - (line.length - width + 1), 0].max if @bar_width == 0 then @out.print(line + eol) else show end else # line.length < width - 1 @bar_width += width - line.length + 1 show end @previous_time = Time.now end def show_progress if @total.zero? cur_percentage = 100 prev_percentage = 0 else cur_percentage = (@current * 100 / @total).to_i prev_percentage = (@previous * 100 / @total).to_i end if cur_percentage > prev_percentage || Time.now - @previous_time >= 1 || @is_finished show end end public def file_transfer_mode @format_arguments = [:title, :percentage, :bar, :stat_for_file_transfer] end def format= (format) @format = format end def format_arguments= (arguments) @format_arguments = arguments end def finish @current = @total @is_finished = true show_progress end def halt @is_finished = true show_progress end def set (count) if count < 0 || count > @total raise "invalid count: #{count} (total: #{@total})" end @current = count show_progress @previous = @current end def inc (step = 1) @current += step @current = @total if @current > @total show_progress @previous = @current end def inspect "(ProgressBar: #{@current}/#{@total})" end end class CommandPattern class AbstractCommandError < Exception; end class UnknownCommandError < RuntimeError; end class CommandAlreadyExists < RuntimeError; end class << self def add(command) command = command.new if command.kind_of?(Class) @commands ||= {} if @commands.has_key?(command.name) raise CommandAlreadyExists else @commands[command.name] = command end if command.respond_to?(:altname) unless @commands.has_key?(command.altname) @commands[command.altname] = command end end end def <<(command) add(command) end attr_accessor :default def default=(command) #:nodoc: if command.kind_of?(CommandPattern) @default = command elsif command.kind_of?(Class) @default = command.new elsif @commands.has_key?(command) @default = @commands[command] else raise UnknownCommandError end end def command?(command) @commands.has_key?(command) end def command(command) if command?(command) @commands[command] else @default end end def [](cmd) self.command(cmd) end def default_ioe(ioe = {}) ioe[:input] ||= $stdin ioe[:output] ||= $stdout ioe[:error] ||= $stderr ioe end end def [](args, opts = {}, ioe = {}) call(args, opts, ioe) end def name raise AbstractCommandError end def call(args, opts = {}, ioe = {}) raise AbstractCommandError end def help raise AbstractCommandError end end class CommandHelp < CommandPattern def name "help" end def call(args, opts = {}, ioe = {}) ioe = CommandPattern.default_ioe(ioe) help_on = args.shift if CommandPattern.command?(help_on) ioe[:output] << CommandPattern[help_on].help elsif help_on == "commands" ioe[:output] << <<-EOH The commands known to minitar are: minitar create Creates a new tarfile. minitar extract Extracts files from a tarfile. minitar list Lists files in the tarfile. All commands accept the options --verbose and --progress, which are mutually exclusive. In "minitar list", --progress means the same as --verbose. --verbose, -V Performs the requested command verbosely. --progress, -P Shows a progress bar, if appropriate, for the action being performed. EOH else ioe[:output] << "Unknown command: #{help_on}\n" unless help_on.nil? or help_on.empty? ioe[:output] << self.help end 0 end def help help = <<-EOH This is a basic help message containing pointers to more information on how to use this command-line tool. Try: minitar help commands list all 'minitar' commands minitar help show help on (e.g., 'minitar help create') EOH end # minitar add Adds a file to an existing tarfile. # minitar delete Deletes a file from an existing tarfile. end class CommandCreate < CommandPattern def name "create" end def altname "cr" end def call(args, opts = {}, ioe = {}) argv = [] while (arg = args.shift) case arg when '--compress', '-z' opts[:compress] = true else argv << arg end end if argv.size < 2 ioe[:output] << "Not enough arguments.\n\n" CommandPattern["help"][["create"]] return 255 end output = argv.shift if '-' == output opts[:name] = "STDOUT" output = ioe[:output] opts[:output] = ioe[:error] else opts[:name] = output output = File.open(output, "wb") opts[:output] = ioe[:output] end if opts[:name] =~ /\.tar\.gz$|\.tgz$/ or opts[:compress] output = Zlib::GzipWriter.new(output) end files = [] if argv.include?("--") # Read stdin for the list of files. files = "" files << ioe[:input].read while not ioe[:input].eof? files = files.split(/\r\n|\n|\r/) args.delete("--") end files << argv.to_a files.flatten! if opts[:verbose] watcher = lambda do |action, name, stats| opts[:output] << "#{name}\n" if action == :dir or action == :file_done end finisher = lambda { opts[:output] << "\n" } elsif opts[:progress] progress = ProgressBar.new(opts[:name], 1) watcher = lambda do |action, name, stats| case action when :file_start, :dir progress.title = File.basename(name) if action == :dir progress.total += 1 progress.inc else progress.total += stats[:size] end when :file_progress progress.inc(stats[:currinc]) end end finisher = lambda do progress.title = opts[:name] progress.finish end else watcher = nil finisher = lambda { } end Archive::Tar::Minitar.pack(files, output, &watcher) finisher.call 0 ensure output.close if output and not output.closed? end def help help = <<-EOH minitar create [OPTIONS] + Creates a new tarfile. If the tarfile is named .tar.gz or .tgz, then it will be compressed automatically. If the tarfile is "-", then it will be output to standard output (stdout) so that minitar may be piped. The files or directories that will be packed into the tarfile are specified after the name of the tarfile itself. Directories will be processed recursively. If the token "--" is found in the list of files to be packed, additional filenames will be read from standard input (stdin). If any file is not found, the packaging will be halted. create Options: --compress, -z Compresses the tarfile with gzip. EOH end end class CommandExtract < CommandPattern def name "extract" end def altname "ex" end def call(args, opts = {}, ioe = {}) argv = [] output = nil dest = "." files = [] while (arg = args.shift) case arg when '--uncompress', '-z' opts[:uncompress] = true when '--pipe' opts[:output] = ioe[:error] output = ioe[:output] when '--output', '-o' dest = args.shift else argv << arg end end if argv.size < 1 ioe[:output] << "Not enough arguments.\n\n" CommandPattern["help"][["extract"]] return 255 end input = argv.shift if '-' == input opts[:name] = "STDIN" input = ioe[:input] else opts[:name] = input input = File.open(input, "rb") end if opts[:name] =~ /\.tar\.gz$|\.tgz$/ or opts[:uncompress] input = Zlib::GzipReader.new(input) end files << argv.to_a files.flatten! if opts[:verbose] watcher = lambda do |action, name, stats| opts[:output] << "#{name}\n" if action == :dir or action == :file_done end finisher = lambda { opts[:output] << "\n" } elsif opts[:progress] progress = ProgressBar.new(opts[:name], 1) watcher = lambda do |action, name, stats| case action when :file_start, :dir progress.title = File.basename(name) if action == :dir progress.total += 1 progress.inc else progress.total += stats[:entry].size end when :file_progress progress.inc(stats[:currinc]) end end finisher = lambda do progress.title = opts[:name] progress.finish end else watcher = nil finisher = lambda { } end if output.nil? Archive::Tar::Minitar.unpack(input, dest, files, &watcher) finisher.call else Archive::Tar::Minitar::Input.open(input) do |inp| inp.each do |entry| stats = { :mode => entry.mode, :mtime => entry.mtime, :size => entry.size, :gid => entry.gid, :uid => entry.uid, :current => 0, :currinc => 0, :entry => entry } if files.empty? or files.include?(entry.full_name) if entry.directory? puts "Directory: #{entry.full_name}" watcher[:dir, dest, stats] unless watcher.nil? else puts "File: #{entry.full_name}" watcher[:file_start, destfile, stats] unless watcher.nil? loop do data = entry.read(4096) break unless data stats[:currinc] = output.write(data) stats[:current] += stats[:currinc] watcher[:file_progress, name, stats] unless watcher.nil? end watcher[:file_done, name, stats] unless watcher.nil? end end end end end 0 end def help help = <<-EOH minitar extract [OPTIONS] [+] Extracts files from an existing tarfile. If the tarfile is named .tar.gz or .tgz, then it will be uncompressed automatically. If the tarfile is "-", then it will be read from standard input (stdin) so that minitar may be piped. The files or directories that will be extracted from the tarfile are specified after the name of the tarfile itself. Directories will be processed recursively. Files must be specified in full. A file "foo/bar/baz.txt" cannot simply be specified by specifying "baz.txt". Any file not found will simply be skipped and an error will be reported. extract Options: --uncompress, -z Uncompresses the tarfile with gzip. --pipe Emits the extracted files to STDOUT for piping. --output, -o Extracts the files to the specified directory. EOH end end class CommandList < CommandPattern def name "list" end def altname "ls" end def modestr(mode) s = "---" s[0] = ?r if (mode & 4) == 4 s[1] = ?w if (mode & 2) == 2 s[2] = ?x if (mode & 1) == 1 s end def call(args, opts = {}, ioe = {}) argv = [] output = nil dest = "." files = [] opts[:field] = "name" while (arg = args.shift) case arg when '--sort', '-S' opts[:sort] = true opts[:field] = args.shift when '--reverse', '-R' opts[:reverse] = true opts[:sort] = true when '--uncompress', '-z' opts[:uncompress] = true when '-l' opts[:verbose] = true else argv << arg end end if argv.size < 1 ioe[:output] << "Not enough arguments.\n\n" CommandPattern["help"][["list"]] return 255 end input = argv.shift if '-' == input opts[:name] = "STDIN" input = ioe[:input] else opts[:name] = input input = File.open(input, "rb") end if opts[:name] =~ /\.tar\.gz$|\.tgz$/ or opts[:uncompress] input = Zlib::GzipReader.new(input) end files << argv.to_a files.flatten! if opts[:verbose] or opts[:progress] format = "%10s %4d %8s %8s %8d %12s %s" datefmt = "%b %d %Y" timefmt = "%b %d %H:%M" fields = %w(permissions inodes user group size date fullname) else format = "%s" fields = %w(fullname) end opts[:field] = opts[:field].intern opts[:field] = :full_name if opts[:field] == :name output = [] Archive::Tar::Minitar::Input.open(input) do |inp| today = Time.now oneyear = Time.mktime(today.year - 1, today.month, today.day) inp.each do |entry| value = format % fields.map do |ff| case ff when "permissions" s = entry.directory? ? "d" : "-" s << modestr(entry.mode / 0100) s << modestr(entry.mode / 0010) s << modestr(entry.mode) when "inodes" entry.size / 512 when "user" entry.uname || entry.uid || 0 when "group" entry.gname || entry.gid || 0 when "size" entry.size when "date" if Time.at(entry.mtime) > (oneyear) Time.at(entry.mtime).strftime(timefmt) else Time.at(entry.mtime).strftime(datefmt) end when "fullname" entry.full_name end end if opts[:sort] output << [entry.send(opts[:field]), value] else ioe[:output] << value << "\n" end end end if opts[:sort] output = output.sort { |a, b| a[0] <=> b[0] } if opts[:reverse] output.reverse_each { |oo| ioe[:output] << oo[1] << "\n" } else output.each { |oo| ioe[:output] << oo[1] << "\n" } end end 0 end def help help = <<-EOH minitar list [OPTIONS] [+] Lists files in an existing tarfile. If the tarfile is named .tar.gz or .tgz, then it will be uncompressed automatically. If the tarfile is "-", then it will be read from standard input (stdin) so that minitar may be piped. If --verbose or --progress is specified, then the file list will be similar to that produced by the Unix command "ls -l". list Options: --uncompress, -z Uncompresses the tarfile with gzip. --sort [], -S Sorts the list of files by the specified field. The sort defaults to the filename. --reverse, -R Reverses the sort. -l Lists the files in detail. Sort Fields: name, mtime, size EOH end end CommandPattern << CommandHelp CommandPattern << CommandCreate CommandPattern << CommandExtract CommandPattern << CommandList # CommandPattern << CommandAdd # CommandPattern << CommandDelete def self.run(argv, input = $stdin, output = $stdout, error = $stderr) ioe = { :input => input, :output => output, :error => error, } opts = { } if argv.include?("--version") output << <<-EOB minitar #{Archive::Tar::Minitar::VERSION} Copyright 2004 Mauricio Julio Fernández Pradier and Austin Ziegler This is free software with ABSOLUTELY NO WARRANTY. see http://rubyforge.org/projects/ruwiki for more information EOB end if argv.include?("--verbose") or argv.include?("-V") opts[:verbose] = true argv.delete("--verbose") argv.delete("-V") end if argv.include?("--progress") or argv.include?("-P") opts[:progress] = true opts[:verbose] = false argv.delete("--progress") argv.delete("-P") end command = CommandPattern[(argv.shift or "").downcase] command ||= CommandPattern["help"] return command[argv, opts, ioe] end end