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.
@@ -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
@@ -0,0 +1,3 @@
1
+ class MultiZip
2
+ VERSION = "0.1.3"
3
+ 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