archive-tar2 1.5
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/archive/tar.rb +80 -0
- data/lib/archive/tar/format.rb +147 -0
- data/lib/archive/tar/reader.rb +327 -0
- data/lib/archive/tar/stat.rb +130 -0
- data/lib/archive/tar/stream_reader.rb +54 -0
- data/lib/archive/tar/writer.rb +117 -0
- metadata +51 -0
data/lib/archive/tar.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
=begin license
|
2
|
+
Copyright (c) 2010 Fritz Grimpen
|
3
|
+
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
5
|
+
of this software and associated documentation files (the "Software"), to deal
|
6
|
+
in the Software without restriction, including without limitation the rights
|
7
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
8
|
+
copies of the Software, and to permit persons to whom the Software is
|
9
|
+
furnished to do so, subject to the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be included in
|
12
|
+
all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
19
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
20
|
+
THE SOFTWARE.
|
21
|
+
=end
|
22
|
+
|
23
|
+
module Archive
|
24
|
+
module Tar
|
25
|
+
VERSION = "1.5.0"
|
26
|
+
|
27
|
+
def normalize_path(path)
|
28
|
+
path = path.gsub("\\", "/")
|
29
|
+
|
30
|
+
while path[-1] == "/"
|
31
|
+
path = path[0..-2]
|
32
|
+
end
|
33
|
+
|
34
|
+
while path[0] == "/"
|
35
|
+
path = path[1..-1]
|
36
|
+
end
|
37
|
+
|
38
|
+
solve_path(path.gsub(/[\/]{2,}/, "/"))
|
39
|
+
end
|
40
|
+
|
41
|
+
def solve_path(path)
|
42
|
+
path_parts = path.split("/")
|
43
|
+
realpath = []
|
44
|
+
|
45
|
+
path_parts.each do |i|
|
46
|
+
if i == "."
|
47
|
+
next
|
48
|
+
end
|
49
|
+
|
50
|
+
if i == ".."
|
51
|
+
realpath = realpath[1..-2]
|
52
|
+
realpath = [] if realpath == nil
|
53
|
+
next
|
54
|
+
end
|
55
|
+
|
56
|
+
realpath << i
|
57
|
+
end
|
58
|
+
|
59
|
+
realpath.join("/")
|
60
|
+
end
|
61
|
+
|
62
|
+
def join_path(*files)
|
63
|
+
absolute = files[0][0] == "/"
|
64
|
+
files = files.map do |element|
|
65
|
+
normalize_path element
|
66
|
+
end
|
67
|
+
|
68
|
+
new_path = files.join("/")
|
69
|
+
new_path = "/" + new_path if absolute
|
70
|
+
|
71
|
+
new_path
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
require "archive/tar/format.rb"
|
77
|
+
require "archive/tar/reader.rb"
|
78
|
+
require "archive/tar/writer.rb"
|
79
|
+
require "archive/tar/stat.rb"
|
80
|
+
require "archive/tar/stream_reader.rb"
|
@@ -0,0 +1,147 @@
|
|
1
|
+
=begin license
|
2
|
+
Copyright (c) 2010 Fritz Grimpen
|
3
|
+
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
5
|
+
of this software and associated documentation files (the "Software"), to deal
|
6
|
+
in the Software without restriction, including without limitation the rights
|
7
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
8
|
+
copies of the Software, and to permit persons to whom the Software is
|
9
|
+
furnished to do so, subject to the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be included in
|
12
|
+
all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
19
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
20
|
+
THE SOFTWARE.
|
21
|
+
=end
|
22
|
+
|
23
|
+
require "archive/tar/stat"
|
24
|
+
|
25
|
+
class Archive::Tar::Format
|
26
|
+
DEC_TYPES = {
|
27
|
+
"\0" => :normal,
|
28
|
+
"0" => :normal,
|
29
|
+
"1" => :link,
|
30
|
+
"2" => :symbolic,
|
31
|
+
"3" => :character,
|
32
|
+
"4" => :block,
|
33
|
+
"5" => :directory,
|
34
|
+
"6" => :fifo,
|
35
|
+
"7" => :reserved,
|
36
|
+
"I" => :index,
|
37
|
+
"g" => :pax_global_header
|
38
|
+
}
|
39
|
+
|
40
|
+
ENC_TYPES = DEC_TYPES.invert
|
41
|
+
|
42
|
+
class << self
|
43
|
+
# Remove all NUL bytes at the end of a string
|
44
|
+
def strip_nuls(string)
|
45
|
+
until string[-1] != "\0"
|
46
|
+
string = string[0..-2]
|
47
|
+
end
|
48
|
+
|
49
|
+
string
|
50
|
+
end
|
51
|
+
|
52
|
+
# Transform tar header to Stat
|
53
|
+
def unpack_header(header)
|
54
|
+
new_obj = Archive::Tar::Stat.new
|
55
|
+
|
56
|
+
new_obj.path = strip_nuls(header[0, 100])
|
57
|
+
new_obj.mode = header[100, 8].oct
|
58
|
+
new_obj.uid = header[108, 8].oct
|
59
|
+
new_obj.gid = header[116, 8].oct
|
60
|
+
new_obj.size = header[124, 12].oct
|
61
|
+
new_obj.mtime = Time.at(header[136, 12].oct)
|
62
|
+
new_obj.checksum = header[148, 8].oct
|
63
|
+
new_obj.type = DEC_TYPES[header[156]]
|
64
|
+
new_obj.dest = strip_nuls(header[157, 100])
|
65
|
+
new_obj.format = header[257, 5] == "ustar" ?
|
66
|
+
( header[257, 6] == "ustar " ? :gnu : :ustar ) : :other
|
67
|
+
new_obj.user = strip_nuls(header[265, 32])
|
68
|
+
new_obj.group = strip_nuls(header[297, 32])
|
69
|
+
new_obj.major = header[329, 8].oct
|
70
|
+
new_obj.minor = header[337, 8].oct
|
71
|
+
|
72
|
+
new_obj.path = header[345, 155].strip + new_obj.path if new_obj.ustar?
|
73
|
+
|
74
|
+
if new_obj.gnu?
|
75
|
+
new_obj.atime = Time.at(header[345, 12].oct)
|
76
|
+
new_obj.ctime = Time.at(header[357, 12].oct)
|
77
|
+
end
|
78
|
+
|
79
|
+
new_obj
|
80
|
+
end
|
81
|
+
|
82
|
+
# Detect type of tar file by header
|
83
|
+
def detect_type(header)
|
84
|
+
return :ustar if header[257, 6] == "ustar0"
|
85
|
+
return :gnu if header[257, 6] == "ustar "
|
86
|
+
|
87
|
+
:other
|
88
|
+
end
|
89
|
+
|
90
|
+
# Generate checksum with header
|
91
|
+
def calculate_checksum(header)
|
92
|
+
checksum = 0
|
93
|
+
|
94
|
+
header.each_byte do |byte|
|
95
|
+
checksum += byte
|
96
|
+
end
|
97
|
+
|
98
|
+
checksum.to_s(8).rjust(6, " ") + "\0 "
|
99
|
+
end
|
100
|
+
|
101
|
+
# Pack header from Stat
|
102
|
+
def pack_header(header)
|
103
|
+
blob = ""
|
104
|
+
|
105
|
+
blob += header.path.ljust(100, "\0")
|
106
|
+
blob += header.mode.to_s(8).rjust(8, "0")
|
107
|
+
blob += header.uid.to_s(8).rjust(8, "0")
|
108
|
+
blob += header.gid.to_s(8).rjust(8, "0")
|
109
|
+
blob += header.size.to_s(8).rjust(12, "0")
|
110
|
+
blob += header.mtime.to_i.to_s(8).rjust(12, "0")
|
111
|
+
blob += " " * 8
|
112
|
+
blob += ENC_TYPES[header.type]
|
113
|
+
blob += header.dest.ljust(100, "\0")
|
114
|
+
|
115
|
+
case header.format
|
116
|
+
when :ustar
|
117
|
+
blob += "ustar\000"
|
118
|
+
when :gnu
|
119
|
+
blob += "ustar \0"
|
120
|
+
end
|
121
|
+
|
122
|
+
if header.gnu? || header.ustar?
|
123
|
+
blob += header.user.ljust(32, "\0")
|
124
|
+
blob += header.group.ljust(32, "\0")
|
125
|
+
blob += header.major.to_s(8).rjust(8, "0")
|
126
|
+
blob += header.minor.to_s(8).rjust(8, "0")
|
127
|
+
|
128
|
+
if header.gnu?
|
129
|
+
blob += header.atime.to_i.to_s(8).rjust(12, "0")
|
130
|
+
blob += header.ctime.to_i.to_s(8).rjust(12, "0")
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
pad_length = 512 - blob.bytesize
|
135
|
+
blob += "\0" * pad_length
|
136
|
+
|
137
|
+
blob[148, 8] = calculate_checksum(blob)
|
138
|
+
|
139
|
+
blob
|
140
|
+
end
|
141
|
+
|
142
|
+
# Calculate quantity of blocks
|
143
|
+
def blocks_for_bytes(bytes)
|
144
|
+
bytes % 512 == 0 ? bytes / 512 : (bytes + 512 - bytes % 512) / 512
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
@@ -0,0 +1,327 @@
|
|
1
|
+
=begin license
|
2
|
+
Copyright (c) 2010 Fritz Grimpen
|
3
|
+
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
5
|
+
of this software and associated documentation files (the "Software"), to deal
|
6
|
+
in the Software without restriction, including without limitation the rights
|
7
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
8
|
+
copies of the Software, and to permit persons to whom the Software is
|
9
|
+
furnished to do so, subject to the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be included in
|
12
|
+
all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
19
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
20
|
+
THE SOFTWARE.
|
21
|
+
=end
|
22
|
+
|
23
|
+
require "fileutils"
|
24
|
+
require "archive/tar/format"
|
25
|
+
|
26
|
+
class Archive::Tar::NoSuchEntryError < RuntimeError
|
27
|
+
end
|
28
|
+
|
29
|
+
class Archive::Tar::Reader
|
30
|
+
include Archive::Tar
|
31
|
+
|
32
|
+
def initialize(stream, options = {})
|
33
|
+
options = {
|
34
|
+
compression: :auto,
|
35
|
+
tmpdir: "/tmp",
|
36
|
+
block_size: 2 ** 19,
|
37
|
+
read_limit: 2 ** 19,
|
38
|
+
cache: true,
|
39
|
+
cache_size: 16,
|
40
|
+
max_cache_size: 2 ** 19,
|
41
|
+
generate_index: true,
|
42
|
+
use_normalized_paths: true,
|
43
|
+
}.merge(options)
|
44
|
+
|
45
|
+
if stream.is_a? String
|
46
|
+
stream = File.new(stream)
|
47
|
+
end
|
48
|
+
|
49
|
+
@options = options
|
50
|
+
@stream = _generate_compressed_stream(stream, options[:compression])
|
51
|
+
@cache_candidates = []
|
52
|
+
@cache = {}
|
53
|
+
|
54
|
+
build_index if options[:generate_index]
|
55
|
+
end
|
56
|
+
|
57
|
+
def stream
|
58
|
+
@stream
|
59
|
+
end
|
60
|
+
|
61
|
+
def index
|
62
|
+
@index
|
63
|
+
end
|
64
|
+
|
65
|
+
def stat(file, exception = true)
|
66
|
+
if @options[:use_normalized_paths]
|
67
|
+
result = @index[normalize_path(file)]
|
68
|
+
else
|
69
|
+
result = @index[file]
|
70
|
+
end
|
71
|
+
raise NoSuchEntryError.new(file) if result == nil && exception
|
72
|
+
|
73
|
+
result
|
74
|
+
end
|
75
|
+
|
76
|
+
def [](file)
|
77
|
+
stat(file, false)
|
78
|
+
end
|
79
|
+
|
80
|
+
def has_entry?(file)
|
81
|
+
@index.key? file
|
82
|
+
end
|
83
|
+
|
84
|
+
def entry?(file)
|
85
|
+
has_entry? file
|
86
|
+
end
|
87
|
+
|
88
|
+
def each(&block)
|
89
|
+
@index.each do |key, value|
|
90
|
+
block.call(*(value))
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def read(name, no_cache = false)
|
95
|
+
header, offset = stat(name)
|
96
|
+
|
97
|
+
if @options[:cache] && header[:size] <= @options[:max_cache_size] && !no_cache
|
98
|
+
@cache_candidates << name
|
99
|
+
rebuild_cache
|
100
|
+
end
|
101
|
+
|
102
|
+
if @cache.key? name && !no_cache
|
103
|
+
return @cache[name]
|
104
|
+
end
|
105
|
+
|
106
|
+
@stream.seek(offset)
|
107
|
+
@stream.read(header[:size])
|
108
|
+
end
|
109
|
+
|
110
|
+
# Extract all files to /dest/ directory
|
111
|
+
def extract_all(dest, options = {})
|
112
|
+
options = {
|
113
|
+
:preserve => false,
|
114
|
+
:override => false
|
115
|
+
}.merge(options)
|
116
|
+
|
117
|
+
unless File::exists? dest
|
118
|
+
FileUtils::mkdir_p dest
|
119
|
+
end
|
120
|
+
|
121
|
+
unless File.directory? dest
|
122
|
+
raise "No such directory: #{dest}"
|
123
|
+
end
|
124
|
+
|
125
|
+
@index.each_key do |entry|
|
126
|
+
ndest = File.join(dest, entry)
|
127
|
+
header, offset = @index[entry]
|
128
|
+
|
129
|
+
_extract(header, offset, ndest, options)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
# Extract file from /source/ to /dest/
|
134
|
+
def extract(source, dest, options = {})
|
135
|
+
options = {
|
136
|
+
recursive: true,
|
137
|
+
preserve: false,
|
138
|
+
override: false
|
139
|
+
}.merge(options)
|
140
|
+
unless @index.key? source
|
141
|
+
raise NoSuchEntryError.new(source)
|
142
|
+
end
|
143
|
+
|
144
|
+
header, offset = @index[source]
|
145
|
+
_extract(header, offset, dest, options)
|
146
|
+
|
147
|
+
if header[:type] == :directory && options[:recursive]
|
148
|
+
@index.each_key do |entry|
|
149
|
+
if entry[0, source.length] == source && entry != source
|
150
|
+
extract(entry, File.join(dest, entry.sub(source, "")), options)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
# Build new index of files
|
157
|
+
def build_index
|
158
|
+
new_index = {}
|
159
|
+
|
160
|
+
@stream.rewind
|
161
|
+
|
162
|
+
until @stream.eof?
|
163
|
+
raw_header = @stream.read(512)
|
164
|
+
break if raw_header == "\0" * 512
|
165
|
+
|
166
|
+
header = Archive::Tar::Format::unpack_header(raw_header)
|
167
|
+
if @options[:use_normalized_paths]
|
168
|
+
header[:path] = normalize_path(header[:path])
|
169
|
+
end
|
170
|
+
|
171
|
+
unless header[:type] == :pax_global_header
|
172
|
+
new_index[header[:path]] = [ header, @stream.tell ]
|
173
|
+
end
|
174
|
+
|
175
|
+
@stream.seek(header[:blocks] * 512, IO::SEEK_CUR)
|
176
|
+
end
|
177
|
+
|
178
|
+
@index = new_index
|
179
|
+
end
|
180
|
+
|
181
|
+
protected
|
182
|
+
def rebuild_cache
|
183
|
+
return nil unless @options[:cache]
|
184
|
+
|
185
|
+
cache_count = {}
|
186
|
+
@cache_candidates.each do |candidate|
|
187
|
+
cache_count[candidate] = 0 unless cache_count.key? candidate
|
188
|
+
cache_count[candidate] += 1
|
189
|
+
end
|
190
|
+
cache_count_sorted = cache_count.sort do |pair_1, pair_2|
|
191
|
+
(pair_1[1] <=> pair_2[1]) * -1
|
192
|
+
end
|
193
|
+
|
194
|
+
puts cache_count_sorted
|
195
|
+
|
196
|
+
@cache = {}
|
197
|
+
i = 0
|
198
|
+
cache_count_sorted.each do |tupel|
|
199
|
+
if i >= @options[:cache_size]
|
200
|
+
break
|
201
|
+
end
|
202
|
+
|
203
|
+
@cache[tupel[0]] = read(tupel[0], true)
|
204
|
+
i += 1
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
def export_to_file(offset, length, source, destination)
|
209
|
+
destination = File.new(destination, "w+b") if destination.is_a?(String)
|
210
|
+
|
211
|
+
if @options[:max_cache_size] >= length && @options[:cache]
|
212
|
+
@cache_candidates << source
|
213
|
+
rebuild_cache
|
214
|
+
end
|
215
|
+
|
216
|
+
if @cache.key? source
|
217
|
+
destination.write(@cache[source])
|
218
|
+
return true
|
219
|
+
end
|
220
|
+
|
221
|
+
@stream.seek(offset)
|
222
|
+
|
223
|
+
if length <= @options[:read_limit]
|
224
|
+
destination.write(@stream.read(length))
|
225
|
+
return true
|
226
|
+
end
|
227
|
+
|
228
|
+
i = 0
|
229
|
+
while i < length
|
230
|
+
destination.write(@stream.read(@options[:block_size]))
|
231
|
+
i += @options[:block_size]
|
232
|
+
end
|
233
|
+
|
234
|
+
true
|
235
|
+
end
|
236
|
+
|
237
|
+
def _extract(header, offset, dest, options)
|
238
|
+
if !options[:override] && File::exists?(dest)
|
239
|
+
return
|
240
|
+
end
|
241
|
+
|
242
|
+
case header[:type]
|
243
|
+
when :normal
|
244
|
+
export_to_file(offset, header[:size], header[:path], dest)
|
245
|
+
when :directory
|
246
|
+
if !File.exists? dest
|
247
|
+
Dir.mkdir(dest)
|
248
|
+
end
|
249
|
+
when :symbolic
|
250
|
+
File.symlink(header[:dest], dest)
|
251
|
+
when :link
|
252
|
+
if header[:dest][0] == "/"
|
253
|
+
FileUtils.touch(header[:dest])
|
254
|
+
else
|
255
|
+
FileUtils.touch(realpath("#{dest}/../#{header[:dest]}"))
|
256
|
+
end
|
257
|
+
|
258
|
+
File.link(header[:dest], dest)
|
259
|
+
when :block
|
260
|
+
system("mknod '#{dest}' b #{header[:major]} #{header[:minor]}")
|
261
|
+
when :character
|
262
|
+
system("mknod '#{dest}' c #{header[:major]} #{header[:minor]}")
|
263
|
+
when :fifo
|
264
|
+
system("mknod '#{dest}' p #{header[:major]} #{header[:minor]}")
|
265
|
+
end
|
266
|
+
|
267
|
+
if options[:preserve]
|
268
|
+
File.chmod(header[:mode], dest)
|
269
|
+
File.chown(header[:uid], header[:gid], dest)
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
private
|
274
|
+
def _detect_compression(filename)
|
275
|
+
return :none unless filename.include? "."
|
276
|
+
|
277
|
+
case filename.slice(Range.new(filename.rindex(".") + 1, -1))
|
278
|
+
when "gz", "tgz"
|
279
|
+
return :gzip
|
280
|
+
when "bz2", "tbz", "tb2"
|
281
|
+
return :bzip2
|
282
|
+
when "xz", "txz"
|
283
|
+
return :xz
|
284
|
+
when "lz", "lzma", "tlz"
|
285
|
+
return :lzma
|
286
|
+
end
|
287
|
+
|
288
|
+
return :none
|
289
|
+
end
|
290
|
+
|
291
|
+
def _generate_compressed_stream(stream, compression)
|
292
|
+
if compression == :auto
|
293
|
+
if stream.is_a? File
|
294
|
+
compression = _detect_compression(stream.path)
|
295
|
+
else
|
296
|
+
compression = :none
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
programs = {
|
301
|
+
gzip: "gzip -d -c -f",
|
302
|
+
bzip2: "bzip2 -d -c -f",
|
303
|
+
lzma: "lzma -d -c -f",
|
304
|
+
xz: "xz -d -c -f"
|
305
|
+
}
|
306
|
+
|
307
|
+
unless programs.key? compression
|
308
|
+
return stream
|
309
|
+
end
|
310
|
+
|
311
|
+
io = IO::popen("/usr/bin/env #{programs[compression]}", "a+b")
|
312
|
+
new_file = File.open("#{@options[:tmpdir]}/#{rand(500512)}", "w+b")
|
313
|
+
Thread.new do
|
314
|
+
until stream.eof?
|
315
|
+
io.write(stream.read(@options[:block_size]))
|
316
|
+
end
|
317
|
+
|
318
|
+
io.close_write
|
319
|
+
end
|
320
|
+
|
321
|
+
until io.eof?
|
322
|
+
new_file.write(io.read(@options[:block_size]))
|
323
|
+
end
|
324
|
+
|
325
|
+
new_file
|
326
|
+
end
|
327
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
=begin license
|
2
|
+
Copyright (c) 2010 Fritz Grimpen
|
3
|
+
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
5
|
+
of this software and associated documentation files (the "Software"), to deal
|
6
|
+
in the Software without restriction, including without limitation the rights
|
7
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
8
|
+
copies of the Software, and to permit persons to whom the Software is
|
9
|
+
furnished to do so, subject to the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be included in
|
12
|
+
all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
19
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
20
|
+
THE SOFTWARE.
|
21
|
+
=end
|
22
|
+
|
23
|
+
class Archive::Tar::Stat
|
24
|
+
attr_accessor :checksum, :path, :mode, :uid, :gid, :size, :mtime, :type,
|
25
|
+
:dest, :format, :user, :group, :atime, :ctime, :major, :minor
|
26
|
+
attr_accessor :path
|
27
|
+
attr_accessor :mode
|
28
|
+
|
29
|
+
def initialize
|
30
|
+
@path = ""
|
31
|
+
@mode = 0777
|
32
|
+
@uid = 0
|
33
|
+
@gid = 0
|
34
|
+
@size = 0
|
35
|
+
@mtime = Time.at(0)
|
36
|
+
@type = :normal
|
37
|
+
@dest = ""
|
38
|
+
@format = :ustar
|
39
|
+
@user = ""
|
40
|
+
@group = ""
|
41
|
+
@atime = Time.at(0)
|
42
|
+
@ctime = Time.at(0)
|
43
|
+
@major = 0
|
44
|
+
@minor = 0
|
45
|
+
@checksum = 0
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.from_file(file)
|
49
|
+
file = File.new(file.to_s) unless file.is_a? File
|
50
|
+
stat = Archive::Tar::Stat.new
|
51
|
+
|
52
|
+
file_stat = file.stat
|
53
|
+
path = file.path
|
54
|
+
|
55
|
+
stat.path = path
|
56
|
+
stat.mode = file_stat.mode
|
57
|
+
stat.uid = file_stat.uid
|
58
|
+
stat.gid = file_stat.gid
|
59
|
+
stat.size = file_stat.size
|
60
|
+
stat.mtime = file_stat.mtime
|
61
|
+
|
62
|
+
if file_stat.blockdev?
|
63
|
+
stat.type = :block
|
64
|
+
stat.size = 0
|
65
|
+
elsif file_stat.chardev?
|
66
|
+
stat.type = :character
|
67
|
+
stat.size = 0
|
68
|
+
elsif file_stat.directory?
|
69
|
+
stat.type = :directory
|
70
|
+
stat.size = 0
|
71
|
+
elsif file_stat.pipe?
|
72
|
+
stat.type = :fifo
|
73
|
+
stat.size = 0
|
74
|
+
elsif file_stat.symlink?
|
75
|
+
stat.type = :symbolic
|
76
|
+
stat.size = 0
|
77
|
+
else
|
78
|
+
stat.type = :normal
|
79
|
+
end
|
80
|
+
|
81
|
+
stat.dest = File.readlink(path) if stat.type == :symbolic
|
82
|
+
stat.format = :ustar
|
83
|
+
stat.atime = file_stat.atime
|
84
|
+
stat.ctime = file_stat.ctime
|
85
|
+
|
86
|
+
stat.major = file_stat.rdev_major
|
87
|
+
stat.minor = file_stat.rdev_minor
|
88
|
+
|
89
|
+
stat
|
90
|
+
end
|
91
|
+
|
92
|
+
def blocks
|
93
|
+
size % 512 == 0 ? size / 512 : (size + 512 - size % 512) / 512
|
94
|
+
end
|
95
|
+
|
96
|
+
def is_ustar?
|
97
|
+
format == :ustar
|
98
|
+
end
|
99
|
+
|
100
|
+
def ustar?
|
101
|
+
is_ustar?
|
102
|
+
end
|
103
|
+
|
104
|
+
def is_gnu?
|
105
|
+
format == :gnu
|
106
|
+
end
|
107
|
+
|
108
|
+
def gnu?
|
109
|
+
is_gnu?
|
110
|
+
end
|
111
|
+
|
112
|
+
def version
|
113
|
+
"00"
|
114
|
+
end
|
115
|
+
|
116
|
+
def [](name)
|
117
|
+
self.method(name.to_sym).call
|
118
|
+
end
|
119
|
+
|
120
|
+
def []=(name, value)
|
121
|
+
self.method((name.to_s + "=").to_sym).call(value)
|
122
|
+
end
|
123
|
+
|
124
|
+
def each(&block)
|
125
|
+
[ :path, :mode, :uid, :gid, :size, :mtime, :type, :dest, :format, :user,
|
126
|
+
:group, :major, :minor, :atime, :ctime, :checksum ].each do |elem|
|
127
|
+
block.call(elem, self[elem])
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
=begin license
|
2
|
+
Copyright (c) 2010 Fritz Grimpen
|
3
|
+
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
5
|
+
of this software and associated documentation files (the "Software"), to deal
|
6
|
+
in the Software without restriction, including without limitation the rights
|
7
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
8
|
+
copies of the Software, and to permit persons to whom the Software is
|
9
|
+
furnished to do so, subject to the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be included in
|
12
|
+
all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
19
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
20
|
+
THE SOFTWARE.
|
21
|
+
=end
|
22
|
+
|
23
|
+
require "archive/tar/reader"
|
24
|
+
|
25
|
+
class Archive::Tar::StreamReader < Archive::Tar::Reader
|
26
|
+
def initialize(stream, options = {})
|
27
|
+
options = {
|
28
|
+
block_size: 2 ** 19,
|
29
|
+
reload_time: 32
|
30
|
+
}.merge(options)
|
31
|
+
|
32
|
+
stream = IO.new(stream) if io.is_a? Integer
|
33
|
+
|
34
|
+
if options[:compression] == :auto
|
35
|
+
raise "Automatic compression is not available for streams"
|
36
|
+
end
|
37
|
+
|
38
|
+
tmp_file = File.new("/tmp/" + rand(500000).to_s(16) + ".tar", "w+b")
|
39
|
+
Thread.new do
|
40
|
+
i = 0
|
41
|
+
|
42
|
+
until stream.eof?
|
43
|
+
read = stream.read(options[:block_size])
|
44
|
+
tmp_file.write(read)
|
45
|
+
self.build_index if i % options[:reload_time] == 0
|
46
|
+
i += 1
|
47
|
+
end
|
48
|
+
|
49
|
+
self.build_index
|
50
|
+
end
|
51
|
+
|
52
|
+
super(tmp_file, options)
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
=begin license
|
2
|
+
Copyright (c) 2010 Fritz Grimpen
|
3
|
+
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
5
|
+
of this software and associated documentation files (the "Software"), to deal
|
6
|
+
in the Software without restriction, including without limitation the rights
|
7
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
8
|
+
copies of the Software, and to permit persons to whom the Software is
|
9
|
+
furnished to do so, subject to the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be included in
|
12
|
+
all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
19
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
20
|
+
THE SOFTWARE.
|
21
|
+
=end
|
22
|
+
|
23
|
+
require "archive/tar/stat.rb"
|
24
|
+
require "archive/tar/format.rb"
|
25
|
+
|
26
|
+
class Archive::Tar::WriterError < RuntimeError
|
27
|
+
end
|
28
|
+
|
29
|
+
class Archive::Tar::Writer
|
30
|
+
include Archive::Tar
|
31
|
+
|
32
|
+
def initialize(stream, options = {})
|
33
|
+
options = {
|
34
|
+
block_size: 2 ** 19,
|
35
|
+
format: :gnu
|
36
|
+
}.merge(options)
|
37
|
+
|
38
|
+
@options = options
|
39
|
+
@stream = stream
|
40
|
+
@inodes = {}
|
41
|
+
end
|
42
|
+
|
43
|
+
def add_entry(header, content)
|
44
|
+
@atream.write(Archive::Tar::Format::pack_header(header))
|
45
|
+
|
46
|
+
content = content[0, header.size].ljust(header.blocks * 512, "\0")
|
47
|
+
@stream.write(content)
|
48
|
+
end
|
49
|
+
|
50
|
+
def add_file(file, path = nil)
|
51
|
+
file = File.is_a?(File) ? file : File.new(file)
|
52
|
+
path = path == nil ? file.path : path
|
53
|
+
|
54
|
+
ino = File::Stat.new(file).ino
|
55
|
+
stat = Archive::Tar::Stat::from_file(file)
|
56
|
+
stat.path = path
|
57
|
+
stat.format = @options[:format]
|
58
|
+
if @inodes.has_key? ino && path != @inodes[ino]
|
59
|
+
stat.type = :link
|
60
|
+
stat.size = 0
|
61
|
+
stat.dest = @inodes[ino]
|
62
|
+
else
|
63
|
+
@inodes[ino] = path
|
64
|
+
end
|
65
|
+
|
66
|
+
header = Archive::Tar::Format::pack_header(stat)
|
67
|
+
@stream.write(header)
|
68
|
+
|
69
|
+
if stat.type == :normal
|
70
|
+
num_of_nils = stat.blocks * 512 - stat.size
|
71
|
+
until file.eof?
|
72
|
+
@stream.write(file.read(@options[:block_size]))
|
73
|
+
end
|
74
|
+
|
75
|
+
@stream.write("\0" * num_of_nils)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def add_directory(dir, options = {})
|
80
|
+
options = {
|
81
|
+
full_path: false,
|
82
|
+
recursive: true,
|
83
|
+
archive_base: ""
|
84
|
+
}.merge(options)
|
85
|
+
|
86
|
+
real_base = dir
|
87
|
+
archive_base = options[:archive_base]
|
88
|
+
|
89
|
+
unless dir.is_a? Dir
|
90
|
+
dir = Dir.open(dir)
|
91
|
+
end
|
92
|
+
|
93
|
+
#add_file(dir.path, archive_base)
|
94
|
+
|
95
|
+
dir.each do |entry|
|
96
|
+
if entry == "." || entry == ".."
|
97
|
+
next
|
98
|
+
end
|
99
|
+
|
100
|
+
realpath = File.join(real_base, entry)
|
101
|
+
real_archive_path = join_path(archive_base, entry)
|
102
|
+
|
103
|
+
add_file(realpath, real_archive_path)
|
104
|
+
|
105
|
+
if File::Stat.new(realpath).directory? && options[:recursive]
|
106
|
+
new_options = options
|
107
|
+
new_options[:archive_base] = real_archive_path
|
108
|
+
|
109
|
+
add_directory(realpath, new_options)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def close()
|
115
|
+
@stream.write("\0" * 1024)
|
116
|
+
end
|
117
|
+
end
|
metadata
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: archive-tar2
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '1.5'
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Fritz Grimpen
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-01-02 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description:
|
15
|
+
email: fritz+archive-tar@grimpen.net
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- lib/archive/tar/format.rb
|
21
|
+
- lib/archive/tar/reader.rb
|
22
|
+
- lib/archive/tar/stream_reader.rb
|
23
|
+
- lib/archive/tar/writer.rb
|
24
|
+
- lib/archive/tar/stat.rb
|
25
|
+
- lib/archive/tar.rb
|
26
|
+
homepage: http://grimpen.net/archive-tar
|
27
|
+
licenses:
|
28
|
+
- MIT
|
29
|
+
post_install_message:
|
30
|
+
rdoc_options: []
|
31
|
+
require_paths:
|
32
|
+
- lib
|
33
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
34
|
+
none: false
|
35
|
+
requirements:
|
36
|
+
- - ! '>='
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: '0'
|
39
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
40
|
+
none: false
|
41
|
+
requirements:
|
42
|
+
- - ! '>='
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: '0'
|
45
|
+
requirements: []
|
46
|
+
rubyforge_project:
|
47
|
+
rubygems_version: 1.8.11
|
48
|
+
signing_key:
|
49
|
+
specification_version: 3
|
50
|
+
summary: Improved TAR implementation in Ruby
|
51
|
+
test_files: []
|