archive-tar2 1.5
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.
- 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: []
|