multi_zip 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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