multi_zip 0.1.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +16 -0
- data/.rspec +3 -0
- data/.travis.yml +17 -0
- data/BACKEND_CONSTANTS.md +26 -0
- data/Gemfile +19 -0
- data/Guardfile +77 -0
- data/LICENSE.txt +22 -0
- data/README.md +294 -0
- data/Rakefile +11 -0
- data/lib/multi_zip/backend/archive_zip.rb +111 -0
- data/lib/multi_zip/backend/rubyzip.rb +73 -0
- data/lib/multi_zip/backend/zipruby.rb +94 -0
- data/lib/multi_zip/errors.rb +35 -0
- data/lib/multi_zip/version.rb +3 -0
- data/lib/multi_zip.rb +195 -0
- data/multi_zip.gemspec +24 -0
- data/spec/backend_shared_example.rb +487 -0
- data/spec/fixtures/invalid.zip +1 -0
- data/spec/fixtures/test.zip +0 -0
- data/spec/lib/multi_zip/backend/archive_zip_spec.rb +6 -0
- data/spec/lib/multi_zip/backend/rubyzip_spec.rb +8 -0
- data/spec/lib/multi_zip/backend/zipruby_spec.rb +8 -0
- data/spec/lib/multi_zip_spec.rb +53 -0
- data/spec/spec_helper.rb +142 -0
- metadata +120 -0
@@ -0,0 +1,94 @@
|
|
1
|
+
module MultiZip::Backend::Zipruby
|
2
|
+
BUFFER_SIZE = 8192
|
3
|
+
def read_member(member_path, options = {})
|
4
|
+
# detect if called asked for a directory instead of a file
|
5
|
+
member_not_found!(member_path) if member_path =~ /\/$/
|
6
|
+
read_operation do |ar|
|
7
|
+
exists_in_archive!(ar, member_path)
|
8
|
+
ar.fopen(member_path) {|member| member.read}
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def list_members(prefix=nil, options={})
|
13
|
+
read_operation do |zip|
|
14
|
+
list = []
|
15
|
+
zip.num_files.times do |i|
|
16
|
+
list << zip.get_name(i)
|
17
|
+
end
|
18
|
+
list.select{|n| prefix ? n =~ /^#{prefix}/ : true}.sort
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def write_member(member_path, member_contents, options = {})
|
23
|
+
flags = (File.exists?(@filename) ? nil : Zip::CREATE)
|
24
|
+
Zip::Archive.open(@filename, flags) do |ar|
|
25
|
+
if ar.map(&:name).include?(member_path)
|
26
|
+
ar.replace_buffer(member_path, member_contents)
|
27
|
+
else
|
28
|
+
ar.add_buffer(member_path, member_contents)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
true
|
32
|
+
end
|
33
|
+
|
34
|
+
def extract_member(member_path, destination_path, options = {})
|
35
|
+
# detect if called asked for a directory instead of a file
|
36
|
+
member_not_found!(member_path) if member_path =~ /\/$/
|
37
|
+
read_operation do |ar|
|
38
|
+
exists_in_archive!(ar, member_path)
|
39
|
+
output_file = ::File.new(destination_path, 'wb')
|
40
|
+
|
41
|
+
ar.fopen(member_path) do |member|
|
42
|
+
while chunk = member.read(BUFFER_SIZE)
|
43
|
+
output_file.write chunk
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
output_file.close
|
48
|
+
end
|
49
|
+
destination_path
|
50
|
+
end
|
51
|
+
|
52
|
+
def remove_member(member_path, options = {})
|
53
|
+
archive_exists!
|
54
|
+
Zip::Archive.open(@filename) do |ar|
|
55
|
+
exists_in_archive!(ar, member_path)
|
56
|
+
ar.fdelete(ar.locate_name(member_path))
|
57
|
+
end
|
58
|
+
true
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
# NOTE: Zip::Archive#locate_name return values
|
64
|
+
# -1 if path not found
|
65
|
+
# 0 if path is a directory
|
66
|
+
# 2 if path is a file
|
67
|
+
#
|
68
|
+
# for a directory to be found it must include the trailing slash ('/').
|
69
|
+
|
70
|
+
def exists_in_archive?(zip, member_path)
|
71
|
+
zip.locate_name(member_path).to_i >= 0 # will find files or dirs.
|
72
|
+
end
|
73
|
+
|
74
|
+
def exists_in_archive!(zip, member_path)
|
75
|
+
unless exists_in_archive?(zip, member_path)
|
76
|
+
zip.close
|
77
|
+
raise member_not_found!(member_path)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def read_operation(&blk)
|
82
|
+
archive_exists!
|
83
|
+
Zip::Archive.open(@filename) do |ar|
|
84
|
+
yield(ar)
|
85
|
+
end
|
86
|
+
rescue Zip::Error => e
|
87
|
+
# not the best way to detect the class of error.
|
88
|
+
if e.message.match('Not a zip archive')
|
89
|
+
raise MultiZip::InvalidArchiveError.new(@filename, e)
|
90
|
+
else
|
91
|
+
raise
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
class MultiZip
|
2
|
+
class BaseError < RuntimeError; end
|
3
|
+
|
4
|
+
class NoSupportedBackendError < BaseError; end
|
5
|
+
|
6
|
+
class ArchiveError < BaseError
|
7
|
+
attr_reader :archive_filename, :original_exception
|
8
|
+
def initialize(archive_filename, original_exception=nil)
|
9
|
+
@archive_filename = archive_filename
|
10
|
+
@original_exception = original_exception
|
11
|
+
end
|
12
|
+
def message
|
13
|
+
"Archive \"#{@archive_filename}\" error: #{@original_exception.message}"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class InvalidArchiveError < ArchiveError; end
|
18
|
+
class ArchiveNotFoundError < ArchiveError
|
19
|
+
def message
|
20
|
+
"Archive \"#{@archive_filename}\" not found"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class MemberError < BaseError
|
25
|
+
attr_reader :member_path
|
26
|
+
def initialize(member_path)
|
27
|
+
@member_path = member_path
|
28
|
+
end
|
29
|
+
def message
|
30
|
+
"Member \"#{@member_path}\" not found."
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class MemberNotFoundError < MemberError; end
|
35
|
+
end
|
data/lib/multi_zip.rb
ADDED
@@ -0,0 +1,195 @@
|
|
1
|
+
class MultiZip
|
2
|
+
module Backend; end # populated later by #extend
|
3
|
+
|
4
|
+
attr_reader :filename
|
5
|
+
|
6
|
+
BACKEND_PREFERENCE = [ :rubyzip, :archive_zip, :zipruby ]
|
7
|
+
BACKENDS = {
|
8
|
+
:rubyzip => {
|
9
|
+
:fingerprints => [
|
10
|
+
['constant', lambda { defined?(Zip::File) } ],
|
11
|
+
[nil, lambda { defined?(Zip::Archive) }]
|
12
|
+
],
|
13
|
+
:constant => lambda { MultiZip::Backend::Rubyzip }
|
14
|
+
},
|
15
|
+
:archive_zip => {
|
16
|
+
:fingerprints => [
|
17
|
+
['constant', lambda { defined?(Archive::Zip) } ]
|
18
|
+
],
|
19
|
+
:constant => lambda { MultiZip::Backend::ArchiveZip }
|
20
|
+
},
|
21
|
+
:zipruby => {
|
22
|
+
:fingerprints => [
|
23
|
+
['constant', lambda { defined?(Zip::File) } ],
|
24
|
+
['constant', lambda { defined?(Zip::Archive) }]
|
25
|
+
],
|
26
|
+
:constant => lambda { MultiZip::Backend::Zipruby }
|
27
|
+
}
|
28
|
+
}
|
29
|
+
|
30
|
+
def initialize(filename, options = {})
|
31
|
+
@filename = filename
|
32
|
+
|
33
|
+
self.backend = if b_end = options.delete(:backend)
|
34
|
+
b_end
|
35
|
+
else
|
36
|
+
default_backend
|
37
|
+
end
|
38
|
+
|
39
|
+
if block_given?
|
40
|
+
yield(self)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def backend
|
45
|
+
@backend ||= default_backend
|
46
|
+
end
|
47
|
+
|
48
|
+
def backend=(backend_name)
|
49
|
+
return if backend_name.nil?
|
50
|
+
if BACKENDS.keys.include?(backend_name.to_sym)
|
51
|
+
@backend = backend_name.to_sym
|
52
|
+
require "multi_zip/backend/#{@backend}"
|
53
|
+
extend BACKENDS[@backend][:constant].call
|
54
|
+
return @backend
|
55
|
+
else
|
56
|
+
raise NoSupportedBackendError, "Not a supported backend. Supported backends are #{BACKENDS.map(&:first).map(&:to_s).sort.join(', ')}"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.supported_backends
|
61
|
+
BACKENDS.keys
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.available_backends
|
65
|
+
available = []
|
66
|
+
BACKENDS.each do |name, opts|
|
67
|
+
if opts[:fingerprints].all?{|expectation, lmb| lmb.call == expectation }
|
68
|
+
available << name
|
69
|
+
end
|
70
|
+
end
|
71
|
+
available
|
72
|
+
end
|
73
|
+
|
74
|
+
# Close the archive, if the archive is open.
|
75
|
+
# If the archive is already closed, behave as though it was open.
|
76
|
+
# Expected to always return true.
|
77
|
+
#
|
78
|
+
# This is currently a non-op since the archives are not kept open between
|
79
|
+
# method calls. It is here so users can write code using it to prepare for
|
80
|
+
# when we *do* keep the archives open.
|
81
|
+
#
|
82
|
+
# Currently, this method MUST NOT be overridden.
|
83
|
+
def close
|
84
|
+
return true
|
85
|
+
end
|
86
|
+
|
87
|
+
# Intended to return the contents of a zip member as a string.
|
88
|
+
#
|
89
|
+
# This method MUST be overridden by a backend module.
|
90
|
+
def read_member(member_path, options={})
|
91
|
+
raise NotImplementedError
|
92
|
+
end
|
93
|
+
|
94
|
+
# Intended to return the contents of zip members as array of strings.
|
95
|
+
#
|
96
|
+
# This method MAY be overridden by backend module for the sake of
|
97
|
+
# efficiency, or will call #read_member for each entry in member_paths.
|
98
|
+
def read_members(member_paths, options={})
|
99
|
+
member_paths.map{|f| read_member(f, options) }
|
100
|
+
end
|
101
|
+
|
102
|
+
# Intended to write the contents of a zip member to a filesystem path.
|
103
|
+
#
|
104
|
+
# This SHOULD be overridden by a backend module because this default
|
105
|
+
# will try to read the whole file in to memory before outputting to disk
|
106
|
+
# and that can be memory-intensive if the file is large.
|
107
|
+
def extract_member(member_path, destination_path, options={})
|
108
|
+
warn "Using default #extract_member which may be memory-inefficient"
|
109
|
+
default_extract_member(member_path, destination_path, options)
|
110
|
+
end
|
111
|
+
|
112
|
+
# List members of the zip file. Optionally can specify a prefix.
|
113
|
+
#
|
114
|
+
# This method MUST be overridden by a backend module.
|
115
|
+
def list_members(prefix = nil, options={})
|
116
|
+
raise NotImplementedError
|
117
|
+
end
|
118
|
+
|
119
|
+
# Boolean, does a given member path exist in the zip file?
|
120
|
+
#
|
121
|
+
# This method MAY be overridden by backend module for the sake of
|
122
|
+
# efficiency. Otherwise it will use #list_members.
|
123
|
+
def member_exists?(member_path, options={})
|
124
|
+
list_members(nil, options).include?(member_path)
|
125
|
+
end
|
126
|
+
|
127
|
+
# Write string contents to a zip member file
|
128
|
+
def write_member(member_path, member_content, options={})
|
129
|
+
raise NotImplementedError
|
130
|
+
end
|
131
|
+
|
132
|
+
# Remove a zip member from the archive.
|
133
|
+
# Expected to raise MemberNotFoundError if the member_path was not found in
|
134
|
+
# the archive
|
135
|
+
#
|
136
|
+
# This method MUST be overridden by a backend module.
|
137
|
+
def remove_member(member_path, options={})
|
138
|
+
raise NotImplementedError
|
139
|
+
end
|
140
|
+
|
141
|
+
# Remove multiple zip member from the archive.
|
142
|
+
# Expected to raise MemberNotFoundError if the member_path was not found in
|
143
|
+
# the archive
|
144
|
+
#
|
145
|
+
# This method MAY be overridden by backend module for the sake of
|
146
|
+
# efficiency. Otherwise it will use #remove_member.
|
147
|
+
def remove_members(member_paths, options={})
|
148
|
+
member_paths.map{|f| remove_member(f, options) }.all?
|
149
|
+
end
|
150
|
+
|
151
|
+
private
|
152
|
+
|
153
|
+
# Convenience method that will raise ArchiveNotFoundError if the archive
|
154
|
+
# doesn't exist or if the archive path given points to something other than
|
155
|
+
# a file.
|
156
|
+
def archive_exists!
|
157
|
+
unless File.file?(@filename)
|
158
|
+
raise ArchiveNotFoundError.new(@filename)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# Convenience method that will raise MemberNotFoundError if the member doesn't exist.
|
163
|
+
# Uses #member_exists? in whatever form (default or custom).
|
164
|
+
def exists!(member_path)
|
165
|
+
unless member_exists?(member_path)
|
166
|
+
member_not_found!(member_path)
|
167
|
+
end
|
168
|
+
true
|
169
|
+
end
|
170
|
+
|
171
|
+
# Raises MemberNotFoundError
|
172
|
+
def member_not_found!(member_path)
|
173
|
+
raise MemberNotFoundError.new(member_path)
|
174
|
+
end
|
175
|
+
|
176
|
+
def default_extract_member(member_path, destination_path, options={})
|
177
|
+
output_file = ::File.new(destination_path, 'wb')
|
178
|
+
output_file.write(read_member(member_path, options))
|
179
|
+
output_file.close
|
180
|
+
destination_path
|
181
|
+
end
|
182
|
+
|
183
|
+
def default_backend
|
184
|
+
BACKEND_PREFERENCE.each do |name|
|
185
|
+
be = BACKENDS[name]
|
186
|
+
if be[:fingerprints].all?{|expectation, lmb| lmb.call == expectation }
|
187
|
+
return name
|
188
|
+
end
|
189
|
+
end
|
190
|
+
raise NoSupportedBackendError, "No supported backend found: #{BACKEND_PREFERENCE.join(', ')}"
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
require "multi_zip/version"
|
195
|
+
require "multi_zip/errors"
|
data/multi_zip.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'multi_zip/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "multi_zip"
|
8
|
+
spec.version = MultiZip::VERSION
|
9
|
+
spec.authors = ["Matthew Nielsen"]
|
10
|
+
spec.email = ["xunker@pyxidis.org"]
|
11
|
+
spec.summary = %q{Abstracts zipping and unzipping using whatever gems are installed, automatically.}
|
12
|
+
spec.description = %q{Abstracts zipping and unzipping using whatever gems are installed using one consistent API. Provides swappable zipping/unzipping backends so you're not tied to just one gem. Supports rubyzip, archive-zip, zipruby and others.}
|
13
|
+
spec.homepage = "https://github.com/xunker/multi_zip"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler"
|
22
|
+
spec.add_development_dependency "rake"
|
23
|
+
spec.add_development_dependency "rspec", "~> 3.1.0"
|
24
|
+
end
|