minitar 0.5.3
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of minitar might be problematic. Click here for more details.
- data/ChangeLog +14 -0
- data/Install +6 -0
- data/README +66 -0
- data/Rakefile +113 -0
- data/bin/minitar +27 -0
- data/lib/archive/tar/minitar.rb +985 -0
- data/lib/archive/tar/minitar/command.rb +814 -0
- data/tests/tc_tar.rb +614 -0
- data/tests/testall.rb +10 -0
- metadata +69 -0
data/ChangeLog
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
Revision history for Ruby library Archive::Tar::Minitar. Unless explicitly
|
2
|
+
noted otherwise, all changes are produced by Austin Ziegler
|
3
|
+
<austin@rubyforge.org>.
|
4
|
+
|
5
|
+
== 0.5.2
|
6
|
+
* Fixed a Ruby 1.9 compatibility error.
|
7
|
+
|
8
|
+
== 0.5.1
|
9
|
+
* Fixed a variable name error.
|
10
|
+
|
11
|
+
== Archive::Tar::Minitar 0.5.0
|
12
|
+
* Initial release. Does files and directories. Command does create, extract,
|
13
|
+
* and list.
|
14
|
+
|
data/Install
ADDED
data/README
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
Archive::Tar::Minitar README
|
2
|
+
============================
|
3
|
+
Archive::Tar::Minitar is a pure-Ruby library and command-line utility that
|
4
|
+
provides the ability to deal with POSIX tar(1) archive files. The
|
5
|
+
implementation is based heavily on Mauricio Ferna'ndez's implementation in
|
6
|
+
rpa-base, but has been reorganised to promote reuse in other projects.
|
7
|
+
|
8
|
+
This release is version 0.5.2, offering a Ruby 1.9 compatibility bugfix over
|
9
|
+
version 0.5.1. The library can only handle files and directories at this
|
10
|
+
point. A future version will be expanded to handle symbolic links and hard
|
11
|
+
links in a portable manner. The command line utility, minitar, can only create
|
12
|
+
archives, extract from archives, and list archive contents.
|
13
|
+
|
14
|
+
Using this library is easy. The simplest case is:
|
15
|
+
|
16
|
+
require 'zlib'
|
17
|
+
require 'archive/tar/minitar'
|
18
|
+
include Archive::Tar
|
19
|
+
|
20
|
+
# Packs everything that matches Find.find('tests')
|
21
|
+
File.open('test.tar', 'wb') { |tar| Minitar.pack('tests', tar) }
|
22
|
+
# Unpacks 'test.tar' to 'x', creating 'x' if necessary.
|
23
|
+
Minitar.unpack('test.tar', 'x')
|
24
|
+
|
25
|
+
A gzipped tar can be written with:
|
26
|
+
|
27
|
+
tgz = Zlib::GzipWriter.new(File.open('test.tgz', 'wb'))
|
28
|
+
# Warning: tgz will be closed!
|
29
|
+
Minitar.pack('tests', tgz)
|
30
|
+
|
31
|
+
tgz = Zlib::GzipReader.new(File.open('test.tgz', 'rb'))
|
32
|
+
# Warning: tgz will be closed!
|
33
|
+
Minitar.unpack(tgz, 'x')
|
34
|
+
|
35
|
+
As the case above shows, one need not write to a file. However, it will
|
36
|
+
sometimes require that one dive a little deeper into the API, as in the case
|
37
|
+
of StringIO objects. Note that I'm not providing a block with Minitar::Output,
|
38
|
+
as Minitar::Output#close automatically closes both the Output object and the
|
39
|
+
wrapped data stream object.
|
40
|
+
|
41
|
+
begin
|
42
|
+
sgz = Zlib::GzipWriter.new(StringIO.new(""))
|
43
|
+
tar = Output.new(sgz)
|
44
|
+
Find.find('tests') do |entry|
|
45
|
+
Minitar.pack_file(entry, tar)
|
46
|
+
end
|
47
|
+
ensure
|
48
|
+
# Closes both tar and sgz.
|
49
|
+
tar.close
|
50
|
+
end
|
51
|
+
|
52
|
+
Copyright
|
53
|
+
=========
|
54
|
+
# Copyright 2004 Mauricio Julio Ferna'ndez Pradier and Austin Ziegler
|
55
|
+
#
|
56
|
+
# This program is based on and incorporates parts of RPA::Package from
|
57
|
+
# rpa-base (lib/rpa/package.rb and lib/rpa/util.rb) by Mauricio and has been
|
58
|
+
# adapted to be more generic by Austin.
|
59
|
+
#
|
60
|
+
# 'minitar' contains an adaptation of Ruby/ProgressBar by Satoru
|
61
|
+
# Takabayashi <satoru@namazu.org>, copyright 2001 - 2004.
|
62
|
+
#
|
63
|
+
# This program is free software. It may be redistributed and/or modified
|
64
|
+
# under the terms of the GPL version 2 (or later) or Ruby's licence.
|
65
|
+
#
|
66
|
+
# $Id$
|
data/Rakefile
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
#! /usr/bin/env rake
|
2
|
+
$LOAD_PATH.unshift('lib')
|
3
|
+
|
4
|
+
require 'rubygems'
|
5
|
+
require 'rake/gempackagetask'
|
6
|
+
require 'rake/contrib/rubyforgepublisher'
|
7
|
+
require 'archive/tar/minitar'
|
8
|
+
require 'zlib'
|
9
|
+
|
10
|
+
DISTDIR = "archive-tar-minitar-#{Archive::Tar::Minitar::VERSION}"
|
11
|
+
TARDIST = "../#{DISTDIR}.tar.gz"
|
12
|
+
|
13
|
+
DATE_RE = %r<(\d{4})[./-]?(\d{2})[./-]?(\d{2})(?:[\sT]?(\d{2})[:.]?(\d{2})[:.]?(\d{2})?)?>
|
14
|
+
|
15
|
+
if ENV['RELEASE_DATE']
|
16
|
+
year, month, day, hour, minute, second = DATE_RE.match(ENV['RELEASE_DATE']).captures
|
17
|
+
year ||= 0
|
18
|
+
month ||= 0
|
19
|
+
day ||= 0
|
20
|
+
hour ||= 0
|
21
|
+
minute ||= 0
|
22
|
+
second ||= 0
|
23
|
+
ReleaseDate = Time.mktime(year, month, day, hour, minute, second)
|
24
|
+
else
|
25
|
+
ReleaseDate = nil
|
26
|
+
end
|
27
|
+
|
28
|
+
task :test do |t|
|
29
|
+
require 'test/unit/testsuite'
|
30
|
+
require 'test/unit/ui/console/testrunner'
|
31
|
+
|
32
|
+
runner = Test::Unit::UI::Console::TestRunner
|
33
|
+
|
34
|
+
$LOAD_PATH.unshift('tests')
|
35
|
+
Dir['tests/tc_*.rb'].each do |testcase|
|
36
|
+
load testcase
|
37
|
+
end
|
38
|
+
|
39
|
+
suite = Test::Unit::TestSuite.new
|
40
|
+
|
41
|
+
ObjectSpace.each_object(Class) do |testcase|
|
42
|
+
suite << testcase.suite if testcase < Test::Unit::TestCase
|
43
|
+
end
|
44
|
+
|
45
|
+
runner.run(suite)
|
46
|
+
end
|
47
|
+
|
48
|
+
spec = eval(File.read("archive-tar-minitar.gemspec"))
|
49
|
+
desc "Build the RubyGem for Archive::Tar::Minitar."
|
50
|
+
task :gem => [ :test ]
|
51
|
+
Rake::GemPackageTask.new(spec) do |g|
|
52
|
+
g.need_tar = false
|
53
|
+
g.need_zip = false
|
54
|
+
g.package_dir = ".."
|
55
|
+
end
|
56
|
+
|
57
|
+
desc "Build an Archive::Tar::Minitar .tar.gz distribution."
|
58
|
+
task :tar => [ TARDIST ]
|
59
|
+
file TARDIST do |t|
|
60
|
+
current = File.basename(Dir.pwd)
|
61
|
+
Dir.chdir("..") do
|
62
|
+
begin
|
63
|
+
files = Dir["#{current}/**/*"].select { |dd| dd !~ %r{(?:/CVS/?|~$)} }
|
64
|
+
files.map! do |dd|
|
65
|
+
ddnew = dd.gsub(/^#{current}/, DISTDIR)
|
66
|
+
mtime = ReleaseDate || File.stat(dd).mtime
|
67
|
+
if File.directory?(dd)
|
68
|
+
{ :name => ddnew, :mode => 0755, :dir => true, :mtime => mtime }
|
69
|
+
else
|
70
|
+
if dd =~ %r{bin/}
|
71
|
+
mode = 0755
|
72
|
+
else
|
73
|
+
mode = 0644
|
74
|
+
end
|
75
|
+
data = File.read(dd)
|
76
|
+
{ :name => ddnew, :mode => mode, :data => data, :size => data.size,
|
77
|
+
:mtime => mtime }
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
ff = File.open(t.name.gsub(%r{^\.\./}o, ''), "wb")
|
82
|
+
gz = Zlib::GzipWriter.new(ff)
|
83
|
+
tw = Archive::Tar::Minitar::Writer.new(gz)
|
84
|
+
|
85
|
+
files.each do |entry|
|
86
|
+
if entry[:dir]
|
87
|
+
tw.mkdir(entry[:name], entry)
|
88
|
+
else
|
89
|
+
tw.add_file_simple(entry[:name], entry) { |os| os.write(entry[:data]) }
|
90
|
+
end
|
91
|
+
end
|
92
|
+
ensure
|
93
|
+
tw.close if tw
|
94
|
+
gz.close if gz
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
task TARDIST => [ :test ]
|
99
|
+
|
100
|
+
def sign(file)
|
101
|
+
sh %("C:\\Program Files\\Windows Privacy Tools\\GnuPG\\Gpg.exe" -ba #{file})
|
102
|
+
end
|
103
|
+
|
104
|
+
task :signtar => [ :tar ] do
|
105
|
+
sign TARDIST
|
106
|
+
end
|
107
|
+
task :signgem => [ :gem ] do
|
108
|
+
sign "../#{DISTDIR}.gem"
|
109
|
+
end
|
110
|
+
|
111
|
+
desc "Build everything."
|
112
|
+
task :default => [ :signtar, :signgem ] do
|
113
|
+
end
|
data/bin/minitar
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#--
|
3
|
+
# Archive::Tar::Minitar 0.5.2
|
4
|
+
# Copyright 2004 Mauricio Julio Ferna'ndez Pradier and Austin Ziegler
|
5
|
+
#
|
6
|
+
# This program is based on and incorporates parts of RPA::Package from
|
7
|
+
# rpa-base (lib/rpa/package.rb and lib/rpa/util.rb) by Mauricio and has been
|
8
|
+
# adapted to be more generic by Austin.
|
9
|
+
#
|
10
|
+
# It is licensed under the GNU General Public Licence or Ruby's licence.
|
11
|
+
#
|
12
|
+
# $Id$
|
13
|
+
#++
|
14
|
+
|
15
|
+
# 1) Try to load Archive::Tar::Minitar from the gem.
|
16
|
+
# 2) Try to load Archive::Tar::Minitar from $LOAD_PATH.
|
17
|
+
begin
|
18
|
+
require 'rubygems'
|
19
|
+
require_gem 'archive-tar-minitar', '= 0.5.2'
|
20
|
+
rescue LoadError
|
21
|
+
nil
|
22
|
+
end
|
23
|
+
|
24
|
+
require 'archive/tar/minitar'
|
25
|
+
require 'archive/tar/minitar/command'
|
26
|
+
|
27
|
+
exit Archive::Tar::Minitar::Command.run(ARGV)
|
@@ -0,0 +1,985 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#--
|
3
|
+
# Archive::Tar::Minitar 0.5.2
|
4
|
+
# Copyright 2004 Mauricio Julio Ferna'ndez Pradier and Austin Ziegler
|
5
|
+
#
|
6
|
+
# This program is based on and incorporates parts of RPA::Package from
|
7
|
+
# rpa-base (lib/rpa/package.rb and lib/rpa/util.rb) by Mauricio and has been
|
8
|
+
# adapted to be more generic by Austin.
|
9
|
+
#
|
10
|
+
# It is licensed under the GNU General Public Licence or Ruby's licence.
|
11
|
+
#
|
12
|
+
# $Id$
|
13
|
+
#++
|
14
|
+
|
15
|
+
module Archive; end
|
16
|
+
module Archive::Tar; end
|
17
|
+
|
18
|
+
# = Archive::Tar::PosixHeader
|
19
|
+
# Implements the POSIX tar header as a Ruby class. The structure of
|
20
|
+
# the POSIX tar header is:
|
21
|
+
#
|
22
|
+
# struct tarfile_entry_posix
|
23
|
+
# { // pack/unpack
|
24
|
+
# char name[100]; // ASCII (+ Z unless filled) a100/Z100
|
25
|
+
# char mode[8]; // 0 padded, octal, null a8 /A8
|
26
|
+
# char uid[8]; // ditto a8 /A8
|
27
|
+
# char gid[8]; // ditto a8 /A8
|
28
|
+
# char size[12]; // 0 padded, octal, null a12 /A12
|
29
|
+
# char mtime[12]; // 0 padded, octal, null a12 /A12
|
30
|
+
# char checksum[8]; // 0 padded, octal, null, space a8 /A8
|
31
|
+
# char typeflag[1]; // see below a /a
|
32
|
+
# char linkname[100]; // ASCII + (Z unless filled) a100/Z100
|
33
|
+
# char magic[6]; // "ustar\0" a6 /A6
|
34
|
+
# char version[2]; // "00" a2 /A2
|
35
|
+
# char uname[32]; // ASCIIZ a32 /Z32
|
36
|
+
# char gname[32]; // ASCIIZ a32 /Z32
|
37
|
+
# char devmajor[8]; // 0 padded, octal, null a8 /A8
|
38
|
+
# char devminor[8]; // 0 padded, octal, null a8 /A8
|
39
|
+
# char prefix[155]; // ASCII (+ Z unless filled) a155/Z155
|
40
|
+
# };
|
41
|
+
#
|
42
|
+
# The +typeflag+ may be one of the following known values:
|
43
|
+
#
|
44
|
+
# <tt>"0"</tt>:: Regular file. NULL should be treated as a synonym, for
|
45
|
+
# compatibility purposes.
|
46
|
+
# <tt>"1"</tt>:: Hard link.
|
47
|
+
# <tt>"2"</tt>:: Symbolic link.
|
48
|
+
# <tt>"3"</tt>:: Character device node.
|
49
|
+
# <tt>"4"</tt>:: Block device node.
|
50
|
+
# <tt>"5"</tt>:: Directory.
|
51
|
+
# <tt>"6"</tt>:: FIFO node.
|
52
|
+
# <tt>"7"</tt>:: Reserved.
|
53
|
+
#
|
54
|
+
# POSIX indicates that "A POSIX-compliant implementation must treat any
|
55
|
+
# unrecognized typeflag value as a regular file."
|
56
|
+
class Archive::Tar::PosixHeader
|
57
|
+
FIELDS = %w(name mode uid gid size mtime checksum typeflag linkname) +
|
58
|
+
%w(magic version uname gname devmajor devminor prefix)
|
59
|
+
|
60
|
+
FIELDS.each { |field| attr_reader field.intern }
|
61
|
+
|
62
|
+
HEADER_PACK_FORMAT = "a100a8a8a8a12a12a7aaa100a6a2a32a32a8a8a155"
|
63
|
+
HEADER_UNPACK_FORMAT = "Z100A8A8A8A12A12A8aZ100A6A2Z32Z32A8A8Z155"
|
64
|
+
|
65
|
+
# Creates a new PosixHeader from a data stream.
|
66
|
+
def self.new_from_stream(stream, long_name = nil)
|
67
|
+
data = stream.read(512)
|
68
|
+
fields = data.unpack(HEADER_UNPACK_FORMAT)
|
69
|
+
name = fields.shift
|
70
|
+
mode = fields.shift.oct
|
71
|
+
uid = fields.shift.oct
|
72
|
+
gid = fields.shift.oct
|
73
|
+
size = fields.shift.oct
|
74
|
+
mtime = fields.shift.oct
|
75
|
+
checksum = fields.shift.oct
|
76
|
+
typeflag = fields.shift
|
77
|
+
linkname = fields.shift
|
78
|
+
magic = fields.shift
|
79
|
+
version = fields.shift.oct
|
80
|
+
uname = fields.shift
|
81
|
+
gname = fields.shift
|
82
|
+
devmajor = fields.shift.oct
|
83
|
+
devminor = fields.shift.oct
|
84
|
+
prefix = fields.shift
|
85
|
+
|
86
|
+
empty = (data == "\0" * 512)
|
87
|
+
|
88
|
+
if typeflag == 'L' && name == '././@LongLink'
|
89
|
+
long_name = stream.read(512).rstrip
|
90
|
+
return new_from_stream(stream, long_name)
|
91
|
+
end
|
92
|
+
|
93
|
+
new(:name => long_name || name,
|
94
|
+
:mode => mode, :uid => uid, :gid => gid,
|
95
|
+
:size => size, :mtime => mtime, :checksum => checksum,
|
96
|
+
:typeflag => typeflag, :magic => magic, :version => version,
|
97
|
+
:uname => uname, :gname => gname, :devmajor => devmajor,
|
98
|
+
:devminor => devminor, :prefix => prefix, :empty => empty)
|
99
|
+
end
|
100
|
+
|
101
|
+
# Creates a new PosixHeader. A PosixHeader cannot be created unless the
|
102
|
+
# #name, #size, #prefix, and #mode are provided.
|
103
|
+
def initialize(vals)
|
104
|
+
unless vals[:name] && vals[:size] && vals[:prefix] && vals[:mode]
|
105
|
+
raise ArgumentError
|
106
|
+
end
|
107
|
+
|
108
|
+
vals[:mtime] ||= 0
|
109
|
+
vals[:checksum] ||= ""
|
110
|
+
vals[:typeflag] ||= "0"
|
111
|
+
vals[:magic] ||= "ustar"
|
112
|
+
vals[:version] ||= "00"
|
113
|
+
|
114
|
+
FIELDS.each do |field|
|
115
|
+
instance_variable_set("@#{field}", vals[field.intern])
|
116
|
+
end
|
117
|
+
@empty = vals[:empty]
|
118
|
+
end
|
119
|
+
|
120
|
+
def empty?
|
121
|
+
@empty
|
122
|
+
end
|
123
|
+
|
124
|
+
def to_s
|
125
|
+
update_checksum
|
126
|
+
header(@checksum)
|
127
|
+
end
|
128
|
+
|
129
|
+
# Update the checksum field.
|
130
|
+
def update_checksum
|
131
|
+
hh = header(" " * 8)
|
132
|
+
@checksum = oct(calculate_checksum(hh), 6)
|
133
|
+
end
|
134
|
+
|
135
|
+
private
|
136
|
+
def oct(num, len)
|
137
|
+
if num.nil?
|
138
|
+
"\0" * (len + 1)
|
139
|
+
else
|
140
|
+
"%0#{len}o" % num
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def calculate_checksum(hdr)
|
145
|
+
hdr.unpack("C*").inject { |aa, bb| aa + bb }
|
146
|
+
end
|
147
|
+
|
148
|
+
def header(chksum)
|
149
|
+
arr = [name, oct(mode, 7), oct(uid, 7), oct(gid, 7), oct(size, 11),
|
150
|
+
oct(mtime, 11), chksum, " ", typeflag, linkname, magic, version,
|
151
|
+
uname, gname, oct(devmajor, 7), oct(devminor, 7), prefix]
|
152
|
+
str = arr.pack(HEADER_PACK_FORMAT)
|
153
|
+
str + "\0" * ((512 - str.size) % 512)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
require 'fileutils'
|
158
|
+
require 'find'
|
159
|
+
|
160
|
+
# = Archive::Tar::Minitar 0.5.2
|
161
|
+
# Archive::Tar::Minitar is a pure-Ruby library and command-line
|
162
|
+
# utility that provides the ability to deal with POSIX tar(1) archive
|
163
|
+
# files. The implementation is based heavily on Mauricio Ferna'ndez's
|
164
|
+
# implementation in rpa-base, but has been reorganised to promote
|
165
|
+
# reuse in other projects.
|
166
|
+
#
|
167
|
+
# This tar class performs a subset of all tar (POSIX tape archive)
|
168
|
+
# operations. We can only deal with typeflags 0, 1, 2, and 5 (see
|
169
|
+
# Archive::Tar::PosixHeader). All other typeflags will be treated as
|
170
|
+
# normal files.
|
171
|
+
#
|
172
|
+
# NOTE::: support for typeflags 1 and 2 is not yet implemented in this
|
173
|
+
# version.
|
174
|
+
#
|
175
|
+
# This release is version 0.5.2. The library can only handle files and
|
176
|
+
# directories at this point. A future version will be expanded to
|
177
|
+
# handle symbolic links and hard links in a portable manner. The
|
178
|
+
# command line utility, minitar, can only create archives, extract
|
179
|
+
# from archives, and list archive contents.
|
180
|
+
#
|
181
|
+
# == Synopsis
|
182
|
+
# Using this library is easy. The simplest case is:
|
183
|
+
#
|
184
|
+
# require 'zlib'
|
185
|
+
# require 'archive/tar/minitar'
|
186
|
+
# include Archive::Tar
|
187
|
+
#
|
188
|
+
# # Packs everything that matches Find.find('tests')
|
189
|
+
# File.open('test.tar', 'wb') { |tar| Minitar.pack('tests', tar) }
|
190
|
+
# # Unpacks 'test.tar' to 'x', creating 'x' if necessary.
|
191
|
+
# Minitar.unpack('test.tar', 'x')
|
192
|
+
#
|
193
|
+
# A gzipped tar can be written with:
|
194
|
+
#
|
195
|
+
# tgz = Zlib::GzipWriter.new(File.open('test.tgz', 'wb'))
|
196
|
+
# # Warning: tgz will be closed!
|
197
|
+
# Minitar.pack('tests', tgz)
|
198
|
+
#
|
199
|
+
# tgz = Zlib::GzipReader.new(File.open('test.tgz', 'rb'))
|
200
|
+
# # Warning: tgz will be closed!
|
201
|
+
# Minitar.unpack(tgz, 'x')
|
202
|
+
#
|
203
|
+
# As the case above shows, one need not write to a file. However, it
|
204
|
+
# will sometimes require that one dive a little deeper into the API,
|
205
|
+
# as in the case of StringIO objects. Note that I'm not providing a
|
206
|
+
# block with Minitar::Output, as Minitar::Output#close automatically
|
207
|
+
# closes both the Output object and the wrapped data stream object.
|
208
|
+
#
|
209
|
+
# begin
|
210
|
+
# sgz = Zlib::GzipWriter.new(StringIO.new(""))
|
211
|
+
# tar = Output.new(sgz)
|
212
|
+
# Find.find('tests') do |entry|
|
213
|
+
# Minitar.pack_file(entry, tar)
|
214
|
+
# end
|
215
|
+
# ensure
|
216
|
+
# # Closes both tar and sgz.
|
217
|
+
# tar.close
|
218
|
+
# end
|
219
|
+
#
|
220
|
+
# == Copyright
|
221
|
+
# Copyright 2004 Mauricio Julio Ferna'ndez Pradier and Austin Ziegler
|
222
|
+
#
|
223
|
+
# This program is based on and incorporates parts of RPA::Package from
|
224
|
+
# rpa-base (lib/rpa/package.rb and lib/rpa/util.rb) by Mauricio and
|
225
|
+
# has been adapted to be more generic by Austin.
|
226
|
+
#
|
227
|
+
# 'minitar' contains an adaptation of Ruby/ProgressBar by Satoru
|
228
|
+
# Takabayashi <satoru@namazu.org>, copyright 2001 - 2004.
|
229
|
+
#
|
230
|
+
# This program is free software. It may be redistributed and/or
|
231
|
+
# modified under the terms of the GPL version 2 (or later) or Ruby's
|
232
|
+
# licence.
|
233
|
+
module Archive::Tar::Minitar
|
234
|
+
VERSION = "0.5.2"
|
235
|
+
|
236
|
+
# The exception raised when a wrapped data stream class is expected to
|
237
|
+
# respond to #rewind or #pos but does not.
|
238
|
+
class NonSeekableStream < StandardError; end
|
239
|
+
# The exception raised when a block is required for proper operation of
|
240
|
+
# the method.
|
241
|
+
class BlockRequired < ArgumentError; end
|
242
|
+
# The exception raised when operations are performed on a stream that has
|
243
|
+
# previously been closed.
|
244
|
+
class ClosedStream < StandardError; end
|
245
|
+
# The exception raised when a filename exceeds 256 bytes in length,
|
246
|
+
# the maximum supported by the standard Tar format.
|
247
|
+
class FileNameTooLong < StandardError; end
|
248
|
+
# The exception raised when a data stream ends before the amount of data
|
249
|
+
# expected in the archive's PosixHeader.
|
250
|
+
class UnexpectedEOF < StandardError; end
|
251
|
+
|
252
|
+
# The class that writes a tar format archive to a data stream.
|
253
|
+
class Writer
|
254
|
+
# A stream wrapper that can only be written to. Any attempt to read
|
255
|
+
# from this restricted stream will result in a NameError being thrown.
|
256
|
+
class RestrictedStream
|
257
|
+
def initialize(anIO)
|
258
|
+
@io = anIO
|
259
|
+
end
|
260
|
+
|
261
|
+
def write(data)
|
262
|
+
@io.write(data)
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
# A RestrictedStream that also has a size limit.
|
267
|
+
class BoundedStream < Archive::Tar::Minitar::Writer::RestrictedStream
|
268
|
+
# The exception raised when the user attempts to write more data to
|
269
|
+
# a BoundedStream than has been allocated.
|
270
|
+
class FileOverflow < RuntimeError; end
|
271
|
+
|
272
|
+
# The maximum number of bytes that may be written to this data
|
273
|
+
# stream.
|
274
|
+
attr_reader :limit
|
275
|
+
# The current total number of bytes written to this data stream.
|
276
|
+
attr_reader :written
|
277
|
+
|
278
|
+
def initialize(io, limit)
|
279
|
+
@io = io
|
280
|
+
@limit = limit
|
281
|
+
@written = 0
|
282
|
+
end
|
283
|
+
|
284
|
+
def write(data)
|
285
|
+
raise FileOverflow if (data.size + @written) > @limit
|
286
|
+
@io.write(data)
|
287
|
+
@written += data.size
|
288
|
+
data.size
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
# With no associated block, +Writer::open+ is a synonym for
|
293
|
+
# +Writer::new+. If the optional code block is given, it will be
|
294
|
+
# passed the new _writer_ as an argument and the Writer object will
|
295
|
+
# automatically be closed when the block terminates. In this instance,
|
296
|
+
# +Writer::open+ returns the value of the block.
|
297
|
+
def self.open(anIO)
|
298
|
+
writer = Writer.new(anIO)
|
299
|
+
|
300
|
+
return writer unless block_given?
|
301
|
+
|
302
|
+
begin
|
303
|
+
res = yield writer
|
304
|
+
ensure
|
305
|
+
writer.close
|
306
|
+
end
|
307
|
+
|
308
|
+
res
|
309
|
+
end
|
310
|
+
|
311
|
+
# Creates and returns a new Writer object.
|
312
|
+
def initialize(anIO)
|
313
|
+
@io = anIO
|
314
|
+
@closed = false
|
315
|
+
end
|
316
|
+
|
317
|
+
# Adds a file to the archive as +name+. +opts+ must contain the
|
318
|
+
# following values:
|
319
|
+
#
|
320
|
+
# <tt>:mode</tt>:: The Unix file permissions mode value.
|
321
|
+
# <tt>:size</tt>:: The size, in bytes.
|
322
|
+
#
|
323
|
+
# +opts+ may contain the following values:
|
324
|
+
#
|
325
|
+
# <tt>:uid</tt>: The Unix file owner user ID number.
|
326
|
+
# <tt>:gid</tt>: The Unix file owner group ID number.
|
327
|
+
# <tt>:mtime</tt>:: The *integer* modification time value.
|
328
|
+
#
|
329
|
+
# It will not be possible to add more than <tt>opts[:size]</tt> bytes
|
330
|
+
# to the file.
|
331
|
+
def add_file_simple(name, opts = {}) # :yields BoundedStream:
|
332
|
+
raise Archive::Tar::Minitar::BlockRequired unless block_given?
|
333
|
+
raise Archive::Tar::ClosedStream if @closed
|
334
|
+
|
335
|
+
name, prefix = split_name(name)
|
336
|
+
|
337
|
+
header = { :name => name, :mode => opts[:mode], :mtime => opts[:mtime],
|
338
|
+
:size => opts[:size], :gid => opts[:gid], :uid => opts[:uid],
|
339
|
+
:prefix => prefix }
|
340
|
+
header = Archive::Tar::PosixHeader.new(header).to_s
|
341
|
+
@io.write(header)
|
342
|
+
|
343
|
+
os = BoundedStream.new(@io, opts[:size])
|
344
|
+
yield os
|
345
|
+
# FIXME: what if an exception is raised in the block?
|
346
|
+
|
347
|
+
min_padding = opts[:size] - os.written
|
348
|
+
@io.write("\0" * min_padding)
|
349
|
+
remainder = (512 - (opts[:size] % 512)) % 512
|
350
|
+
@io.write("\0" * remainder)
|
351
|
+
end
|
352
|
+
|
353
|
+
# Adds a file to the archive as +name+. +opts+ must contain the
|
354
|
+
# following value:
|
355
|
+
#
|
356
|
+
# <tt>:mode</tt>:: The Unix file permissions mode value.
|
357
|
+
#
|
358
|
+
# +opts+ may contain the following values:
|
359
|
+
#
|
360
|
+
# <tt>:uid</tt>: The Unix file owner user ID number.
|
361
|
+
# <tt>:gid</tt>: The Unix file owner group ID number.
|
362
|
+
# <tt>:mtime</tt>:: The *integer* modification time value.
|
363
|
+
#
|
364
|
+
# The file's size will be determined from the amount of data written
|
365
|
+
# to the stream.
|
366
|
+
#
|
367
|
+
# For #add_file to be used, the Archive::Tar::Minitar::Writer must be
|
368
|
+
# wrapping a stream object that is seekable (e.g., it responds to
|
369
|
+
# #pos=). Otherwise, #add_file_simple must be used.
|
370
|
+
#
|
371
|
+
# +opts+ may be modified during the writing to the stream.
|
372
|
+
def add_file(name, opts = {}) # :yields RestrictedStream, +opts+:
|
373
|
+
raise Archive::Tar::Minitar::BlockRequired unless block_given?
|
374
|
+
raise Archive::Tar::Minitar::ClosedStream if @closed
|
375
|
+
raise Archive::Tar::Minitar::NonSeekableStream unless @io.respond_to?(:pos=)
|
376
|
+
|
377
|
+
name, prefix = split_name(name)
|
378
|
+
init_pos = @io.pos
|
379
|
+
@io.write("\0" * 512) # placeholder for the header
|
380
|
+
|
381
|
+
yield RestrictedStream.new(@io), opts
|
382
|
+
# FIXME: what if an exception is raised in the block?
|
383
|
+
|
384
|
+
size = @io.pos - (init_pos + 512)
|
385
|
+
remainder = (512 - (size % 512)) % 512
|
386
|
+
@io.write("\0" * remainder)
|
387
|
+
|
388
|
+
final_pos = @io.pos
|
389
|
+
@io.pos = init_pos
|
390
|
+
|
391
|
+
header = { :name => name, :mode => opts[:mode], :mtime => opts[:mtime],
|
392
|
+
:size => size, :gid => opts[:gid], :uid => opts[:uid],
|
393
|
+
:prefix => prefix }
|
394
|
+
header = Archive::Tar::PosixHeader.new(header).to_s
|
395
|
+
@io.write(header)
|
396
|
+
@io.pos = final_pos
|
397
|
+
end
|
398
|
+
|
399
|
+
# Creates a directory in the tar.
|
400
|
+
def mkdir(name, opts = {})
|
401
|
+
raise ClosedStream if @closed
|
402
|
+
name, prefix = split_name(name)
|
403
|
+
header = { :name => name, :mode => opts[:mode], :typeflag => "5",
|
404
|
+
:size => 0, :gid => opts[:gid], :uid => opts[:uid],
|
405
|
+
:mtime => opts[:mtime], :prefix => prefix }
|
406
|
+
header = Archive::Tar::PosixHeader.new(header).to_s
|
407
|
+
@io.write(header)
|
408
|
+
nil
|
409
|
+
end
|
410
|
+
|
411
|
+
# Passes the #flush method to the wrapped stream, used for buffered
|
412
|
+
# streams.
|
413
|
+
def flush
|
414
|
+
raise ClosedStream if @closed
|
415
|
+
@io.flush if @io.respond_to?(:flush)
|
416
|
+
end
|
417
|
+
|
418
|
+
# Closes the Writer.
|
419
|
+
def close
|
420
|
+
return if @closed
|
421
|
+
@io.write("\0" * 1024)
|
422
|
+
@closed = true
|
423
|
+
end
|
424
|
+
|
425
|
+
private
|
426
|
+
def split_name(name)
|
427
|
+
raise FileNameTooLong if name.size > 256
|
428
|
+
if name.size <= 100
|
429
|
+
prefix = ""
|
430
|
+
else
|
431
|
+
parts = name.split(/\//)
|
432
|
+
newname = parts.pop
|
433
|
+
|
434
|
+
nxt = ""
|
435
|
+
|
436
|
+
loop do
|
437
|
+
nxt = parts.pop
|
438
|
+
break if newname.size + 1 + nxt.size > 100
|
439
|
+
newname = "#{nxt}/#{newname}"
|
440
|
+
end
|
441
|
+
|
442
|
+
prefix = (parts + [nxt]).join("/")
|
443
|
+
|
444
|
+
name = newname
|
445
|
+
|
446
|
+
raise FileNameTooLong if name.size > 100 || prefix.size > 155
|
447
|
+
end
|
448
|
+
return name, prefix
|
449
|
+
end
|
450
|
+
end
|
451
|
+
|
452
|
+
# The class that reads a tar format archive from a data stream. The data
|
453
|
+
# stream may be sequential or random access, but certain features only work
|
454
|
+
# with random access data streams.
|
455
|
+
class Reader
|
456
|
+
# This marks the EntryStream closed for reading without closing the
|
457
|
+
# actual data stream.
|
458
|
+
module InvalidEntryStream
|
459
|
+
def read(len = nil); raise ClosedStream; end
|
460
|
+
def getc; raise ClosedStream; end
|
461
|
+
def rewind; raise ClosedStream; end
|
462
|
+
end
|
463
|
+
|
464
|
+
# EntryStreams are pseudo-streams on top of the main data stream.
|
465
|
+
class EntryStream
|
466
|
+
Archive::Tar::PosixHeader::FIELDS.each do |field|
|
467
|
+
attr_reader field.intern
|
468
|
+
end
|
469
|
+
|
470
|
+
def initialize(header, anIO)
|
471
|
+
@io = anIO
|
472
|
+
@name = header.name
|
473
|
+
@mode = header.mode
|
474
|
+
@uid = header.uid
|
475
|
+
@gid = header.gid
|
476
|
+
@size = header.size
|
477
|
+
@mtime = header.mtime
|
478
|
+
@checksum = header.checksum
|
479
|
+
@typeflag = header.typeflag
|
480
|
+
@linkname = header.linkname
|
481
|
+
@magic = header.magic
|
482
|
+
@version = header.version
|
483
|
+
@uname = header.uname
|
484
|
+
@gname = header.gname
|
485
|
+
@devmajor = header.devmajor
|
486
|
+
@devminor = header.devminor
|
487
|
+
@prefix = header.prefix
|
488
|
+
@read = 0
|
489
|
+
@orig_pos = @io.pos
|
490
|
+
end
|
491
|
+
|
492
|
+
# Reads +len+ bytes (or all remaining data) from the entry. Returns
|
493
|
+
# +nil+ if there is no more data to read.
|
494
|
+
def read(len = nil)
|
495
|
+
return nil if @read >= @size
|
496
|
+
len ||= @size - @read
|
497
|
+
max_read = [len, @size - @read].min
|
498
|
+
ret = @io.read(max_read)
|
499
|
+
@read += ret.size
|
500
|
+
ret
|
501
|
+
end
|
502
|
+
|
503
|
+
# Reads one byte from the entry. Returns +nil+ if there is no more data
|
504
|
+
# to read.
|
505
|
+
def getc
|
506
|
+
return nil if @read >= @size
|
507
|
+
ret = @io.getc
|
508
|
+
@read += 1 if ret
|
509
|
+
ret
|
510
|
+
end
|
511
|
+
|
512
|
+
# Returns +true+ if the entry represents a directory.
|
513
|
+
def directory?
|
514
|
+
@typeflag == "5"
|
515
|
+
end
|
516
|
+
alias_method :directory, :directory?
|
517
|
+
|
518
|
+
# Returns +true+ if the entry represents a plain file.
|
519
|
+
def file?
|
520
|
+
@typeflag == "0"
|
521
|
+
end
|
522
|
+
alias_method :file, :file?
|
523
|
+
|
524
|
+
# Returns +true+ if the current read pointer is at the end of the
|
525
|
+
# EntryStream data.
|
526
|
+
def eof?
|
527
|
+
@read >= @size
|
528
|
+
end
|
529
|
+
|
530
|
+
# Returns the current read pointer in the EntryStream.
|
531
|
+
def pos
|
532
|
+
@read
|
533
|
+
end
|
534
|
+
|
535
|
+
# Sets the current read pointer to the beginning of the EntryStream.
|
536
|
+
def rewind
|
537
|
+
raise NonSeekableStream unless @io.respond_to?(:pos=)
|
538
|
+
@io.pos = @orig_pos
|
539
|
+
@read = 0
|
540
|
+
end
|
541
|
+
|
542
|
+
def bytes_read
|
543
|
+
@read
|
544
|
+
end
|
545
|
+
|
546
|
+
# Returns the full and proper name of the entry.
|
547
|
+
def full_name
|
548
|
+
if @prefix != ""
|
549
|
+
File.join(@prefix, @name)
|
550
|
+
else
|
551
|
+
@name
|
552
|
+
end
|
553
|
+
end
|
554
|
+
|
555
|
+
# Closes the entry.
|
556
|
+
def close
|
557
|
+
invalidate
|
558
|
+
end
|
559
|
+
|
560
|
+
private
|
561
|
+
def invalidate
|
562
|
+
extend InvalidEntryStream
|
563
|
+
end
|
564
|
+
end
|
565
|
+
|
566
|
+
# With no associated block, +Reader::open+ is a synonym for
|
567
|
+
# +Reader::new+. If the optional code block is given, it will be passed
|
568
|
+
# the new _writer_ as an argument and the Reader object will
|
569
|
+
# automatically be closed when the block terminates. In this instance,
|
570
|
+
# +Reader::open+ returns the value of the block.
|
571
|
+
def self.open(anIO)
|
572
|
+
reader = Reader.new(anIO)
|
573
|
+
|
574
|
+
return reader unless block_given?
|
575
|
+
|
576
|
+
begin
|
577
|
+
res = yield reader
|
578
|
+
ensure
|
579
|
+
reader.close
|
580
|
+
end
|
581
|
+
|
582
|
+
res
|
583
|
+
end
|
584
|
+
|
585
|
+
# Creates and returns a new Reader object.
|
586
|
+
def initialize(anIO)
|
587
|
+
@io = anIO
|
588
|
+
@init_pos = anIO.pos
|
589
|
+
end
|
590
|
+
|
591
|
+
# Iterates through each entry in the data stream.
|
592
|
+
def each(&block)
|
593
|
+
each_entry(&block)
|
594
|
+
end
|
595
|
+
|
596
|
+
# Resets the read pointer to the beginning of data stream. Do not call
|
597
|
+
# this during a #each or #each_entry iteration. This only works with
|
598
|
+
# random access data streams that respond to #rewind and #pos.
|
599
|
+
def rewind
|
600
|
+
if @init_pos == 0
|
601
|
+
raise NonSeekableStream unless @io.respond_to?(:rewind)
|
602
|
+
@io.rewind
|
603
|
+
else
|
604
|
+
raise NonSeekableStream unless @io.respond_to?(:pos=)
|
605
|
+
@io.pos = @init_pos
|
606
|
+
end
|
607
|
+
end
|
608
|
+
|
609
|
+
# Iterates through each entry in the data stream.
|
610
|
+
def each_entry
|
611
|
+
loop do
|
612
|
+
return if @io.eof?
|
613
|
+
|
614
|
+
header = Archive::Tar::PosixHeader.new_from_stream(@io)
|
615
|
+
return if header.empty?
|
616
|
+
|
617
|
+
entry = EntryStream.new(header, @io)
|
618
|
+
size = entry.size
|
619
|
+
|
620
|
+
yield entry
|
621
|
+
|
622
|
+
skip = (512 - (size % 512)) % 512
|
623
|
+
|
624
|
+
if @io.respond_to?(:seek)
|
625
|
+
# avoid reading...
|
626
|
+
@io.seek(size - entry.bytes_read, IO::SEEK_CUR)
|
627
|
+
else
|
628
|
+
pending = size - entry.bytes_read
|
629
|
+
while pending > 0
|
630
|
+
bread = @io.read([pending, 4096].min).size
|
631
|
+
raise UnexpectedEOF if @io.eof?
|
632
|
+
pending -= bread
|
633
|
+
end
|
634
|
+
end
|
635
|
+
@io.read(skip) # discard trailing zeros
|
636
|
+
# make sure nobody can use #read, #getc or #rewind anymore
|
637
|
+
entry.close
|
638
|
+
end
|
639
|
+
end
|
640
|
+
|
641
|
+
def close
|
642
|
+
end
|
643
|
+
end
|
644
|
+
|
645
|
+
# Wraps a Archive::Tar::Minitar::Reader with convenience methods and
|
646
|
+
# wrapped stream management; Input only works with random access data
|
647
|
+
# streams. See Input::new for details.
|
648
|
+
class Input
|
649
|
+
include Enumerable
|
650
|
+
|
651
|
+
# With no associated block, +Input::open+ is a synonym for
|
652
|
+
# +Input::new+. If the optional code block is given, it will be passed
|
653
|
+
# the new _writer_ as an argument and the Input object will
|
654
|
+
# automatically be closed when the block terminates. In this instance,
|
655
|
+
# +Input::open+ returns the value of the block.
|
656
|
+
def self.open(input)
|
657
|
+
stream = Input.new(input)
|
658
|
+
return stream unless block_given?
|
659
|
+
|
660
|
+
begin
|
661
|
+
res = yield stream
|
662
|
+
ensure
|
663
|
+
stream.close
|
664
|
+
end
|
665
|
+
|
666
|
+
res
|
667
|
+
end
|
668
|
+
|
669
|
+
# Creates a new Input object. If +input+ is a stream object that responds
|
670
|
+
# to #read), then it will simply be wrapped. Otherwise, one will be
|
671
|
+
# created and opened using Kernel#open. When Input#close is called, the
|
672
|
+
# stream object wrapped will be closed.
|
673
|
+
def initialize(input)
|
674
|
+
if input.respond_to?(:read)
|
675
|
+
@io = input
|
676
|
+
else
|
677
|
+
@io = open(input, "rb")
|
678
|
+
end
|
679
|
+
@tarreader = Archive::Tar::Minitar::Reader.new(@io)
|
680
|
+
end
|
681
|
+
|
682
|
+
# Iterates through each entry and rewinds to the beginning of the stream
|
683
|
+
# when finished.
|
684
|
+
def each(&block)
|
685
|
+
@tarreader.each { |entry| yield entry }
|
686
|
+
ensure
|
687
|
+
@tarreader.rewind
|
688
|
+
end
|
689
|
+
|
690
|
+
# Extracts the current +entry+ to +destdir+. If a block is provided, it
|
691
|
+
# yields an +action+ Symbol, the full name of the file being extracted
|
692
|
+
# (+name+), and a Hash of statistical information (+stats+).
|
693
|
+
#
|
694
|
+
# The +action+ will be one of:
|
695
|
+
# <tt>:dir</tt>:: The +entry+ is a directory.
|
696
|
+
# <tt>:file_start</tt>:: The +entry+ is a file; the extract of the
|
697
|
+
# file is just beginning.
|
698
|
+
# <tt>:file_progress</tt>:: Yielded every 4096 bytes during the extract
|
699
|
+
# of the +entry+.
|
700
|
+
# <tt>:file_done</tt>:: Yielded when the +entry+ is completed.
|
701
|
+
#
|
702
|
+
# The +stats+ hash contains the following keys:
|
703
|
+
# <tt>:current</tt>:: The current total number of bytes read in the
|
704
|
+
# +entry+.
|
705
|
+
# <tt>:currinc</tt>:: The current number of bytes read in this read
|
706
|
+
# cycle.
|
707
|
+
# <tt>:entry</tt>:: The entry being extracted; this is a
|
708
|
+
# Reader::EntryStream, with all methods thereof.
|
709
|
+
def extract_entry(destdir, entry) # :yields action, name, stats:
|
710
|
+
stats = {
|
711
|
+
:current => 0,
|
712
|
+
:currinc => 0,
|
713
|
+
:entry => entry
|
714
|
+
}
|
715
|
+
|
716
|
+
if entry.directory?
|
717
|
+
dest = File.join(destdir, entry.full_name)
|
718
|
+
|
719
|
+
yield :dir, entry.full_name, stats if block_given?
|
720
|
+
|
721
|
+
if Archive::Tar::Minitar.dir?(dest)
|
722
|
+
begin
|
723
|
+
FileUtils.chmod(entry.mode, dest)
|
724
|
+
rescue Exception
|
725
|
+
nil
|
726
|
+
end
|
727
|
+
else
|
728
|
+
FileUtils.mkdir_p(dest, :mode => entry.mode)
|
729
|
+
FileUtils.chmod(entry.mode, dest)
|
730
|
+
end
|
731
|
+
|
732
|
+
fsync_dir(dest)
|
733
|
+
fsync_dir(File.join(dest, ".."))
|
734
|
+
return
|
735
|
+
else # it's a file
|
736
|
+
destdir = File.join(destdir, File.dirname(entry.full_name))
|
737
|
+
FileUtils.mkdir_p(destdir, :mode => 0755)
|
738
|
+
|
739
|
+
destfile = File.join(destdir, File.basename(entry.full_name))
|
740
|
+
FileUtils.chmod(0600, destfile) rescue nil # Errno::ENOENT
|
741
|
+
|
742
|
+
yield :file_start, entry.full_name, stats if block_given?
|
743
|
+
|
744
|
+
File.open(destfile, "wb", entry.mode) do |os|
|
745
|
+
loop do
|
746
|
+
data = entry.read(4096)
|
747
|
+
break unless data
|
748
|
+
|
749
|
+
stats[:currinc] = os.write(data)
|
750
|
+
stats[:current] += stats[:currinc]
|
751
|
+
|
752
|
+
yield :file_progress, entry.full_name, stats if block_given?
|
753
|
+
end
|
754
|
+
os.fsync
|
755
|
+
end
|
756
|
+
|
757
|
+
FileUtils.chmod(entry.mode, destfile)
|
758
|
+
fsync_dir(File.dirname(destfile))
|
759
|
+
fsync_dir(File.join(File.dirname(destfile), ".."))
|
760
|
+
|
761
|
+
yield :file_done, entry.full_name, stats if block_given?
|
762
|
+
end
|
763
|
+
end
|
764
|
+
|
765
|
+
# Returns the Reader object for direct access.
|
766
|
+
def tar
|
767
|
+
@tarreader
|
768
|
+
end
|
769
|
+
|
770
|
+
# Closes the Reader object and the wrapped data stream.
|
771
|
+
def close
|
772
|
+
@io.close
|
773
|
+
@tarreader.close
|
774
|
+
end
|
775
|
+
|
776
|
+
private
|
777
|
+
def fsync_dir(dirname)
|
778
|
+
# make sure this hits the disc
|
779
|
+
dir = open(dirname, 'rb')
|
780
|
+
dir.fsync
|
781
|
+
rescue # ignore IOError if it's an unpatched (old) Ruby
|
782
|
+
nil
|
783
|
+
ensure
|
784
|
+
dir.close if dir rescue nil
|
785
|
+
end
|
786
|
+
end
|
787
|
+
|
788
|
+
# Wraps a Archive::Tar::Minitar::Writer with convenience methods and
|
789
|
+
# wrapped stream management; Output only works with random access data
|
790
|
+
# streams. See Output::new for details.
|
791
|
+
class Output
|
792
|
+
# With no associated block, +Output::open+ is a synonym for
|
793
|
+
# +Output::new+. If the optional code block is given, it will be passed
|
794
|
+
# the new _writer_ as an argument and the Output object will
|
795
|
+
# automatically be closed when the block terminates. In this instance,
|
796
|
+
# +Output::open+ returns the value of the block.
|
797
|
+
def self.open(output)
|
798
|
+
stream = Output.new(output)
|
799
|
+
return stream unless block_given?
|
800
|
+
|
801
|
+
begin
|
802
|
+
res = yield stream
|
803
|
+
ensure
|
804
|
+
stream.close
|
805
|
+
end
|
806
|
+
|
807
|
+
res
|
808
|
+
end
|
809
|
+
|
810
|
+
# Creates a new Output object. If +output+ is a stream object that
|
811
|
+
# responds to #read), then it will simply be wrapped. Otherwise, one will
|
812
|
+
# be created and opened using Kernel#open. When Output#close is called,
|
813
|
+
# the stream object wrapped will be closed.
|
814
|
+
def initialize(output)
|
815
|
+
if output.respond_to?(:write)
|
816
|
+
@io = output
|
817
|
+
else
|
818
|
+
@io = ::File.open(output, "wb")
|
819
|
+
end
|
820
|
+
@tarwriter = Archive::Tar::Minitar::Writer.new(@io)
|
821
|
+
end
|
822
|
+
|
823
|
+
# Returns the Writer object for direct access.
|
824
|
+
def tar
|
825
|
+
@tarwriter
|
826
|
+
end
|
827
|
+
|
828
|
+
# Closes the Writer object and the wrapped data stream.
|
829
|
+
def close
|
830
|
+
@tarwriter.close
|
831
|
+
@io.close
|
832
|
+
end
|
833
|
+
end
|
834
|
+
|
835
|
+
class << self
|
836
|
+
# Tests if +path+ refers to a directory. Fixes an apparently
|
837
|
+
# corrupted <tt>stat()</tt> call on Windows.
|
838
|
+
def dir?(path)
|
839
|
+
File.directory?((path[-1] == ?/) ? path : "#{path}/")
|
840
|
+
end
|
841
|
+
|
842
|
+
# A convenience method for wrapping Archive::Tar::Minitar::Input.open
|
843
|
+
# (mode +r+) and Archive::Tar::Minitar::Output.open (mode +w+). No other
|
844
|
+
# modes are currently supported.
|
845
|
+
def open(dest, mode = "r", &block)
|
846
|
+
case mode
|
847
|
+
when "r"
|
848
|
+
Input.open(dest, &block)
|
849
|
+
when "w"
|
850
|
+
Output.open(dest, &block)
|
851
|
+
else
|
852
|
+
raise "Unknown open mode for Archive::Tar::Minitar.open."
|
853
|
+
end
|
854
|
+
end
|
855
|
+
|
856
|
+
# A convenience method to packs the file provided. +entry+ may either be
|
857
|
+
# a filename (in which case various values for the file (see below) will
|
858
|
+
# be obtained from <tt>File#stat(entry)</tt> or a Hash with the fields:
|
859
|
+
#
|
860
|
+
# <tt>:name</tt>:: The filename to be packed into the tarchive.
|
861
|
+
# *REQUIRED*.
|
862
|
+
# <tt>:mode</tt>:: The mode to be applied.
|
863
|
+
# <tt>:uid</tt>:: The user owner of the file. (Ignored on Windows.)
|
864
|
+
# <tt>:gid</tt>:: The group owner of the file. (Ignored on Windows.)
|
865
|
+
# <tt>:mtime</tt>:: The modification Time of the file.
|
866
|
+
#
|
867
|
+
# During packing, if a block is provided, #pack_file yields an +action+
|
868
|
+
# Symol, the full name of the file being packed, and a Hash of
|
869
|
+
# statistical information, just as with
|
870
|
+
# Archive::Tar::Minitar::Input#extract_entry.
|
871
|
+
#
|
872
|
+
# The +action+ will be one of:
|
873
|
+
# <tt>:dir</tt>:: The +entry+ is a directory.
|
874
|
+
# <tt>:file_start</tt>:: The +entry+ is a file; the extract of the
|
875
|
+
# file is just beginning.
|
876
|
+
# <tt>:file_progress</tt>:: Yielded every 4096 bytes during the extract
|
877
|
+
# of the +entry+.
|
878
|
+
# <tt>:file_done</tt>:: Yielded when the +entry+ is completed.
|
879
|
+
#
|
880
|
+
# The +stats+ hash contains the following keys:
|
881
|
+
# <tt>:current</tt>:: The current total number of bytes read in the
|
882
|
+
# +entry+.
|
883
|
+
# <tt>:currinc</tt>:: The current number of bytes read in this read
|
884
|
+
# cycle.
|
885
|
+
# <tt>:name</tt>:: The filename to be packed into the tarchive.
|
886
|
+
# *REQUIRED*.
|
887
|
+
# <tt>:mode</tt>:: The mode to be applied.
|
888
|
+
# <tt>:uid</tt>:: The user owner of the file. (+nil+ on Windows.)
|
889
|
+
# <tt>:gid</tt>:: The group owner of the file. (+nil+ on Windows.)
|
890
|
+
# <tt>:mtime</tt>:: The modification Time of the file.
|
891
|
+
def pack_file(entry, outputter) #:yields action, name, stats:
|
892
|
+
outputter = outputter.tar if outputter.kind_of?(Archive::Tar::Minitar::Output)
|
893
|
+
|
894
|
+
stats = {}
|
895
|
+
|
896
|
+
if entry.kind_of?(Hash)
|
897
|
+
name = entry[:name]
|
898
|
+
|
899
|
+
entry.each { |kk, vv| stats[kk] = vv unless vv.nil? }
|
900
|
+
else
|
901
|
+
name = entry
|
902
|
+
end
|
903
|
+
|
904
|
+
name = name.sub(%r{\./}, '')
|
905
|
+
stat = File.stat(name)
|
906
|
+
stats[:mode] ||= stat.mode
|
907
|
+
stats[:mtime] ||= stat.mtime
|
908
|
+
stats[:size] = stat.size
|
909
|
+
|
910
|
+
if RUBY_PLATFORM =~ /win32/
|
911
|
+
stats[:uid] = nil
|
912
|
+
stats[:gid] = nil
|
913
|
+
else
|
914
|
+
stats[:uid] ||= stat.uid
|
915
|
+
stats[:gid] ||= stat.gid
|
916
|
+
end
|
917
|
+
|
918
|
+
case
|
919
|
+
when File.file?(name)
|
920
|
+
outputter.add_file_simple(name, stats) do |os|
|
921
|
+
stats[:current] = 0
|
922
|
+
yield :file_start, name, stats if block_given?
|
923
|
+
File.open(name, "rb") do |ff|
|
924
|
+
until ff.eof?
|
925
|
+
stats[:currinc] = os.write(ff.read(4096))
|
926
|
+
stats[:current] += stats[:currinc]
|
927
|
+
yield :file_progress, name, stats if block_given?
|
928
|
+
end
|
929
|
+
end
|
930
|
+
yield :file_done, name, stats if block_given?
|
931
|
+
end
|
932
|
+
when dir?(name)
|
933
|
+
yield :dir, name, stats if block_given?
|
934
|
+
outputter.mkdir(name, stats)
|
935
|
+
else
|
936
|
+
raise "Don't yet know how to pack this type of file."
|
937
|
+
end
|
938
|
+
end
|
939
|
+
|
940
|
+
# A convenience method to pack files specified by +src+ into +dest+. If
|
941
|
+
# +src+ is an Array, then each file detailed therein will be packed into
|
942
|
+
# the resulting Archive::Tar::Minitar::Output stream; if +recurse_dirs+
|
943
|
+
# is true, then directories will be recursed.
|
944
|
+
#
|
945
|
+
# If +src+ is an Array, it will be treated as the argument to Find.find;
|
946
|
+
# all files matching will be packed.
|
947
|
+
def pack(src, dest, recurse_dirs = true, &block)
|
948
|
+
Output.open(dest) do |outp|
|
949
|
+
if src.kind_of?(Array)
|
950
|
+
src.each do |entry|
|
951
|
+
pack_file(entry, outp, &block)
|
952
|
+
if dir?(entry) and recurse_dirs
|
953
|
+
Dir["#{entry}/**/**"].each do |ee|
|
954
|
+
pack_file(ee, outp, &block)
|
955
|
+
end
|
956
|
+
end
|
957
|
+
end
|
958
|
+
else
|
959
|
+
Find.find(src) do |entry|
|
960
|
+
pack_file(entry, outp, &block)
|
961
|
+
end
|
962
|
+
end
|
963
|
+
end
|
964
|
+
end
|
965
|
+
|
966
|
+
# A convenience method to unpack files from +src+ into the directory
|
967
|
+
# specified by +dest+. Only those files named explicitly in +files+
|
968
|
+
# will be extracted.
|
969
|
+
def unpack(src, dest, files = [], &block)
|
970
|
+
Input.open(src) do |inp|
|
971
|
+
if File.exist?(dest) and (not dir?(dest))
|
972
|
+
raise "Can't unpack to a non-directory."
|
973
|
+
elsif not File.exist?(dest)
|
974
|
+
FileUtils.mkdir_p(dest)
|
975
|
+
end
|
976
|
+
|
977
|
+
inp.each do |entry|
|
978
|
+
if files.empty? or files.include?(entry.full_name)
|
979
|
+
inp.extract_entry(dest, entry, &block)
|
980
|
+
end
|
981
|
+
end
|
982
|
+
end
|
983
|
+
end
|
984
|
+
end
|
985
|
+
end
|