zip_tricks 4.7.3 → 4.8.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.rubocop.yml +4 -1
- data/CHANGELOG.md +30 -0
- data/LICENSE.txt +1 -1
- data/README.md +13 -8
- data/lib/zip_tricks/path_set.rb +148 -0
- data/lib/zip_tricks/size_estimator.rb +3 -2
- data/lib/zip_tricks/stream_crc32.rb +14 -1
- data/lib/zip_tricks/streamer.rb +30 -40
- data/lib/zip_tricks/streamer/deflated_writer.rb +2 -0
- data/lib/zip_tricks/uniquify_filename.rb +38 -0
- data/lib/zip_tricks/version.rb +1 -1
- data/lib/zip_tricks/write_buffer.rb +2 -0
- data/lib/zip_tricks/zip_writer.rb +15 -8
- data/zip_tricks.gemspec +1 -0
- metadata +19 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: a8147080f700510b88cb5e807e61ac71caf94e4b35af7a13162f3fcb0557b221
|
4
|
+
data.tar.gz: 507c345c51f13b2874577497359f49179447b494eacf55f81c6a0b182feeb97a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 328483359ec8a1ced39c3cdc4b853d0fadda7a2c66e1d36e0babc3e701648e57b212b85b5adf6cc24fc92d63eff28889216b11438252f8d1e32d9bdfbb1acaab
|
7
|
+
data.tar.gz: 99d3e5e9a31ab39ae1bbf39128a086373eadb29a44dee76fc4674eb73ad96ed9c190ca50d9d7227b16f6421d01b2603026386d0e16260eec33f417fd68f85e58
|
data/.rubocop.yml
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
inherit_gem:
|
2
2
|
wetransfer_style: ruby/default.yml
|
3
|
+
AllCops:
|
4
|
+
TargetRubyVersion: 2.1
|
3
5
|
Layout/FirstMethodArgumentLineBreak:
|
4
6
|
Enabled: false
|
5
7
|
Layout/FirstMethodParameterLineBreak:
|
@@ -7,4 +9,5 @@ Layout/FirstMethodParameterLineBreak:
|
|
7
9
|
Style/GlobalVars:
|
8
10
|
Exclude:
|
9
11
|
- qa/*.rb
|
10
|
-
- spec/spec_helper.rb
|
12
|
+
- spec/spec_helper.rb
|
13
|
+
- spec/support/zip_inspection.rb
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,33 @@
|
|
1
|
+
## 4.8.3
|
2
|
+
|
3
|
+
* Fix a leak of 1 zlib deflater object per deflated file when writing out compressed files with data descriptors. The deflater
|
4
|
+
needs to be closed explicitly, which we weren't previously doing.
|
5
|
+
|
6
|
+
## 4.8.2
|
7
|
+
|
8
|
+
* Fix extended timestamp timestamp value encoding. Previously we would use an incorrect encoding for the timestamp value, which would output correct but nonsensical timestamps. The pack specifier is now changed to output the correct value.
|
9
|
+
|
10
|
+
## 4.8.1
|
11
|
+
|
12
|
+
* Fix extended timestamp extra field output. The first bit of the flag would be set instead of the last bit of
|
13
|
+
the flag, which made it impossible for Rubyzip to read the timestamp of the entry - and it would also make
|
14
|
+
the extra field useless for most reading applications.
|
15
|
+
|
16
|
+
## 4.8.0
|
17
|
+
|
18
|
+
* Make sure that when directories clobber files and vice versa we raise a clear error. Add `PathSet` which keeps track of entries
|
19
|
+
and all the directories needed to create them, document `PathSet`
|
20
|
+
* Move the `uniquify_filenames` function into a module for easier removal later
|
21
|
+
* Add the `auto_rename_duplicate_filenames` parameter to `Streamer` constructor. We need to make this optional
|
22
|
+
because making filenames unique can be very tricky when subdirectories are involved, and strictly
|
23
|
+
speaking we should not be applying this transformation at all - there should be no output of
|
24
|
+
duplicate filenames by the caller. So making the filenames should be available, but optional.
|
25
|
+
|
26
|
+
## 4.7.4
|
27
|
+
|
28
|
+
* Use a single fixed capacity string in StreamCRC32.from_io to avoid unnecessary allocations
|
29
|
+
* Fix a few tests that were calling out to external binaries
|
30
|
+
|
1
31
|
## 4.7.3
|
2
32
|
|
3
33
|
* Fix RemoteUncap#request_object_size to function correctly
|
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
# zip_tricks
|
2
2
|
|
3
|
-
[![Build Status](https://travis-ci.org/WeTransfer/zip_tricks.svg?branch=
|
4
|
-
[![Gem Version](https://badge.fury.io/rb/zip_tricks.svg)](https://badge.fury.io/rb/zip_tricks)
|
3
|
+
[![Build Status](https://travis-ci.org/WeTransfer/zip_tricks.svg?branch=backports-v4)](https://travis-ci.org/WeTransfer/zip_tricks)
|
5
4
|
|
6
5
|
Allows streaming, non-rewinding ZIP file output from Ruby.
|
7
6
|
|
@@ -30,7 +29,7 @@ The easiest is to use the Rails' built-in streaming feature:
|
|
30
29
|
class ZipsController < ActionController::Base
|
31
30
|
include ActionController::Live # required for streaming
|
32
31
|
include ZipTricks::RailsStreaming
|
33
|
-
|
32
|
+
|
34
33
|
def download
|
35
34
|
zip_tricks_stream do |zip|
|
36
35
|
zip.write_deflated_file('report1.csv') do |sink|
|
@@ -122,10 +121,10 @@ ZipTricks::Streamer.open(io) do | zip |
|
|
122
121
|
# raw_file is written "as is" (STORED mode).
|
123
122
|
# Write the local file header first..
|
124
123
|
zip.add_stored_entry(filename: "first-file.bin", size: raw_file.size, crc32: raw_file_crc32)
|
125
|
-
|
124
|
+
|
126
125
|
# then send the actual file contents bypassing the Streamer interface
|
127
126
|
io.sendfile(my_temp_file)
|
128
|
-
|
127
|
+
|
129
128
|
# ...and then adjust the ZIP offsets within the Streamer
|
130
129
|
zip.simulate_write(my_temp_file.size)
|
131
130
|
end
|
@@ -143,7 +142,7 @@ It is slightly more convenient for the purpose than using the raw Zlib library f
|
|
143
142
|
|
144
143
|
```ruby
|
145
144
|
crc = ZipTricks::StreamCRC32.new
|
146
|
-
crc <<
|
145
|
+
crc << next_chunk_of_data
|
147
146
|
...
|
148
147
|
|
149
148
|
crc.to_i # Returns the actual CRC32 value computed so far
|
@@ -152,6 +151,12 @@ crc.to_i # Returns the actual CRC32 value computed so far
|
|
152
151
|
crc.append(precomputed_crc32, size_of_the_blob_computed_from)
|
153
152
|
```
|
154
153
|
|
154
|
+
You can also compute the CRC32 for an entire IO object if it responds to `#eof?`:
|
155
|
+
|
156
|
+
```ruby
|
157
|
+
crc = ZipTricks::StreamCRC32.from_io(file) # Returns an Integer
|
158
|
+
```
|
159
|
+
|
155
160
|
## Reading ZIP files
|
156
161
|
|
157
162
|
The library contains a reader module, play with it to see what is possible. It is not a complete ZIP reader
|
@@ -160,7 +165,7 @@ as such it performs it's function quite well. Please beware of the security impl
|
|
160
165
|
that have not been formally verified (ours hasn't been).
|
161
166
|
|
162
167
|
## Contributing to zip_tricks
|
163
|
-
|
168
|
+
|
164
169
|
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
|
165
170
|
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
|
166
171
|
* Fork the project.
|
@@ -172,4 +177,4 @@ that have not been formally verified (ours hasn't been).
|
|
172
177
|
|
173
178
|
## Copyright
|
174
179
|
|
175
|
-
Copyright (c)
|
180
|
+
Copyright (c) 2019 WeTransfer. See LICENSE.txt for further details.
|
@@ -0,0 +1,148 @@
|
|
1
|
+
# rubocop:disable Layout/IndentHeredoc
|
2
|
+
|
3
|
+
# A ZIP archive contains a flat list of entries. These entries can implicitly
|
4
|
+
# create directories when the archive is expanded. For example, an entry with
|
5
|
+
# the filename of "some folder/file.docx" will make the unarchiving application
|
6
|
+
# create a directory called "some folder" automatically, and then deposit the
|
7
|
+
# file "file.docx" in that directory. These "implicit" directories can be
|
8
|
+
# arbitrarily nested, and create a tree structure of directories. That structure
|
9
|
+
# however is implicit as the archive contains a flat list.
|
10
|
+
#
|
11
|
+
# This creates opportunities for conflicts. For example, imagine the following
|
12
|
+
# structure:
|
13
|
+
#
|
14
|
+
# * `something/` - specifies an empty directory with the name "something"
|
15
|
+
# * `something` - specifies a file, creates a conflict
|
16
|
+
#
|
17
|
+
# This can be prevented with filename uniqueness checks. It does get funkier however
|
18
|
+
# as the rabbit hole goes down:
|
19
|
+
#
|
20
|
+
# * `dir/subdir/another_subdir/yet_another_subdir/file.bin` - declares a file and directories
|
21
|
+
# * `dir/subdir/another_subdir/yet_another_subdir` - declares a file at one of the levels, creates a conflict
|
22
|
+
#
|
23
|
+
# The results of this ZIP structure aren't very easy to predict as they depend on the
|
24
|
+
# application that opens the archive. For example, BOMArchiveHelper on macOS will expand files
|
25
|
+
# as they are declared in the ZIP, but once a conflict occurs it will fail with "error -21". It
|
26
|
+
# is not very transparent to the user why unarchiving fails, and it has to - and can reliably - only
|
27
|
+
# be prevented when the archive gets created.
|
28
|
+
#
|
29
|
+
# Unfortunately that conflicts with another "magical" feature of ZipTricks which automatically
|
30
|
+
# "fixes" duplicate filenames - filenames (paths) which have already been added to the archive.
|
31
|
+
# This fix is performed by appending (1), then (2) and so forth to the filename so that the
|
32
|
+
# conflict is avoided. This is not possible to apply to directories, because when one of the
|
33
|
+
# path components is reused in multiple filenames it means those entities should end up in
|
34
|
+
# the same directory (subdirectory) once the archive is opened.
|
35
|
+
class ZipTricks::PathSet
|
36
|
+
class Conflict < StandardError
|
37
|
+
end
|
38
|
+
|
39
|
+
class FileClobbersDirectory < Conflict
|
40
|
+
end
|
41
|
+
|
42
|
+
class DirectoryClobbersFile < Conflict
|
43
|
+
end
|
44
|
+
|
45
|
+
def initialize
|
46
|
+
@known_directories = Set.new
|
47
|
+
@known_files = Set.new
|
48
|
+
end
|
49
|
+
|
50
|
+
# Adds a directory path to the set of known paths, including
|
51
|
+
# all the directories that contain it. So, calling
|
52
|
+
# add_directory_path("dir/dir2/dir3")
|
53
|
+
# will add "dir", "dir/dir2", "dir/dir2/dir3".
|
54
|
+
#
|
55
|
+
# @param path[String] the path to the directory to add
|
56
|
+
# @return [void]
|
57
|
+
def add_directory_path(path)
|
58
|
+
path_and_ancestors(path).each do |parent_directory_path|
|
59
|
+
if @known_files.include?(parent_directory_path)
|
60
|
+
# Have to use the old-fashioned heredocs because ZipTricks
|
61
|
+
# aims to be compatible with MRI 2.1+ syntax, and squiggly
|
62
|
+
# heredoc is only available starting 2.3+
|
63
|
+
error_message = <<ERR
|
64
|
+
The path #{parent_directory_path.inspect} which has to be added
|
65
|
+
as a directory is already used for a file.
|
66
|
+
|
67
|
+
The directory at this path would get created implicitly
|
68
|
+
to produce #{path.inspect} during decompresison.
|
69
|
+
|
70
|
+
This would make some archive utilities refuse to open
|
71
|
+
the ZIP.
|
72
|
+
ERR
|
73
|
+
raise DirectoryClobbersFile, error_message
|
74
|
+
end
|
75
|
+
@known_directories << parent_directory_path
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Adds a file path to the set of known paths, including
|
80
|
+
# all the directories that contain it. Once a file has been added,
|
81
|
+
# it is no longer possible to add a directory having the same path
|
82
|
+
# as this would cause conflict.
|
83
|
+
#
|
84
|
+
# The operation also adds all the containing directories for the file, so
|
85
|
+
# add_file_path("dir/dir2/file.doc")
|
86
|
+
# will add "dir" and "dir/dir2" as directories, "dir/dir2/dir3".
|
87
|
+
#
|
88
|
+
# @param file_path[String] the path to the directory to add
|
89
|
+
# @return [void]
|
90
|
+
def add_file_path(file_path)
|
91
|
+
if @known_files.include?(file_path)
|
92
|
+
error_message = <<ERR
|
93
|
+
The file at #{file_path.inspect} has already been included
|
94
|
+
in the archive. Adding it the second time would cause
|
95
|
+
the first file to be overwritten during unarchiving, and
|
96
|
+
could also get the archive flagged as invalid.
|
97
|
+
ERR
|
98
|
+
raise Conflict, error_message
|
99
|
+
end
|
100
|
+
|
101
|
+
if @known_directories.include?(file_path)
|
102
|
+
error_message = <<ERR
|
103
|
+
The path #{file_path.inspect} is already used for
|
104
|
+
a directory, but you are trying to add it as a file.
|
105
|
+
|
106
|
+
This would make some archive utilities refuse
|
107
|
+
to open the ZIP.
|
108
|
+
ERR
|
109
|
+
raise FileClobbersDirectory, error_message
|
110
|
+
end
|
111
|
+
|
112
|
+
# Add all the directories which this file is contained in
|
113
|
+
*dir_components, _file_name = non_empty_path_components(file_path)
|
114
|
+
add_directory_path(dir_components.join('/'))
|
115
|
+
|
116
|
+
# ...and then the file itself
|
117
|
+
@known_files << file_path
|
118
|
+
end
|
119
|
+
|
120
|
+
# Tells whether a specific full path is already known to the PathSet.
|
121
|
+
# Can be a path for a directory or for a file.
|
122
|
+
#
|
123
|
+
# @param path_in_archive[String] the path to check for inclusion
|
124
|
+
# @return [Boolean]
|
125
|
+
def include?(path_in_archive)
|
126
|
+
@known_files.include?(path_in_archive) || @known_directories.include?(path_in_archive)
|
127
|
+
end
|
128
|
+
|
129
|
+
# Clears the contained sets
|
130
|
+
# @return [void]
|
131
|
+
def clear
|
132
|
+
@known_files.clear
|
133
|
+
@known_directories.clear
|
134
|
+
end
|
135
|
+
|
136
|
+
private
|
137
|
+
|
138
|
+
def non_empty_path_components(path)
|
139
|
+
path.split('/').reject(&:empty?)
|
140
|
+
end
|
141
|
+
|
142
|
+
def path_and_ancestors(path)
|
143
|
+
path_components = non_empty_path_components(path)
|
144
|
+
path_components.each_with_object([]) do |component, seen|
|
145
|
+
seen << [seen.last, component].compact.join('/')
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
@@ -20,10 +20,11 @@ class ZipTricks::SizeEstimator
|
|
20
20
|
# uncompressed_size: 89281911, compressed_size: 121908)
|
21
21
|
# end
|
22
22
|
#
|
23
|
+
# @param kwargs_for_streamer_new Any options to pass to Streamer, see {Streamer#initialize}
|
23
24
|
# @return [Integer] the size of the resulting archive, in bytes
|
24
25
|
# @yield [SizeEstimator] the estimator
|
25
|
-
def self.estimate
|
26
|
-
streamer = ZipTricks::Streamer.new(ZipTricks::NullWriter)
|
26
|
+
def self.estimate(**kwargs_for_streamer_new)
|
27
|
+
streamer = ZipTricks::Streamer.new(ZipTricks::NullWriter, **kwargs_for_streamer_new)
|
27
28
|
estimator = new(streamer)
|
28
29
|
yield(estimator)
|
29
30
|
streamer.close # Returns the .tell of the contained IO
|
@@ -2,13 +2,26 @@
|
|
2
2
|
|
3
3
|
# A simple stateful class for keeping track of a CRC32 value through multiple writes
|
4
4
|
class ZipTricks::StreamCRC32
|
5
|
+
STRINGS_HAVE_CAPACITY_SUPPORT = begin
|
6
|
+
String.new('', capacity: 1)
|
7
|
+
true
|
8
|
+
rescue ArgumentError
|
9
|
+
false
|
10
|
+
end
|
11
|
+
CRC_BUF_SIZE = 1024 * 512
|
12
|
+
private_constant :STRINGS_HAVE_CAPACITY_SUPPORT, :CRC_BUF_SIZE
|
13
|
+
|
5
14
|
# Compute a CRC32 value from an IO object. The object should respond to `read` and `eof?`
|
6
15
|
#
|
7
16
|
# @param io[IO] the IO to read the data from
|
8
17
|
# @return [Fixnum] the computed CRC32 value
|
9
18
|
def self.from_io(io)
|
19
|
+
# If we can specify the string capacity upfront we will not have to resize
|
20
|
+
# the string during operation. This saves time but is only available on
|
21
|
+
# recent Ruby 2.x versions.
|
22
|
+
blob = STRINGS_HAVE_CAPACITY_SUPPORT ? String.new('', capacity: CRC_BUF_SIZE) : String.new('')
|
10
23
|
crc = new
|
11
|
-
crc << io.read(
|
24
|
+
crc << io.read(CRC_BUF_SIZE, blob) until io.eof?
|
12
25
|
crc.to_i
|
13
26
|
end
|
14
27
|
|
data/lib/zip_tricks/streamer.rb
CHANGED
@@ -140,13 +140,19 @@ class ZipTricks::Streamer
|
|
140
140
|
# @param stream[IO] the destination IO for the ZIP. Anything that responds to `<<` can be used.
|
141
141
|
# @param writer[ZipTricks::ZipWriter] the object to be used as the writer.
|
142
142
|
# Defaults to an instance of ZipTricks::ZipWriter, normally you won't need to override it
|
143
|
-
|
143
|
+
# @param auto_rename_duplicate_filenames[Boolean] whether duplicate filenames, when encountered,
|
144
|
+
# should be suffixed with (1), (2) etc. Default value is `true` since it
|
145
|
+
# used to be the default behavior.
|
146
|
+
#
|
147
|
+
# **DEPRECATION NOTICE** In ZipTricks version 5 `auto_rename_duplicate_filenames` will default to `false`
|
148
|
+
def initialize(stream, writer: create_writer, auto_rename_duplicate_filenames: true)
|
144
149
|
raise InvalidOutput, 'The stream must respond to #<<' unless stream.respond_to?(:<<)
|
145
150
|
|
151
|
+
@dedupe_filenames = auto_rename_duplicate_filenames
|
146
152
|
@out = ZipTricks::WriteAndTell.new(stream)
|
147
153
|
@files = []
|
148
154
|
@local_header_offsets = []
|
149
|
-
@
|
155
|
+
@path_set = ZipTricks::PathSet.new
|
150
156
|
@writer = writer
|
151
157
|
end
|
152
158
|
|
@@ -387,7 +393,7 @@ class ZipTricks::Streamer
|
|
387
393
|
|
388
394
|
# Clear the files so that GC will not have to trace all the way to here to deallocate them
|
389
395
|
@files.clear
|
390
|
-
@
|
396
|
+
@path_set.clear
|
391
397
|
|
392
398
|
# and return the final offset
|
393
399
|
@out.tell
|
@@ -429,22 +435,31 @@ class ZipTricks::Streamer
|
|
429
435
|
private
|
430
436
|
|
431
437
|
def add_file_and_write_local_header(
|
432
|
-
filename:,
|
433
|
-
modification_time:,
|
434
|
-
crc32:,
|
435
|
-
storage_mode:,
|
436
|
-
compressed_size:,
|
437
|
-
uncompressed_size:,
|
438
|
-
use_data_descriptor:)
|
439
|
-
|
440
|
-
# Clean backslashes
|
438
|
+
filename:,
|
439
|
+
modification_time:,
|
440
|
+
crc32:,
|
441
|
+
storage_mode:,
|
442
|
+
compressed_size:,
|
443
|
+
uncompressed_size:,
|
444
|
+
use_data_descriptor:)
|
445
|
+
|
446
|
+
# Clean backslashes
|
441
447
|
filename = remove_backslash(filename)
|
442
|
-
filename = uniquify_name(filename) if @filenames_set.include?(filename)
|
443
|
-
|
444
448
|
raise UnknownMode, "Unknown compression mode #{storage_mode}" unless [STORED, DEFLATED].include?(storage_mode)
|
445
|
-
|
446
449
|
raise Overflow, 'Filename is too long' if filename.bytesize > 0xFFFF
|
447
450
|
|
451
|
+
# If we need to massage filenames to enforce uniqueness,
|
452
|
+
# do so before we check for file/directory conflicts
|
453
|
+
filename = ZipTricks::UniquifyFilename.call(filename, @path_set) if @dedupe_filenames
|
454
|
+
|
455
|
+
# Make sure there is no file/directory clobbering (conflicts), or - if deduping is disabled -
|
456
|
+
# no duplicate filenames/paths
|
457
|
+
if filename.end_with?('/')
|
458
|
+
@path_set.add_directory_path(filename)
|
459
|
+
else
|
460
|
+
@path_set.add_file_path(filename)
|
461
|
+
end
|
462
|
+
|
448
463
|
if use_data_descriptor
|
449
464
|
crc32 = 0
|
450
465
|
compressed_size = 0
|
@@ -460,7 +475,6 @@ use_data_descriptor:)
|
|
460
475
|
use_data_descriptor)
|
461
476
|
|
462
477
|
@files << e
|
463
|
-
@filenames_set << e.filename
|
464
478
|
@local_header_offsets << @out.tell
|
465
479
|
|
466
480
|
@writer.write_local_file_header(io: @out,
|
@@ -476,28 +490,4 @@ use_data_descriptor:)
|
|
476
490
|
def remove_backslash(filename)
|
477
491
|
filename.tr('\\', '_')
|
478
492
|
end
|
479
|
-
|
480
|
-
def uniquify_name(filename)
|
481
|
-
# we add (1), (2), (n) at the end of a filename if there is a duplicate
|
482
|
-
copy_pattern = /\((\d+)\)$/
|
483
|
-
parts = filename.split('.')
|
484
|
-
ext = if parts.last =~ /gz|zip/ && parts.size > 2
|
485
|
-
parts.pop(2)
|
486
|
-
elsif parts.size > 1
|
487
|
-
parts.pop
|
488
|
-
end
|
489
|
-
fn_last_part = parts.pop
|
490
|
-
|
491
|
-
duplicate_counter = 1
|
492
|
-
loop do
|
493
|
-
fn_last_part = if fn_last_part =~ copy_pattern
|
494
|
-
fn_last_part.sub(copy_pattern, "(#{duplicate_counter})")
|
495
|
-
else
|
496
|
-
"#{fn_last_part} (#{duplicate_counter})"
|
497
|
-
end
|
498
|
-
new_filename = (parts + [fn_last_part, ext]).compact.join('.')
|
499
|
-
return new_filename unless @filenames_set.include?(new_filename)
|
500
|
-
duplicate_counter += 1
|
501
|
-
end
|
502
|
-
end
|
503
493
|
end
|
@@ -50,6 +50,8 @@ class ZipTricks::Streamer::DeflatedWriter
|
|
50
50
|
def finish
|
51
51
|
@compressed_io << @deflater.finish until @deflater.finished?
|
52
52
|
{crc32: @crc.to_i, compressed_size: @compressed_io.tell, uncompressed_size: @uncompressed_size}
|
53
|
+
ensure
|
54
|
+
@deflater.close
|
53
55
|
end
|
54
56
|
|
55
57
|
private
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module ZipTricks::UniquifyFilename
|
2
|
+
|
3
|
+
# Makes a given filename unique by appending a (n) suffix
|
4
|
+
# between just before the filename extension. So "file.txt" gets
|
5
|
+
# transformed into "file (1).txt". The transformation is applied
|
6
|
+
# repeatedly as long as the generated filename is present
|
7
|
+
# in `while_included_in` object
|
8
|
+
#
|
9
|
+
# @param path[String] the path to make unique
|
10
|
+
# @param while_included_in[#include?] an object that stores the list of already used paths
|
11
|
+
# @return [String] the path as is, or with the suffix required to make it unique
|
12
|
+
def self.call(path, while_included_in)
|
13
|
+
return path unless while_included_in.include?(path)
|
14
|
+
|
15
|
+
# we add (1), (2), (n) at the end of a filename before the filename extension,
|
16
|
+
# but only if there is a duplicate
|
17
|
+
copy_pattern = /\((\d+)\)$/
|
18
|
+
parts = path.split('.')
|
19
|
+
ext = if parts.last =~ /gz|zip/ && parts.size > 2
|
20
|
+
parts.pop(2)
|
21
|
+
elsif parts.size > 1
|
22
|
+
parts.pop
|
23
|
+
end
|
24
|
+
fn_last_part = parts.pop
|
25
|
+
|
26
|
+
duplicate_counter = 1
|
27
|
+
loop do
|
28
|
+
fn_last_part = if fn_last_part =~ copy_pattern
|
29
|
+
fn_last_part.sub(copy_pattern, "(#{duplicate_counter})")
|
30
|
+
else
|
31
|
+
"#{fn_last_part} (#{duplicate_counter})"
|
32
|
+
end
|
33
|
+
new_path = (parts + [fn_last_part, ext]).compact.join('.')
|
34
|
+
return new_path unless while_included_in.include?(new_path)
|
35
|
+
duplicate_counter += 1
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
data/lib/zip_tricks/version.rb
CHANGED
@@ -57,7 +57,7 @@ class ZipTricks::ZipWriter
|
|
57
57
|
C_UINT2 = 'v' # Encode a 2-byte unsigned little-endian uint
|
58
58
|
C_UINT8 = 'Q<' # Encode an 8-byte unsigned little-endian uint
|
59
59
|
C_CHAR = 'C' # For bit-encoded strings
|
60
|
-
C_INT4 = '
|
60
|
+
C_INT4 = 'l<' # Encode a 4-byte signed little-endian int
|
61
61
|
|
62
62
|
private_constant :FOUR_BYTE_MAX_UINT,
|
63
63
|
:TWO_BYTE_MAX_UINT,
|
@@ -118,7 +118,7 @@ class ZipTricks::ZipWriter
|
|
118
118
|
if requires_zip64
|
119
119
|
extra_fields << zip_64_extra_for_local_file_header(compressed_size: compressed_size, uncompressed_size: uncompressed_size)
|
120
120
|
end
|
121
|
-
extra_fields <<
|
121
|
+
extra_fields << timestamp_extra_for_local_file_header(mtime)
|
122
122
|
|
123
123
|
io << [extra_fields.size].pack(C_UINT2) # extra field length 2 bytes
|
124
124
|
|
@@ -182,7 +182,7 @@ class ZipTricks::ZipWriter
|
|
182
182
|
compressed_size: compressed_size,
|
183
183
|
uncompressed_size: uncompressed_size)
|
184
184
|
end
|
185
|
-
extra_fields <<
|
185
|
+
extra_fields << timestamp_extra_for_central_directory_entry(mtime)
|
186
186
|
|
187
187
|
io << [extra_fields.size].pack(C_UINT2) # extra field length 2 bytes
|
188
188
|
|
@@ -345,12 +345,14 @@ class ZipTricks::ZipWriter
|
|
345
345
|
pack_array(data_and_packspecs)
|
346
346
|
end
|
347
347
|
|
348
|
-
# Writes the extended timestamp information field
|
348
|
+
# Writes the extended timestamp information field for local headers.
|
349
|
+
#
|
350
|
+
# The spec defines 2
|
349
351
|
# different formats - the one for the local file header can also accomodate the
|
350
352
|
# atime and ctime, whereas the one for the central directory can only take
|
351
353
|
# the mtime - and refers the reader to the local header extra to obtain the
|
352
354
|
# remaining times
|
353
|
-
def
|
355
|
+
def timestamp_extra_for_local_file_header(mtime)
|
354
356
|
# Local-header version:
|
355
357
|
#
|
356
358
|
# Value Size Description
|
@@ -378,16 +380,21 @@ class ZipTricks::ZipWriter
|
|
378
380
|
# bit 1 if set, access time is present
|
379
381
|
# bit 2 if set, creation time is present
|
380
382
|
# bits 3-7 reserved for additional timestamps; not set
|
381
|
-
flags =
|
383
|
+
flags = 0b00000001 # Set the lowest bit only, to indicate that only mtime is present
|
382
384
|
data_and_packspecs = [
|
383
385
|
0x5455, C_UINT2, # tag for this extra block type ("UT")
|
384
|
-
(1 + 4), C_UINT2, # the size of this block (1 byte used for the Flag +
|
386
|
+
(1 + 4), C_UINT2, # the size of this block (1 byte used for the Flag + 3 longs used for the timestamp)
|
385
387
|
flags, C_CHAR, # encode a single byte
|
386
|
-
mtime.utc.to_i, C_INT4, # Use a signed
|
388
|
+
mtime.utc.to_i, C_INT4, # Use a signed int, not the unsigned one used by the rest of the ZIP spec.
|
387
389
|
]
|
390
|
+
# The atime and ctime can be omitted if not present
|
388
391
|
pack_array(data_and_packspecs)
|
389
392
|
end
|
390
393
|
|
394
|
+
# Since we do not supply atime or ctime, the contents of the two extra fields (central dir and local header)
|
395
|
+
# is exactly the same, so we can use a method alias.
|
396
|
+
alias_method :timestamp_extra_for_central_directory_entry, :timestamp_extra_for_local_file_header
|
397
|
+
|
391
398
|
# Writes the Zip64 extra field for the central directory header.It differs from the extra used in the local file header because it
|
392
399
|
# also contains the location of the local file header in the ZIP as an 8-byte int.
|
393
400
|
#
|
data/zip_tricks.gemspec
CHANGED
@@ -40,6 +40,7 @@ Gem::Specification.new do |spec|
|
|
40
40
|
spec.add_development_dependency 'complexity_assert'
|
41
41
|
spec.add_development_dependency 'coderay'
|
42
42
|
spec.add_development_dependency 'benchmark-ips'
|
43
|
+
spec.add_development_dependency 'allocation_stats', '~> 0.1.5'
|
43
44
|
spec.add_development_dependency 'yard', '~> 0.9'
|
44
45
|
spec.add_development_dependency 'wetransfer_style', '0.6.0'
|
45
46
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: zip_tricks
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 4.
|
4
|
+
version: 4.8.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Julik Tarkhanov
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-11-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -150,6 +150,20 @@ dependencies:
|
|
150
150
|
- - ">="
|
151
151
|
- !ruby/object:Gem::Version
|
152
152
|
version: '0'
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: allocation_stats
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - "~>"
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: 0.1.5
|
160
|
+
type: :development
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - "~>"
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: 0.1.5
|
153
167
|
- !ruby/object:Gem::Dependency
|
154
168
|
name: yard
|
155
169
|
requirement: !ruby/object:Gem::Requirement
|
@@ -214,6 +228,7 @@ files:
|
|
214
228
|
- lib/zip_tricks/file_reader/stored_reader.rb
|
215
229
|
- lib/zip_tricks/null_writer.rb
|
216
230
|
- lib/zip_tricks/output_enumerator.rb
|
231
|
+
- lib/zip_tricks/path_set.rb
|
217
232
|
- lib/zip_tricks/rack_body.rb
|
218
233
|
- lib/zip_tricks/rails_streaming.rb
|
219
234
|
- lib/zip_tricks/remote_io.rb
|
@@ -225,6 +240,7 @@ files:
|
|
225
240
|
- lib/zip_tricks/streamer/entry.rb
|
226
241
|
- lib/zip_tricks/streamer/stored_writer.rb
|
227
242
|
- lib/zip_tricks/streamer/writable.rb
|
243
|
+
- lib/zip_tricks/uniquify_filename.rb
|
228
244
|
- lib/zip_tricks/version.rb
|
229
245
|
- lib/zip_tricks/write_and_tell.rb
|
230
246
|
- lib/zip_tricks/write_buffer.rb
|
@@ -258,8 +274,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
258
274
|
- !ruby/object:Gem::Version
|
259
275
|
version: '0'
|
260
276
|
requirements: []
|
261
|
-
|
262
|
-
rubygems_version: 2.6.11
|
277
|
+
rubygems_version: 3.1.2
|
263
278
|
signing_key:
|
264
279
|
specification_version: 4
|
265
280
|
summary: Stream out ZIP files from Ruby
|