folio 0.3.0 → 0.4.0

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