multi_zip 0.1.4 → 0.1.6

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b4d9becd80958a0eb07dab22a0207dfcb91ebe00
4
- data.tar.gz: eb1092f782ab4fc2e6933eb5a36b84036bf9cfca
3
+ metadata.gz: 6ae79eb36822d59d1b6c74fbc01954469af7affc
4
+ data.tar.gz: e907a498f8b2f26bf0c5c09b79604e1ae3c55d5f
5
5
  SHA512:
6
- metadata.gz: 9a6eca1072c53f5266e9e8afdee331b7df518df68ebceb5a10b9120fc9041e3710552fdcd557eba80ed51ec95692864b2724cf11f64afa0f5986fc1883412e7d
7
- data.tar.gz: e90c64a98a10a98dcedda42f190d1551a507f83166d3ea5612f0996e6a47e8d7b9a8d087998caeee975d8b146c3984bdf3e2f848fe5d9723549bf5ca2e43c7f2
6
+ metadata.gz: fffb4dc3201100b24d680b0e7f1b34001f30ea653c286b378018c037ce99b66bc4628d57c7afed364a27f8318c2b0ac4b99fe2accd2d1b453ac10bfbe8d8b6ef
7
+ data.tar.gz: e02558c21895d05e01db7222d83dcf6be10067e074618b4bd7eebd1a90d24ec7927041bd6624c4e5507ad459ff519c48a2ee10a5e1052383c2ea3aa2ba400ab6
data/.gitignore CHANGED
@@ -15,3 +15,5 @@ mkmf.log
15
15
  .ruby-*
16
16
  *.gem
17
17
  test_in_all_rubies.sh
18
+ .byebug_history
19
+ destination
data/.travis.yml CHANGED
@@ -9,6 +9,7 @@ rvm:
9
9
  - 2.3.0
10
10
  - jruby-18mode
11
11
  - jruby-19mode
12
+ - jruby-9.0.5.0
12
13
  - rbx-2
13
14
  matrix:
14
15
  allow_failures:
data/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ ### 0.1.6 - Dec 20, 2017
2
+
3
+ Added '#member_info` method for retrieving file size and information.
4
+
5
+ ### 0.1.5 - May 4, 2016
6
+
7
+ Add experimental support for using command-line utilities to zip and unzip.
data/Gemfile CHANGED
@@ -3,7 +3,7 @@ source 'https://rubygems.org'
3
3
  gemspec
4
4
 
5
5
  # can't use pry on old rubies or on rubinius
6
- if RUBY_VERSION.to_f >= 2.0 && RUBY_ENGINE == 'ruby'
6
+ if RUBY_VERSION.to_f >= 2.2 && RUBY_ENGINE == 'ruby'
7
7
  gem 'byebug', :require => false
8
8
  gem 'guard-rspec', :require => false
9
9
  end
data/README.md CHANGED
@@ -7,7 +7,7 @@ interface regardless of which is being used. This allows for code that is more
7
7
  portable and helps to avoid namespace collisions (zipruby vs. rubyzip for example)
8
8
  and other implementation restrictions (MRI vs. Jruby, Unix vs. Windows, etc,).
9
9
 
10
- It currently supports `.zip` archives only. See TODO for info on others.
10
+ It currently supports `.zip` archives only.
11
11
 
12
12
  MultiZip provides a very small and focused set of functions:
13
13
 
@@ -17,8 +17,6 @@ MultiZip provides a very small and focused set of functions:
17
17
  * Extract files from an archive to a local file.
18
18
  * List files contained in an archive.
19
19
  * Delete files from an archive.
20
- * Get information for a file in an archive. (Pending TODO)
21
- * Add a local file to an archive. (Pending TODO).
22
20
 
23
21
  It is meant for most common zip/unzip tasks. For anything more
24
22
  complicated than these basics, you should use a specific (un)zipping library
@@ -26,16 +24,16 @@ instead.
26
24
 
27
25
  Rubies supported (see [CI status](https://travis-ci.org/xunker/multi_zip) for more detail):
28
26
  * MRI 2.x.x, 1.9.3, 1.8.7 and REE.
29
- * Jruby
27
+ * Jruby (1.8 and 1.9 mode)
30
28
  * Rubinius 2
31
29
 
32
30
  For information about which backend gems work in which ruby, see [Supported Backend Gems](#supported-backend-gems).
33
31
 
34
- This work is inspired by [multi_json](https://github.com/intridea/multi_json)
35
- and [multi_xml](https://github.com/sferik/multi_xml). Work began while I was
36
- visiting Japan in 2014 and is dedicated to the Rubyists of
37
- [Asukusa.rb](https://asakusarb.doorkeeper.jp/) and
38
- [John Mettraux](https://twitter.com/jmettraux).
32
+ This gem was inspired by [multi_json](https://github.com/intridea/multi_json)
33
+ and [multi_xml](https://github.com/sferik/multi_xml), and is dedicated to the
34
+ Rubyists of [Asukusa.rb](https://asakusarb.doorkeeper.jp/) and
35
+ [John Mettraux](https://twitter.com/jmettraux) of Hiroshima.
36
+ おもてなしのあなたはありがとう!
39
37
 
40
38
  ## Installation
41
39
 
@@ -50,12 +48,14 @@ which ones can be used.
50
48
 
51
49
  `multi_zip` will try to use the available gem backends in the following order:
52
50
 
53
- * rubyzip
54
- * archive/zip
55
- * zipruby
51
+ * [rubyzip](https://rubygems.org/gems/rubyzip)
52
+ * [archive-zip](https://rubygems.org/gems/archive-zip)
53
+ * [zipruby](https://rubygems.org/gems/zipruby)
56
54
 
57
- If no usable backends are loaded a `MultiZip::NoSupportedBackendError` will be
58
- raised for any operation.
55
+ If no usable gems are found, it will then look for a compatible `zip`/ `unzip`
56
+ program in your path and will try to use that instead of a gem. If no
57
+ compatible gems or program can be found, a `MultiZip::NoSupportedBackendError`
58
+ exception will be raised.
59
59
 
60
60
  If you have multiple gems available and want to choose your backend, you can
61
61
  do that in the initializer:
@@ -112,6 +112,38 @@ zip.list_members
112
112
  # ]
113
113
  ```
114
114
 
115
+ #### Check if a file exists in an archive
116
+
117
+ Returns `true` if the file exists in the archive, otherwise `false`
118
+
119
+ ```ruby
120
+ zip.member_exists?('/path/inside/archive/to/file.txt')
121
+ # => true
122
+
123
+ zip.member_exists?('/doesnt_exist')
124
+ # => false
125
+ ```
126
+
127
+ #### Get member information (file size, etc)
128
+
129
+ Returns a hash if the file exists in the archive, otherwise will raise exception.
130
+
131
+ ```ruby
132
+ zip.member_info('/path/inside/archive/to/file.txt')
133
+ # => { :path => '/path/inside/archive/to/file.txt', :size => 14323, type: :file }
134
+ ```
135
+
136
+ At a minimum, the returned hash will contain:
137
+ :path the full path of the member file (should equal `member_path` arg)
138
+ :size the UNCOMPRESSED file size, in bytes
139
+
140
+ Optionally, it MAY contain these keys:
141
+ :type Filesystem type of the member (:directory, :file, :symlink, etc)
142
+ :created_at creation timestamp of the file as an instance of Time
143
+ :compressed_size size of the COMPRESSED file in bytes
144
+ :original The original member info object as returned from the backend
145
+ gem. Ex: using rubyzip, it would be an instace of Zip::Entry.
146
+
115
147
  #### Read file from zip archive
116
148
 
117
149
  ```ruby
@@ -186,8 +218,7 @@ end
186
218
 
187
219
  Planned for the future:
188
220
 
189
- * [archive](https://rubygems.org/gems/archive)
190
- * [unix_utils](https://rubygems.org/gems/unix_utils)
221
+ * [archive gem](https://rubygems.org/gems/archive)
191
222
  * Other archive formats like gzip, bzip2, 7zip, tar, etc.
192
223
  * Jruby-specific gems
193
224
  * Others (please suggest them in a [new issue](https://github.com/xunker/multi_zip/issues/new))
data/TODO.md CHANGED
@@ -5,12 +5,10 @@
5
5
  * #add_member: add file to archive from filesystem (new method).
6
6
  * Document exceptions raised, what they mean and how to use them.
7
7
  * Add inline docs for methods.
8
- * support *nix zip(1L)/unzip(1L) without needing backend gem (with warning).
9
8
 
10
9
  #### Other things that need to be done, in no particular order:
11
10
 
12
11
  * Add support for more backends.
13
- * Support for backend gems to process other formats (gzip, bzip2, 7zip, tar, etc).
14
12
  * Keep the backend archive open between method calls.
15
13
  * Add soak tests for memory usage once archived are kept open.
16
14
  * Ensure #close is executed when MultiZip instance goes out of scope.
@@ -39,8 +37,9 @@
39
37
  #### Things that I'd **like** to do, but won't.
40
38
 
41
39
  These are thinks that would be really nice to have but that are probably not
42
- realistic because they cannot be abstracted across all backend gems:
40
+ realistic because they cannot be abstracted across all backend gems and systems:
43
41
 
42
+ * Support for backend gems to process other formats (gzip, bzip2, 7zip, tar, etc).
44
43
  * Ability to set location and compression format and level of archive.
45
44
  * Ability to set compression format and level of individual members (for Epub compatibility).
46
45
  * Ability to set archive location of individual members (for Epub compatibility).
data/lib/multi_zip.rb CHANGED
@@ -3,7 +3,7 @@ class MultiZip
3
3
 
4
4
  attr_reader :filename
5
5
 
6
- BACKEND_PREFERENCE = [ :rubyzip, :archive_zip, :zipruby ]
6
+ BACKEND_PREFERENCE = [ :rubyzip, :archive_zip, :zipruby, :cli ]
7
7
  BACKENDS = {
8
8
  :rubyzip => {
9
9
  :fingerprints => [
@@ -24,6 +24,12 @@ class MultiZip
24
24
  ['constant', lambda { defined?(Zip::Archive) }]
25
25
  ],
26
26
  :constant => lambda { MultiZip::Backend::Zipruby }
27
+ },
28
+ :cli => {
29
+ :fingerprints => [
30
+ [true, lambda { MultiZip::Backend::Cli.strategy_available? } ]
31
+ ],
32
+ :constant => lambda { MultiZip::Backend::Cli.strategy.extend_class.call }
27
33
  }
28
34
  }
29
35
 
@@ -33,7 +39,15 @@ class MultiZip
33
39
  self.backend = if b_end = options.delete(:backend)
34
40
  b_end
35
41
  else
36
- default_backend
42
+ begin
43
+ default_backend
44
+ rescue NoSupportedBackendError => e
45
+ if !!options[:allow_no_backends] # TODO: Fix this test mode hack
46
+ nil
47
+ else
48
+ raise e
49
+ end
50
+ end
37
51
  end
38
52
 
39
53
  if block_given?
@@ -46,7 +60,7 @@ class MultiZip
46
60
  end
47
61
 
48
62
  def backend=(backend_name)
49
- return if backend_name.nil?
63
+ return @backend if backend_name.nil?
50
64
  if BACKENDS.keys.include?(backend_name.to_sym)
51
65
  @backend = backend_name.to_sym
52
66
  require "multi_zip/backend/#{@backend}"
@@ -124,6 +138,23 @@ class MultiZip
124
138
  list_members(nil, options).include?(member_path)
125
139
  end
126
140
 
141
+ # Returns a hash of information about the member file. At a minimum, the hash
142
+ # MUST contain these keys:
143
+ # :path the full path of the member file (should equal `member_path` arg)
144
+ # :size the UNCOMPRESSED file size, in bytes
145
+ #
146
+ # Optionally, it MAY contain these keys:
147
+ # :created_at creation timestamp of the file as an instance of Time
148
+ # :compressed_size size of the COMPRESSED file
149
+ # :type Filesystem type of the member (:directory, :file, :symlink, etc)
150
+ # :original The original member info object as returned from the backend
151
+ # gem. Ex: using rubyzip, it would be an instace of Zip::Entry.
152
+ #
153
+ # This method MUST be overridden by a backend module.
154
+ def member_info(member_path, options={})
155
+ raise NotImplementedError
156
+ end
157
+
127
158
  # Write string contents to a zip member file
128
159
  def write_member(member_path, member_content, options={})
129
160
  raise NotImplementedError
@@ -191,5 +222,6 @@ private
191
222
  end
192
223
  end
193
224
 
225
+ require "multi_zip/backend/cli"
194
226
  require "multi_zip/version"
195
227
  require "multi_zip/errors"
@@ -29,7 +29,7 @@ module MultiZip::Backend::ArchiveZip
29
29
  tempfile = Tempfile.new('multizip_member')
30
30
  tempfile.write(member_contents)
31
31
  tempfile.close
32
-
32
+
33
33
  zip = Archive::Zip.new(@filename, :w)
34
34
  new_entry = Archive::Zip::Entry.from_file(tempfile.path, :zip_path => member_path)
35
35
  zip.add_entry(new_entry)
@@ -47,7 +47,7 @@ module MultiZip::Backend::ArchiveZip
47
47
  end
48
48
  end
49
49
 
50
- def extract_member(member_path, destination_path, options = {})
50
+ def extract_member(member_path, destination_path, options = {})
51
51
  archive_operation do |zip|
52
52
  member = zip.find{|m| m.zip_path == member_path}
53
53
  if member && member.file?
@@ -73,18 +73,18 @@ module MultiZip::Backend::ArchiveZip
73
73
  member_paths.each do |member_path|
74
74
  exists!(member_path)
75
75
  end
76
-
76
+
77
77
  # archive-zip doesn't have the #remove_entry method any more, so we do
78
78
  # this in a really slow way: we dump the entire dir to the filesystem,
79
79
  # delete `member_path` and zip the whole thing up again.
80
-
80
+
81
81
  Dir.mktmpdir do |tmp_dir|
82
82
  Archive::Zip.extract(@filename, tmp_dir)
83
83
 
84
84
  member_paths.each do |member_path|
85
85
  FileUtils.rm("#{tmp_dir}/#{member_path}")
86
86
  end
87
-
87
+
88
88
  # create a tempfile and immediately delete it, we just want the name.
89
89
  tempfile = Tempfile.new(['multizip_temp', '.zip'])
90
90
  tempfile_path = tempfile.path
@@ -98,6 +98,23 @@ module MultiZip::Backend::ArchiveZip
98
98
  true
99
99
  end
100
100
 
101
+ def member_info(member_path, options = {})
102
+ archive_operation do |zip|
103
+ member = zip.find{|m| m.zip_path == member_path}
104
+ if member
105
+ return {
106
+ path: member.zip_path,
107
+ size: member.expected_data_descriptor.uncompressed_size.to_i,
108
+ type: member.ftype,
109
+ original: member
110
+ }
111
+ else
112
+ zip.close
113
+ member_not_found!(member_path)
114
+ end
115
+ end
116
+ end
117
+
101
118
  private
102
119
 
103
120
  def archive_operation(mode = :r) # mode is either :r or :w
@@ -108,4 +125,4 @@ private
108
125
  rescue Archive::Zip::UnzipError => e
109
126
  raise MultiZip::InvalidArchiveError.new(@filename, e)
110
127
  end
111
- end
128
+ end
@@ -0,0 +1,68 @@
1
+ # If no suitable gems are found, this is the last-gasp fallback. We use
2
+ # whatever command-line zipping/unzipping tools we can find and that we
3
+ # know how to use.
4
+ #
5
+ # This will likely be a huge work in progress since it involves a lot of
6
+ # detection. We'll need to detect the host OS and version, the tools available
7
+ # in the path (or specified by the user, optionally) and the command-line
8
+ # arguments to be used for those tools.
9
+ #
10
+ # OS Unzip Zip
11
+ #------------------------------------------------------------------------------
12
+ # OS X 10.10 Info-ZIP UnZip 5.52 Info-ZIP Zip 3.0
13
+ # RHEL-Based Info-ZIP UnZip 5.xx to 6.xx Info-ZIP Zip 2.x to 3.x
14
+ # Ubuntu Info-ZIP UnZip 6.xx Info-ZIP Zip 3.0
15
+ # Raspbian Info-ZIP UnZip 6.00 Info-ZIP Zip 3.0
16
+ # Windows
17
+ # GNU UnZip (from INFO-Zip UnZip) GNU Zip (from INFO-Zip Zip)
18
+ # 7-Zip Command Line Version 7-Zip Command Line Version
19
+ # PKWare pkunzip PKWare pkzip
20
+ # WinZip CLI WinZip CLI
21
+ # WinRAR CLI WinRAR CLI
22
+ #
23
+ # First, check for the programs that would be installed by default.
24
+ # If none can be found, raise an error and tell the user how they can resolve
25
+ # the problem: Either tell them to install a different backend gem (preferred)
26
+ # or tell them how to install a supported CLI program (depending on OS).
27
+ #
28
+ # When using this shell method, always emit a warning that it is very
29
+ # inefficient and should not never no-way no-how no-where be used in
30
+ # production environments.
31
+
32
+ module MultiZip::Backend::Cli
33
+ STRATEGY_MODULES = [
34
+ [ :info_zip, lambda { InfoZip }]
35
+ ]
36
+
37
+ def self.extended(mod)
38
+ if strategy_available?
39
+ require "multi_zip/backend/cli/#{strategy.require_name}"
40
+ extend strategy.extend_class.call
41
+ warn([
42
+ "MultiZip is using the \"#{strategy.human_name}\" command-line program.",
43
+ 'This feature is considered PRE-ALPHA, unstable and inefficient and',
44
+ 'should not be used in production environments.'
45
+ ].join("\n"))
46
+ else
47
+ raise MultiZip::NoSupportedBackendError, "MultiZip::Backend::Cli could find no suitable zipping/unzipping programs in path."
48
+ end
49
+ end
50
+
51
+ def self.strategy_available?
52
+ !!strategy
53
+ end
54
+
55
+ def self.strategy
56
+ @strategy ||= detect_strategy
57
+ end
58
+
59
+ def self.detect_strategy
60
+ STRATEGY_MODULES.detect{|strategy_module_file, strategy_module_constant|
61
+ require "multi_zip/backend/cli/#{strategy_module_file}"
62
+ strategy_module = strategy_module_constant.call
63
+ if strategy_module.available?
64
+ return strategy_module
65
+ end
66
+ }
67
+ end
68
+ end
@@ -0,0 +1,292 @@
1
+ module MultiZip::Backend::Cli
2
+ module InfoZip
3
+ require 'stringio'
4
+ require 'open3'
5
+
6
+ BUFFER_SIZE = 8192
7
+
8
+ # TODO: better way to find full path to programs?
9
+ WHICH_PROGRAM = 'which'
10
+
11
+ ZIP_AND_UNZIP_ARE_SAME_PROGRAM = false
12
+
13
+ ZIP_PROGRAM = 'zip'
14
+ ZIP_PROGRAM_SIGNATURE = /This is Zip [2-3].\d+\s.+, by Info-ZIP/
15
+ ZIP_PROGRAM_SIGNATURE_SWITCH = '-v'
16
+ ZIP_PROGRAM_REMOVE_MEMBER_SWITCH = '-d'
17
+
18
+ UNZIP_PROGRAM ='unzip'
19
+ UNZIP_PROGRAM_SIGNATURE = /UnZip [5-6]\.\d+ of .+, by Info-ZIP/
20
+ UNZIP_PROGRAM_SIGNATURE_SWITCH = '-v'
21
+ UNZIP_PROGRAM_LIST_MEMBERS_SWITCHES = ['-Z', '-1']
22
+ UNZIP_PROGRAM_READ_MEMBER_SWITCH = '-p'
23
+ UNZIP_PROGRAM_MEMBER_INFO_SWITCHES = ['-Z']
24
+
25
+ # TODO: does this change between versions?
26
+ # TODO: does this change with system language?
27
+ UNZIP_PROGRAM_EMPTY_ZIPFILE_MESSAGE = 'Empty zipfile.'
28
+ UNZIP_PROGRAM_MEMBER_NOT_FOUND_MESSAGE = 'caution: filename not matched:'
29
+ UNZIP_PROGRAM_INVALID_FILE_MESSAGE = 'End-of-central-directory signature not found'
30
+
31
+ def self.require_name
32
+ 'info_zip'
33
+ end
34
+
35
+ def self.extend_class
36
+ lambda { MultiZip::Backend::Cli::InfoZip::InstanceMethods }
37
+ end
38
+
39
+ def self.human_name
40
+ 'Info-ZIP - zip(1L)/unzip(1L)'
41
+ end
42
+
43
+ def self.available?
44
+ @available ||= programs_found?
45
+ end
46
+
47
+ def self.programs_found?
48
+ if ZIP_AND_UNZIP_ARE_SAME_PROGRAM
49
+ zip_program_found?
50
+ else
51
+ zip_program_found? && unzip_program_found?
52
+ end
53
+ end
54
+
55
+ def self.zip_program_found?
56
+ zip_program_path = `#{WHICH_PROGRAM} #{ZIP_PROGRAM}`.strip
57
+ return false unless zip_program_path =~ /#{ZIP_PROGRAM}/
58
+ return false unless File.exists?(zip_program_path)
59
+
60
+ spawn([ZIP_PROGRAM, ZIP_PROGRAM_SIGNATURE_SWITCH]).first =~ ZIP_PROGRAM_SIGNATURE
61
+ end
62
+
63
+ def self.unzip_program_found?
64
+ unzip_program_path = `#{WHICH_PROGRAM} #{UNZIP_PROGRAM}`.strip
65
+ return false unless unzip_program_path =~ /#{UNZIP_PROGRAM}/
66
+ return false unless File.exists?(unzip_program_path)
67
+
68
+ spawn([UNZIP_PROGRAM, UNZIP_PROGRAM_SIGNATURE_SWITCH]).first =~ UNZIP_PROGRAM_SIGNATURE
69
+ end
70
+
71
+ # Blatant copy from https://github.com/seamusabshere/unix_utils/blob/master/lib/unix_utils.rb
72
+ def self.spawn(argv, options = {}) # :nodoc:
73
+ input = if (read_from = options[:read_from])
74
+ if RUBY_DESCRIPTION =~ /jruby 1.7.0/
75
+ raise "MultiZip: Can't use `#{argv.first}` since JRuby 1.7.0 has a broken IO implementation!"
76
+ end
77
+ File.open(read_from, 'r')
78
+ end
79
+ output = if (write_to = options[:write_to])
80
+ output_redirected = true
81
+ File.open(write_to, 'wb')
82
+ else
83
+ output_redirected = false
84
+ StringIO.new
85
+ end
86
+ error = StringIO.new
87
+ if (chdir = options[:chdir])
88
+ Dir.chdir(chdir) do
89
+ _spawn argv, input, output, error
90
+ end
91
+ else
92
+ _spawn argv, input, output, error
93
+ end
94
+ error.rewind
95
+ whole_error = error.read
96
+ unless whole_error.empty?
97
+ $stderr.puts "MultiZip: `#{argv.join(' ')}` STDERR:"
98
+ $stderr.puts whole_error
99
+ end
100
+ unless output_redirected
101
+ output.rewind
102
+ [output.read, whole_error].map{|o| o.empty? ? nil : o}
103
+ end
104
+ ensure
105
+ [input, output, error].each { |io| io.close if io and not io.closed? }
106
+ end
107
+
108
+ # Blatant copy from https://github.com/seamusabshere/unix_utils/blob/master/lib/unix_utils.rb
109
+ def self._spawn(argv, input, output, error)
110
+ # lifted from posix-spawn
111
+ # https://github.com/rtomayko/posix-spawn/blob/master/lib/posix/spawn/child.rb
112
+ Open3.popen3(*argv) do |stdin, stdout, stderr|
113
+ readers = [stdout, stderr]
114
+ if RUBY_DESCRIPTION =~ /jruby 1.7.0/
115
+ readers.delete stderr
116
+ end
117
+ writers = if input
118
+ [stdin]
119
+ else
120
+ stdin.close
121
+ []
122
+ end
123
+ while readers.any? or writers.any?
124
+ ready = IO.select(readers, writers, readers + writers)
125
+ # write to stdin stream
126
+ ready[1].each do |fd|
127
+ begin
128
+ boom = nil
129
+ size = fd.write input.read(BUFFER_SIZE)
130
+ rescue Errno::EPIPE => boom
131
+ rescue Errno::EAGAIN, Errno::EINTR
132
+ end
133
+ if boom || size < BUFFER_SIZE
134
+ stdin.close
135
+ input.close
136
+ writers.delete stdin
137
+ end
138
+ end
139
+ # read from stdout and stderr streams
140
+ ready[0].each do |fd|
141
+ buf = (fd == stdout) ? output : error
142
+ if fd.eof?
143
+ readers.delete fd
144
+ fd.close
145
+ else
146
+ begin
147
+ # buf << fd.gets(BUFFER_SIZE) # maybe?
148
+ buf << fd.readpartial(BUFFER_SIZE)
149
+ rescue Errno::EAGAIN, Errno::EINTR
150
+ end
151
+ end
152
+ end
153
+ end
154
+ # thanks @tmm1 and @rtomayko for showing how it's done!
155
+ end
156
+ end
157
+
158
+ module InstanceMethods
159
+ def list_members(prefix = nil, options={})
160
+ archive_exists!
161
+ response = MultiZip::Backend::Cli::InfoZip.spawn([
162
+ UNZIP_PROGRAM, UNZIP_PROGRAM_LIST_MEMBERS_SWITCHES, @filename
163
+ ].flatten)
164
+
165
+ return [] if response.first.to_s =~ /^#{UNZIP_PROGRAM_EMPTY_ZIPFILE_MESSAGE}/
166
+
167
+ if response.first
168
+ member_list = response.first.split("\n").sort
169
+ member_list = member_list.select{|m| m =~ /^#{prefix}/} if prefix
170
+ return member_list
171
+ else # error, response.last should contain error message
172
+ raise_info_zip_error!(response.last)
173
+ end
174
+ end
175
+
176
+ def read_member(member_path, options={})
177
+ archive_exists!
178
+ member_not_found!(member_path) if member_path =~ /\/$/
179
+ response = MultiZip::Backend::Cli::InfoZip.spawn([
180
+ UNZIP_PROGRAM, UNZIP_PROGRAM_READ_MEMBER_SWITCH, @filename, member_path
181
+ ].flatten)
182
+
183
+ return response.first if response.first
184
+
185
+ raise_info_zip_error!(response.last, :member_path => member_path)
186
+ end
187
+
188
+ def write_member(member_path, member_content, options={})
189
+ Dir.mktmpdir do |tempdir|
190
+ member_file = File.new("#{tempdir}/#{member_path}", 'wb')
191
+ member_file.print member_content
192
+ member_file.close
193
+
194
+ cwd = Dir.pwd
195
+ Dir.chdir(tempdir)
196
+
197
+ response = MultiZip::Backend::Cli::InfoZip.spawn([
198
+ ZIP_PROGRAM, @filename, member_path
199
+ ])
200
+
201
+ Dir.chdir(cwd)
202
+ end
203
+ true
204
+ end
205
+
206
+ def remove_member(member_path, options={})
207
+ archive_exists!
208
+ raise MultiZip::MemberNotFoundError.new(member_path) unless member_exists?(member_path)
209
+
210
+ response = MultiZip::Backend::Cli::InfoZip.spawn([
211
+ ZIP_PROGRAM, ZIP_PROGRAM_REMOVE_MEMBER_SWITCH, @filename, member_path
212
+ ].flatten)
213
+
214
+ return response.first if response.first
215
+
216
+ raise_info_zip_error!(response.last, :member_path => member_path)
217
+ end
218
+
219
+ def member_info(member_path, options={})
220
+ archive_exists!
221
+
222
+ response = MultiZip::Backend::Cli::InfoZip.spawn([
223
+ UNZIP_PROGRAM, UNZIP_PROGRAM_MEMBER_INFO_SWITCHES, @filename, member_path
224
+ ].flatten).compact
225
+
226
+ if response.join =~ /#{UNZIP_PROGRAM_INVALID_FILE_MESSAGE}/
227
+ raise_info_zip_error!(response.join)
228
+ end
229
+
230
+ # example line:
231
+ # -rwx------ 2.1 unx 558 bX defN 15-Jun-22 17:53 ROBO3DR1PLUSV1/BlinkM.cpp
232
+
233
+ line = response.detect{|r| r.strip.match(/#{member_path}$/)}
234
+
235
+ raise MultiZip::MemberNotFoundError.new(member_path) if line.nil? || line =~ /#{UNZIP_PROGRAM_MEMBER_NOT_FOUND_MESSAGE}/i
236
+
237
+ fields = line.split(/\s+/)
238
+
239
+ path = fields.last
240
+ unless path == member_path
241
+ raise MultiZip::Backend::Cli::InfoZip::ResponseError, "Unexpected file name format or position: #{line.inspect}"
242
+ end
243
+
244
+ size = fields[3]
245
+ unless size =~ /\d+/
246
+ raise MultiZip::Backend::Cli::InfoZip::ResponseError, "Unexpected file size format or position: #{line.inspect}"
247
+ end
248
+
249
+ type = case fields[0].slice(0,1)
250
+ when 'd'
251
+ :directory
252
+ when 'l'
253
+ :symlink
254
+ when '-'
255
+ :file
256
+ else
257
+ raise MultiZip::Backend::Cli::InfoZip::ResponseError, "Unexpected file type field format or position: #{line.inspect}"
258
+ end
259
+
260
+ {
261
+ path: fields.last,
262
+ size: size.to_i,
263
+ type: type,
264
+ original: line
265
+ }
266
+
267
+ end
268
+
269
+ def raise_info_zip_error!(message, options={})
270
+ infozip_error = MultiZip::Backend::Cli::InfoZip::ResponseError.new(message)
271
+ case message
272
+ when /#{UNZIP_PROGRAM_INVALID_FILE_MESSAGE}/
273
+ raise MultiZip::InvalidArchiveError.new(@filename, infozip_error)
274
+ when /cannot find or open/
275
+ raise MultiZip::ArchiveNotFoundError.new(@filename, infozip_error)
276
+ when /filename not matched/
277
+ raise MultiZip::MemberNotFoundError.new(options[:member_path])
278
+ else
279
+ raise MultiZip::UnknownError.new(@filename, infozip_error)
280
+ end
281
+ end
282
+ end
283
+
284
+
285
+ class ResponseError < MultiZip::InvalidArchiveError
286
+ attr_reader :message
287
+ def initialize(error_message)
288
+ @message = error_message
289
+ end
290
+ end
291
+ end
292
+ end
@@ -55,6 +55,21 @@ module MultiZip::Backend::Rubyzip
55
55
  end
56
56
  end
57
57
 
58
+ def member_info(member_path, options = {})
59
+ read_operation do |zip_file|
60
+ if zip_entry = zip_file.detect{|ze| ze.name == member_path}
61
+ {
62
+ path: zip_entry.name,
63
+ size: zip_entry.size.to_i,
64
+ type: zip_entry.ftype,
65
+ original: zip_entry
66
+ }
67
+ else
68
+ member_not_found!(member_path)
69
+ end
70
+ end
71
+ end
72
+
58
73
  private
59
74
 
60
75
  def read_operation(&blk)
@@ -70,4 +85,4 @@ private
70
85
  raise
71
86
  end
72
87
  end
73
- end
88
+ end
@@ -58,6 +58,25 @@ module MultiZip::Backend::Zipruby
58
58
  true
59
59
  end
60
60
 
61
+ def member_info(member_path, options = {})
62
+ read_operation do |zip|
63
+ member_location = zip.locate_name(member_path)
64
+ if member_location == -1
65
+ zip.close
66
+ member_not_found!(member_path)
67
+ end
68
+
69
+ member_stats = zip.get_stat(member_location)
70
+
71
+ {
72
+ path: member_stats.name,
73
+ size: member_stats.size.to_i,
74
+ type: member_stats.directory? ? :directory : :file,
75
+ original: member_stats
76
+ }
77
+ end
78
+ end
79
+
61
80
  private
62
81
 
63
82
  # NOTE: Zip::Archive#locate_name return values
@@ -91,4 +110,4 @@ private
91
110
  raise
92
111
  end
93
112
  end
94
- end
113
+ end
@@ -36,6 +36,8 @@ class MultiZip
36
36
  end
37
37
  end
38
38
 
39
+ class UnknownError < ArchiveError; end
40
+
39
41
  class InvalidArchiveError < ArchiveError; end
40
42
  class ArchiveNotFoundError < ArchiveError
41
43
  def message
@@ -1,3 +1,3 @@
1
1
  class MultiZip
2
- VERSION = "0.1.4"
2
+ VERSION = "0.1.6"
3
3
  end
@@ -200,6 +200,55 @@ shared_examples 'zip backend' do |backend_name|
200
200
  end
201
201
  end
202
202
 
203
+ describe '#member_info' do
204
+ context 'member found' do
205
+ archive_member_files.each do |member_file|
206
+ it "returns hash of member information" do
207
+ info = subject.member_info(member_file)
208
+ expect(info).to include(
209
+ {
210
+ path: member_file,
211
+ size: archive_member_size(member_file),
212
+ type: :file
213
+ }
214
+ )
215
+
216
+ expect(info[:original]).to_not be_nil
217
+ end
218
+ end
219
+ end
220
+
221
+ context 'member not found' do
222
+ it_behaves_like 'raises MemberNotFoundError', :member_info, 'doesnt_exist'
223
+ end
224
+
225
+ context 'member is a directory' do
226
+ it "returns hash of member information" do
227
+ expect(
228
+ subject.member_info(archive_member_directories.first)
229
+ ).to include(
230
+ {
231
+ path: archive_member_directories.first,
232
+ size: archive_member_size(archive_member_directories.first).to_i,
233
+ type: :directory
234
+ }
235
+ )
236
+ end
237
+ end
238
+
239
+ it_behaves_like 'archive not found, raises ArchiveNotFoundError', :member_info, archive_member_files.first
240
+
241
+ it_behaves_like 'archive is not a file, raises ArchiveNotFoundError', :member_info, archive_member_files.first
242
+
243
+ context 'archive cannot be accessed due to permissions' do
244
+ it 'raises ArchiveNotAccessibleError'
245
+ end
246
+
247
+ context 'invalid or unreadable archive' do
248
+ it_behaves_like 'raises InvalidArchiveError', :member_info, archive_member_files.first
249
+ end
250
+ end
251
+
203
252
  describe '#write_member' do
204
253
  after { FileUtils.rm(filename) if File.exists?(filename) }
205
254
 
@@ -318,35 +367,51 @@ shared_examples 'zip backend' do |backend_name|
318
367
  context 'archive exists' do
319
368
  before do
320
369
  FileUtils.cp(filename, temp_filename)
321
- expect(
322
- MultiZip.new(temp_filename).member_exists?(member_file_name)
323
- ).to be_truthy
324
370
  end
325
371
 
326
- let!(:result) do
327
- subject.remove_member(member_file_name)
328
- end
329
-
330
- context 'member removed successfully' do
331
- it 'returns true' do
332
- expect(result).to be_truthy
333
- end
334
- it 'removes the member from the file' do
372
+ context 'member found in archive' do
373
+ let!(:result) do
335
374
  expect(
336
375
  MultiZip.new(temp_filename).member_exists?(member_file_name)
337
- ).to be_falsey
376
+ ).to be_truthy
377
+
378
+ subject.remove_member(member_file_name)
338
379
  end
339
- it 'does not remove any other members' do
340
- zip = MultiZip.new(temp_filename)
341
- (archive_member_files - [member_file_name]).each do |mfn|
342
- expect(zip.member_exists?(mfn)).to be_truthy
380
+
381
+ context 'member removed successfully' do
382
+ it 'returns true' do
383
+ expect(result).to be_truthy
384
+ end
385
+ it 'removes the member from the file' do
386
+ expect(
387
+ MultiZip.new(temp_filename).member_exists?(member_file_name)
388
+ ).to be_falsey
389
+ end
390
+ it 'does not remove any other members' do
391
+ zip = MultiZip.new(temp_filename)
392
+ (archive_member_files - [member_file_name]).each do |mfn|
393
+ expect(zip.member_exists?(mfn)).to be_truthy
394
+ end
343
395
  end
344
396
  end
397
+
398
+ context 'member not successfully removed' do
399
+ it 'raises MemberNotRemovedError'
400
+ it 'does not remove member from the archive'
401
+ end
345
402
  end
346
403
 
347
- context 'member not successfully added' do
348
- it 'raises MemberNotRemovedError'
349
- it 'does not remove member from the archive'
404
+ context 'member not found in archive' do
405
+ before do
406
+ expect(
407
+ MultiZip.new(temp_filename).member_exists?('doesnt_exist')
408
+ ).to be_falsey
409
+ end
410
+ it 'raises MemberNotFoundError' do
411
+ expect(
412
+ lambda { subject.remove_member('doesnt_exist') }
413
+ ).to raise_error(MultiZip::MemberNotFoundError)
414
+ end
350
415
  end
351
416
  end
352
417
 
@@ -463,4 +528,3 @@ shared_examples 'archive is not a file, raises ArchiveNotFoundError' do |*args|
463
528
  it_behaves_like 'raises ArchiveNotFoundError', *args
464
529
  end
465
530
  end
466
-
@@ -2,5 +2,7 @@ require 'spec_helper'
2
2
  require 'backend_shared_example'
3
3
 
4
4
  RSpec.describe MultiZip do
5
- it_behaves_like 'zip backend', 'archive_zip'
5
+ if test_with_archive_zip?
6
+ it_behaves_like 'zip backend', 'archive_zip'
7
+ end
6
8
  end
@@ -0,0 +1,8 @@
1
+ require 'spec_helper'
2
+ require 'backend_shared_example'
3
+
4
+ RSpec.describe MultiZip do
5
+ if test_with_cli?
6
+ it_behaves_like 'zip backend', 'cli'
7
+ end
8
+ end
@@ -5,4 +5,4 @@ RSpec.describe MultiZip do
5
5
  if test_with_rubyzip?
6
6
  it_behaves_like 'zip backend', 'rubyzip'
7
7
  end
8
- end
8
+ end
@@ -2,7 +2,8 @@ require 'spec_helper'
2
2
 
3
3
  RSpec.describe MultiZip do
4
4
  let(:filename) { archive_fixture_filename }
5
- let(:subject) { MultiZip.new(filename) }
5
+ let(:subject) { MultiZip.new(filename, instance_options) }
6
+ let(:instance_options) { {} }
6
7
 
7
8
  describe '.new' do
8
9
  it 'accepts a block'
@@ -24,10 +25,11 @@ RSpec.describe MultiZip do
24
25
  end
25
26
 
26
27
  context 'unknown backend' do
28
+ let(:instance_options) { { :allow_no_backends => true } }
27
29
  it 'raises exception' do
28
30
  expect(
29
31
  lambda { subject.backend = 'unsupported' }
30
- ).to raise_exception(MultiZip::NoSupportedBackendError)
32
+ ).to raise_exception(MultiZip::InvalidBackendError)
31
33
  end
32
34
  end
33
35
  end
@@ -50,4 +52,4 @@ RSpec.describe MultiZip do
50
52
  end
51
53
  end
52
54
  end
53
- end
55
+ end
data/spec/spec_helper.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  require 'multi_zip'
2
2
 
3
3
  # can't use pry on old rubies or on rubinius
4
- if RUBY_VERSION.to_f >= 2.0 && RUBY_ENGINE == 'ruby'
4
+ if RUBY_VERSION.to_f >= 2.2 && RUBY_ENGINE == 'ruby'
5
5
  require 'byebug'
6
6
  end
7
7
 
@@ -76,28 +76,61 @@ def archive_member_directories
76
76
  end
77
77
 
78
78
  def test_with_rubyzip?
79
- # rubyzip requires ruby >= 1.9.2
80
- if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("1.9.2")
81
- gem 'rubyzip'
82
- return true
79
+ test = if ENV['ONLY']
80
+ ENV['ONLY'] == 'rubyzip'
81
+ else
82
+ true
83
+ end
84
+
85
+ if test
86
+ # rubyzip requires ruby >= 1.9.2
87
+ if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("1.9.2")
88
+ gem 'rubyzip'
89
+ return true
90
+ end
91
+ return false
83
92
  end
84
- false
85
93
  rescue Gem::LoadError
86
94
  false
87
95
  end
88
96
 
89
97
  def test_with_zipruby?
90
- gem 'zipruby'
91
- true
98
+ test = if ENV['ONLY']
99
+ ENV['ONLY'] == 'zipruby'
100
+ else
101
+ true
102
+ end
103
+
104
+ if test
105
+ gem 'zipruby'
106
+ return true
107
+ end
92
108
  rescue Gem::LoadError
93
109
  false
94
110
  end
95
111
 
112
+ def test_with_archive_zip?
113
+ if ENV['ONLY']
114
+ ENV['ONLY'] =~ /archive/
115
+ else
116
+ true
117
+ end
118
+ end
119
+
120
+ def test_with_cli?
121
+ if ENV['ONLY']
122
+ ENV['ONLY'] = 'cli'
123
+ else
124
+ MultiZip::Backend::Cli.strategy_available?
125
+ end
126
+ end
127
+
96
128
  def backends_to_test
97
129
  @backends_to_test ||= [
98
130
  (test_with_zipruby? ? :zipruby : nil),
99
131
  (test_with_rubyzip? ? :rubyzip : nil),
100
- :archive_zip
132
+ (test_with_archive_zip? ? :archive_zip : nil),
133
+ (test_with_cli? ? :cli : nil),
101
134
  ].compact
102
135
  end
103
136
 
@@ -108,8 +141,13 @@ if excluded_backends.length > 0
108
141
  warn "*** Backends that will not be tested: #{excluded_backends.map(&:to_s).join(', ')} ***"
109
142
  end
110
143
 
111
- BACKEND_CONSTANTS = {}
112
- BACKEND_CLASSES = {}
144
+ puts "*** MultiZip::Backend::Cli.strategy_available?: #{MultiZip::Backend::Cli.strategy_available?.inspect}"
145
+ if MultiZip::Backend::Cli.strategy_available?
146
+ puts "*** MultiZip::Backend::Cli.strategy: #{MultiZip::Backend::Cli.strategy.inspect}"
147
+ end
148
+
149
+ BACKEND_CONSTANTS = Hash.new([])
150
+ BACKEND_CLASSES = Hash.new([])
113
151
 
114
152
  def set_backend_class(lib, klass)
115
153
  BACKEND_CONSTANTS[lib.to_sym] = klass.constants
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: multi_zip
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthew Nielsen
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-04-06 00:00:00.000000000 Z
11
+ date: 2017-12-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -65,6 +65,7 @@ files:
65
65
  - ".rspec"
66
66
  - ".travis.yml"
67
67
  - BACKEND_CONSTANTS.md
68
+ - CHANGELOG.md
68
69
  - Gemfile
69
70
  - Guardfile
70
71
  - LICENSE.txt
@@ -73,6 +74,8 @@ files:
73
74
  - TODO.md
74
75
  - lib/multi_zip.rb
75
76
  - lib/multi_zip/backend/archive_zip.rb
77
+ - lib/multi_zip/backend/cli.rb
78
+ - lib/multi_zip/backend/cli/info_zip.rb
76
79
  - lib/multi_zip/backend/rubyzip.rb
77
80
  - lib/multi_zip/backend/zipruby.rb
78
81
  - lib/multi_zip/errors.rb
@@ -84,6 +87,7 @@ files:
84
87
  - spec/fixtures/test.zip
85
88
  - spec/fixtures/test/.gitkeep
86
89
  - spec/lib/multi_zip/backend/archive_zip_spec.rb
90
+ - spec/lib/multi_zip/backend/cli_spec.rb
87
91
  - spec/lib/multi_zip/backend/rubyzip_spec.rb
88
92
  - spec/lib/multi_zip/backend/zipruby_spec.rb
89
93
  - spec/lib/multi_zip_spec.rb
@@ -108,7 +112,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
108
112
  version: '0'
109
113
  requirements: []
110
114
  rubyforge_project:
111
- rubygems_version: 2.2.2
115
+ rubygems_version: 2.4.5.1
112
116
  signing_key:
113
117
  specification_version: 4
114
118
  summary: Abstracts zipping and unzipping using whatever gems are installed, automatically.
@@ -119,6 +123,7 @@ test_files:
119
123
  - spec/fixtures/test.zip
120
124
  - spec/fixtures/test/.gitkeep
121
125
  - spec/lib/multi_zip/backend/archive_zip_spec.rb
126
+ - spec/lib/multi_zip/backend/cli_spec.rb
122
127
  - spec/lib/multi_zip/backend/rubyzip_spec.rb
123
128
  - spec/lib/multi_zip/backend/zipruby_spec.rb
124
129
  - spec/lib/multi_zip_spec.rb