multi_zip 0.1.3
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.
- 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
|