multi_zip 0.1.4 → 0.1.6

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 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