zipping 0.2.3 → 0.2.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (5) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/lib/zipping/version.rb +1 -1
  4. data/lib/zipping.rb +341 -570
  5. metadata +3 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 70577713282f8648bbde02ed2cd685e278d7266c
4
- data.tar.gz: 344ca0c1c1b95e31a5d8e7e0291762809b1fe05a
3
+ metadata.gz: 243995bc8d06cc56eb535cbca1b3ad5de5b61dbc
4
+ data.tar.gz: 5f563d079b0c7cbfbd0674f05c150e7ad330c7b1
5
5
  SHA512:
6
- metadata.gz: dea1a7d86f095c275af8aa06a01ab7c586f3bd8db8df4a54d7fc64d485b88ce61b9a2de74ca557e5159e5b34bbc8d06a01ac42bc13b420a46fe3814509f1bd09
7
- data.tar.gz: 72ffa3ecd714fbb0600dcf09c30bdfb81067271ac9e937c2ad0f4527c4d8c6c8688bd76a4b355ea8e8b6de5fc82ac6da0ed9aaf703a83b3543ffa8a9d202b91d
6
+ metadata.gz: 201505429f0c6572fe1b0dea7ee4e440598696bd73a537ba663fc73a28f5901597dfe38286697d452139822ebbde5571877707e73b1a474bebb29b08f2804907
7
+ data.tar.gz: 42df47b06e8e292857f030d58f397f8b623f0df7805d64d392f1020706761a422c8b7afd416b1fb678b6cdbfcd1011d72d610c5bce4eb70c0a10af68eb2ed8c0
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  zipping
2
2
  =======
3
- [![Gem Version](https://badge.fury.io/rb/zipping.png)](http://badge.fury.io/rb/zipping)
3
+ <a href="http://badge.fury.io/rb/zipping"><img src="https://badge.fury.io/rb/zipping@2x.png" alt="Gem Version" height="18"></a>
4
4
 
5
5
  This gem is for compressing files as a zip and outputting to a stream (or a stream-like interface object). The output to a stream proceeds little by little, as files are compressed.
6
6
 
@@ -65,4 +65,4 @@ Then, you get zip binary data in `zip_data`.
65
65
 
66
66
  ---
67
67
 
68
- Copyright (c) 2013 [Nekojarashi Inc.](http://www.nekojarashi.com)
68
+ Copyright [Nekojarashi Inc.](http://www.nekojarashi.com)
@@ -1,3 +1,3 @@
1
1
  module Zipping
2
- VERSION = "0.2.3"
2
+ VERSION = "0.2.5"
3
3
  end
data/lib/zipping.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # -*- encoding: utf-8 -*-
2
2
  require "zipping/version"
3
3
  require 'zip'
4
+ require 'pathname'
4
5
 
5
6
  module Zipping
6
7
 
@@ -9,21 +10,33 @@ module Zipping
9
10
  # Constants
10
11
  #
11
12
 
13
+ # header signatures
12
14
  ZS_c_pk0102 = 0x02014b50
13
15
  ZS_c_pk0304 = 0x04034b50
14
16
  ZS_c_pk0506 = 0x06054b50
15
17
  ZS_c_pk0708 = 0x08074b50
16
- ZS_c_ver_dir = 0x000a
17
- ZS_c_ver_file = 0x0014
18
+
19
+ # values for zip version
20
+ ZS_c_ver_nocomp = 0x000a
21
+ ZS_c_ver_deflate = 0x0014
18
22
  ZS_c_ver_made = 0x0315
23
+
24
+ # values for option
19
25
  ZS_c_opt_none = 0x0000
20
- ZS_c_opt_nosize = 0x0008
26
+ ZS_c_opt_0708 = 0x0008
27
+
28
+ # compression types
21
29
  ZS_c_comp_deflate = 0x0008
30
+ ZS_c_comp_none = 0x0000
31
+
32
+ # empty values
22
33
  ZS_c_int2_zero = 0x0000
23
34
  ZS_c_int4_zero = 0x00000000
24
35
 
36
+ # OS-specific attributes (OS X)
25
37
  ZS_c_oattr_dir = 0x41ed4000
26
38
  ZS_c_oattr_file = 0x81a44000
39
+ ZS_c_oattr_symlink = 0xa1ed4000
27
40
 
28
41
 
29
42
 
@@ -32,679 +45,424 @@ module Zipping
32
45
  #
33
46
 
34
47
  def self.files_to_zip(output_stream, files, file_division_size = 1048576, encoding = :utf8)
35
- builder = ZipBuilder.new output_stream, files, file_division_size, encoding
36
- builder.pack
48
+ builder = ZipBuilder.new output_stream, file_division_size, encoding
49
+ builder.pack files
50
+ builder.close
51
+ output_stream
52
+ end
53
+
54
+ # use if you need to make File directly.
55
+ def self.files_to_zipfile(path, files, encoding = :utf8)
56
+ File.open(path, 'wb'){|f| files_to_zip f, files, 1048576, encoding}
57
+ end
58
+
59
+ # use if you need to make binary String directly.
60
+ def self.files_to_zipdata(files, encoding = :utf8)
61
+ files_to_zip ''.force_encoding('ASCII-8bit'), files, 1048576, encoding
37
62
  end
38
63
 
39
64
 
65
+
40
66
  #
41
67
  # Internal Classes
42
68
  #
43
69
 
70
+ # Errors
71
+ class Error < StandardError
72
+ end
73
+
44
74
  # Output stream wrapper that measures size of stream passing through.
45
75
  class StreamMeter
46
76
  def initialize(output_stream)
47
77
  @output_stream = output_stream
48
78
  @size = 0
79
+ @crc = Zlib.crc32
49
80
  end
50
81
  def << (data)
51
82
  @size += data.length
83
+ @crc = Zlib.crc32 data, @crc
52
84
  @output_stream << data
53
85
  end
54
86
  attr_reader :size
87
+ attr_reader :crc
55
88
  end
56
89
 
57
90
  class ZipBuilder
58
91
 
59
92
  # Initialize ZipBuilder.
60
93
  # 'files' must be a String(file or directory path), a Hash(entity), or an Array of Strings and/or Hashes.
61
- def initialize(output_stream, files, file_division_size = 1048576, encoding = :utf8)
62
- @o = output_stream
63
- @f = ZipBuilder.to_entities files
94
+ def initialize(output_stream, file_division_size = 1048576, encoding = :utf8)
95
+ @w = Writer.new output_stream, file_division_size
64
96
  @e = encoding
65
- @s = file_division_size
97
+ @l = []
66
98
  end
67
99
 
68
- # Create an Array of entity Hashes.
69
- def self.to_entities(files)
70
- if files.is_a? Hash
71
- ret = [files]
72
- elsif files.is_a? String
73
- entity = ZipBuilder.to_entity(files)
74
- ret = entity.nil? ? [] : [entity]
75
- elsif files.is_a? Array
76
- ret = []
77
- files.each do |f|
78
- entity = ZipBuilder.to_entity(f)
79
- ret << entity unless entity.nil?
80
- end
81
- else
82
- ret = []
83
- end
84
- ret
100
+ ### Attr controls
101
+
102
+ def reset_state
103
+ @pending_dirs = []
104
+ @current_dir = {name: '', time: Time.now}
85
105
  end
86
106
 
87
- # Create an entity Hash with a path String
88
- def self.to_entity(path)
89
- return path if path.is_a?(Hash)
90
- return nil unless path.is_a?(String) && File.exists?(path)
91
- ret = {
92
- :path => path,
93
- :name => File.basename(path),
94
- :time => Time.now
95
- }
107
+ def has_dir?
108
+ ! @pending_dirs.empty?
96
109
  end
97
110
 
98
- # Create ASCII-8bits string. Also convert encoding if needed.
99
- def self.to_bytes(str, encoding = :utf8)
100
- unless encoding.nil? || encoding == :utf8
101
- case encoding
102
- when :shift_jis
103
- begin
104
- str = str.encode 'Shift_JIS', :invalid => :replace, :undef => :replace, :replace => '??'
105
- rescue => e
106
- end
107
- end
108
- end
109
- [str].pack('a*')
111
+ def next_dir
112
+ @pending_dirs.shift
110
113
  end
111
114
 
112
- # Pack file and directory entities and output to stream.
113
- def pack
114
- return if @f.empty?
115
+ def postpone_dir(dir)
116
+ queue_entity dir, @pending_dirs
117
+ end
115
118
 
116
- # directory entities found but not packed
117
- @pending_dirs = []
119
+ def postpone_symlink(link)
120
+ queue_entity link, @l
121
+ end
122
+
123
+ def queue_entity(entity, queue)
124
+ entity[:abs_path] = abs_path_for entity[:name] || File.basename(entity[:path])
125
+ queue << entity
126
+ end
118
127
 
119
- # current directory
120
- @current_dir = ''
121
- @current_dir_created_at = Time.now
128
+ def cd(dir)
129
+ @current_dir = dir
130
+ end
131
+
132
+ def root_dir?
133
+ @current_dir.nil? || @current_dir[:abs_path].nil?
134
+ end
135
+
136
+ def current_dir
137
+ root_dir? ? '' : @current_dir[:abs_path]
138
+ end
122
139
 
123
- # data of positions necessary to create headers
124
- @dp = []
140
+ def current_dir_entity
141
+ @current_dir
142
+ end
125
143
 
126
- o = StreamMeter.new @o
144
+ def abs_path_for(name)
145
+ root_dir? ? name : (@current_dir[:abs_path] + '/' + name)
146
+ end
127
147
 
128
- # pack file entities, store directory entities into @pending_dirs
129
- pack_file_entities o
148
+ def abs_path_for_entity(entity)
149
+ abs_path_for entity[:name] || File.basename(entity[:path])
150
+ end
130
151
 
131
- # retrieve and pack stored directories
132
- until @pending_dirs.empty? do
152
+ ### Conversions
133
153
 
134
- current_dir = @pending_dirs.shift
135
- @current_dir = current_dir[:name] << '/'
136
- @current_dir_created_at = current_dir[:time]
154
+ # Get entities of files in dir
155
+ def subdir_entities(dir = @current_dir)
156
+ Dir.glob(dir[:path] + '/*').map!{|path| {path: path, time: File.mtime(path), name: File.basename(path)}}
157
+ end
137
158
 
138
- # write directory entry
139
- pack_directory o, current_dir[:time]
159
+ # Fix an entity: time -> DOSTime object, name -> abs path in zip & encoded
160
+ def fix_entity(entity)
161
+ {
162
+ path: entity[:path],
163
+ filetime: Zip::DOSTime.at(entity[:time] || File.mtime(entity[:path])),
164
+ binary_name: string_to_bytes(abs_path_for_entity(entity)),
165
+ zip_path: abs_path_for_entity(entity)
166
+ }
167
+ end
140
168
 
141
- begin
142
- # get files in the directory
143
- files = Dir.glob(current_dir[:path] + '/*')
169
+ def fix_current_dir_entity
170
+ fix_entity(@current_dir).merge!(
171
+ {
172
+ binary_name: string_to_bytes(@current_dir[:abs_path] + '/'),
173
+ zip_path: @current_dir[:abs_path]
174
+ }
175
+ )
176
+ end
144
177
 
145
- # pack files, store directories as entities into @pending_dirs
146
- pack_files o, files, current_dir
147
- rescue => e
178
+ # Create ASCII-8bits string. Also convert encoding if needed.
179
+ def string_to_bytes(str)
180
+ unless @e.nil? || @e == :utf8
181
+ if @e == :shift_jis
182
+ begin
183
+ str = str.encode 'Shift_JIS', :invalid => :replace, :undef => :replace, :replace => '??'
184
+ rescue => e
185
+ end
148
186
  end
149
187
  end
188
+ [str].pack('a*')
189
+ end
150
190
 
151
- # prepare to create headers
152
- @header_offset = o.size
153
- o = StreamMeter.new(@o)
191
+ ### Compression operations
154
192
 
155
- # write headers
156
- write_central_dir_headers o
157
- @header_size = o.size
158
- write_end_central_dir_header
193
+ # Pack file and directory entities and output to stream.
194
+ def pack(files)
195
+ entities = Entity.entities_from files
196
+ return if entities.empty?
197
+
198
+ reset_state
199
+ pack_entities entities
200
+ while has_dir?
201
+ cd next_dir
202
+ pack_current_dir
203
+ end
159
204
  end
160
205
 
161
- # Pack file entities and output to stream. Directory entities are not packed but stored.
162
- def pack_file_entities(o = @o, files = @f, dir = @current_dir, dirs = @pending_dirs, data_positions = @dp, encoding = @e, file_division_size = @s)
163
- files.each do |file|
164
- next unless file.is_a?(Hash) && file[:path]
206
+ # Pack a directory
207
+ def pack_current_dir
208
+ pack_current_directory_entity
209
+ pack_entities subdir_entities
210
+ end
165
211
 
166
- f = file[:path]
167
- if File.directory? f
168
- dirs << file
169
- next
170
- end
171
- next unless File.file? f
172
- data_offset = o.size
173
- file_path = f
174
- fname = dir.clone << (file[:name] || File.basename(file_path))
175
- bfname = ZipBuilder.to_bytes(fname, encoding)
176
- filetime = Zip::DOSTime.at(file[:time] || File.mtime(file_path))
177
- filesize = File.size(file_path)
178
- pk = [
179
- ZS_c_pk0304,
180
- ZS_c_ver_file,
181
- ZS_c_opt_nosize,
182
- ZS_c_comp_deflate,
183
- filetime.to_binary_dos_time,
184
- filetime.to_binary_dos_date,
185
- ZS_c_int4_zero,
186
- ZS_c_int4_zero,
187
- ZS_c_int4_zero,
188
- bfname.length,
189
- ZS_c_int2_zero
190
- ]
191
- bin = pk.pack('VvvvvvVVVvv')
192
- o << bin
193
- bin = bfname
194
- o << bin
195
-
196
- m = StreamMeter.new(o)
197
- d = Zip::Deflater.new(m)
198
- File.open(file_path) do |f|
199
- cur_filesize = filesize
200
- while cur_filesize > 0
201
- if cur_filesize >= file_division_size
202
- d << f.read(file_division_size)
203
- cur_filesize -= file_division_size
204
- else
205
- d << f.read(cur_filesize)
206
- cur_filesize = 0
207
- end
208
- end
212
+ # Pack symlinks if its link path exists in zip
213
+ def pack_symlinks
214
+ reset_state
215
+ @l.each do |link|
216
+ if @w.path_exists? Entity.linked_path(link[:abs_path], File.readlink(link[:path]))
217
+ link[:name] = link[:abs_path]
218
+ pack_symbolic_link_entity link
209
219
  end
210
- d.finish
211
-
212
- pk = [
213
- ZS_c_pk0708,
214
- d.crc,
215
- m.size,
216
- d.size
217
- ]
218
- bin = pk.pack('VVVV')
219
- o << bin
220
- data_positions << {
221
- :folder => false,
222
- :file => fname,
223
- :file_dos_time => filetime.to_binary_dos_time,
224
- :file_dos_date => filetime.to_binary_dos_date,
225
- :binary_fname => bfname,
226
- :offset => data_offset,
227
- :crc => d.crc,
228
- :complen => m.size,
229
- :uncomplen => d.size
230
- }
231
220
  end
232
221
  end
233
222
 
234
- # Pack directory and output to stream.
235
- def pack_directory(o = @o, time_created_at = @current_dir_created_at, dir = @current_dir, data_positions = @dp, encoding = @e)
236
- bdir = ZipBuilder.to_bytes(dir, encoding)
237
- data_offset = o.size
238
- filetime = Zip::DOSTime.at(time_created_at);
239
- filesize = 0
240
- pk = [
241
- ZS_c_pk0304,
242
- ZS_c_ver_dir,
243
- ZS_c_opt_none,
244
- ZS_c_comp_deflate,
245
- filetime.to_binary_dos_time,
246
- filetime.to_binary_dos_date,
247
- ZS_c_int4_zero,
248
- ZS_c_int4_zero,
249
- ZS_c_int4_zero,
250
- bdir.length,
251
- ZS_c_int2_zero
252
- ]
253
- bin = pk.pack('VvvvvvVVVvv')
254
- o << bin
255
- bin = bdir
256
- o << bin
257
- data_positions << {
258
- :folder => true,
259
- :file => dir,
260
- :file_dos_time => filetime.to_binary_dos_time,
261
- :file_dos_date => filetime.to_binary_dos_date,
262
- :binary_fname => bdir,
263
- :offset => data_offset,
264
- :crc => ZS_c_int4_zero,
265
- :complen => ZS_c_int4_zero,
266
- :uncomplen => ZS_c_int4_zero
267
- }
223
+ # Create central directories
224
+ def close
225
+ pack_symlinks
226
+ @w.close
268
227
  end
269
228
 
270
- # Pack files and output to stream. Directories are not packed but stored.
271
- def pack_files(o, files, dir, dirs = @pending_dirs, data_positions = @dp, encoding = @e, file_division_size = @s)
272
- files.each do |f|
273
- if File.directory? f
274
- dirs << {
275
- :path => f,
276
- :name => dir[:name] + File.basename(f),
277
- :time => dir[:time]
278
- }
279
- next
280
- end
281
- next unless File.file? f
282
- data_offset = o.size
283
- file_path = f
284
- file = dir[:name] + File.basename(file_path)
285
- bfile = ZipBuilder.to_bytes(file, encoding)
286
- filetime = Zip::DOSTime.at(dir[:time] || File.mtime(file_path))
287
- filesize = File.size(file_path)
288
- pk = [
289
- ZS_c_pk0304,
290
- ZS_c_ver_file,
291
- ZS_c_opt_nosize,
292
- ZS_c_comp_deflate,
293
- filetime.to_binary_dos_time,
294
- filetime.to_binary_dos_date,
295
- ZS_c_int4_zero,
296
- ZS_c_int4_zero,
297
- ZS_c_int4_zero,
298
- bfile.length,
299
- ZS_c_int2_zero
300
- ]
301
- bin = pk.pack('VvvvvvVVVvv')
302
- o << bin
303
- bin = bfile
304
- o << bin
305
-
306
- m = StreamMeter.new(o)
307
- d = Zip::Deflater.new(m)
308
- File.open(file_path) do |f|
309
- cur_filesize = filesize
310
- while cur_filesize > 0
311
- if cur_filesize >= file_division_size
312
- d << f.read(file_division_size)
313
- cur_filesize -= file_division_size
314
- else
315
- d << f.read(cur_filesize)
316
- cur_filesize = 0
317
- end
318
- end
229
+ # Pack file entities. Directory entities are queued, not packed in this method.
230
+ def pack_entities(entities)
231
+ entities.each do |entity|
232
+ # ignore bad entities
233
+ next unless entity.is_a?(Hash) && entity[:path]
234
+
235
+ path = entity[:path]
236
+ if File.symlink? path
237
+ postpone_symlink entity
238
+ elsif File.directory? path
239
+ postpone_dir entity
240
+ elsif File.file? path
241
+ pack_file_entity entity
319
242
  end
320
- d.finish
321
-
322
- pk = [
323
- ZS_c_pk0708,
324
- d.crc,
325
- m.size,
326
- d.size
327
- ]
328
- bin = pk.pack('VVVV')
329
- o << bin
330
- data_positions << {
331
- :folder => false,
332
- :file => file,
333
- :file_dos_time => filetime.to_binary_dos_time,
334
- :file_dos_date => filetime.to_binary_dos_date,
335
- :binary_fname => bfile,
336
- :offset => data_offset,
337
- :crc => d.crc,
338
- :complen => m.size,
339
- :uncomplen => d.size
340
- }
341
243
  end
342
244
  end
343
245
 
344
- # Write central directories.
345
- def write_central_dir_headers(o = @o, data_positions = @dp)
346
- data_positions.each do |dp|
347
- pk = [
348
- ZS_c_pk0102,
349
- ZS_c_ver_made,
350
- (dp[:folder] ? ZS_c_ver_dir : ZS_c_ver_file),
351
- ZS_c_opt_nosize,
352
- ZS_c_comp_deflate,
353
- dp[:file_dos_time],
354
- dp[:file_dos_date],
355
- dp[:crc],
356
- dp[:complen],
357
- dp[:uncomplen],
358
- dp[:binary_fname].length,
359
- ZS_c_int2_zero,
360
- ZS_c_int2_zero,
361
- ZS_c_int2_zero,
362
- ZS_c_int2_zero,
363
- (dp[:folder] ? ZS_c_oattr_dir : ZS_c_oattr_file),
364
- dp[:offset]
365
- ]
366
- bin = pk.pack('VvvvvvvVVVvvvvvVV')
367
- o << bin
368
- bin = dp[:binary_fname]
369
- o << bin
246
+ def pack_file_entity(entity)
247
+ pack_entity entity do
248
+ @w.write_file_entry
370
249
  end
371
250
  end
372
251
 
373
- # Write end of central directory.
374
- def write_end_central_dir_header(o = @o, entry_count = @dp.length, dirsize = @header_size, offset = @header_offset)
375
- pk = [
376
- ZS_c_pk0506,
377
- ZS_c_int2_zero,
378
- ZS_c_int2_zero,
379
- entry_count,
380
- entry_count,
381
- dirsize,
382
- offset,
383
- ZS_c_int2_zero
384
- ]
385
- o << pk.pack('VvvvvVVv')
252
+ def pack_symbolic_link_entity(entity)
253
+ pack_entity entity do
254
+ @w.write_symbolic_link_entry
255
+ end
386
256
  end
387
- end
388
257
 
258
+ def pack_current_directory_entity
259
+ @w.load_entity fix_current_dir_entity
260
+ @w.write_directory_entry
261
+ end
389
262
 
390
- #
391
- # Internal method
392
- #
263
+ def pack_entity(entity)
264
+ @w.load_entity fix_entity entity
265
+ yield
266
+ end
267
+ end
393
268
 
394
- # Pack files into zip.
395
- # You must pass an entity Hash (or Array of them) consists of :path(path of file to pack into zip) and :name(file path inside zip) to this method as 'target_files'.
396
- # Optionally, you may add :time(time created at) to the entity Hashes.
397
- def self.create_zip_file_with_file_entities(outputStream, target_files, usesjis = true, file_division_size = 1048576)
398
- begin
399
-
400
- # prepare entry list for zip
401
- target_files = [target_files] unless target_files.instance_of? Array
402
- entries = []
403
- target_files.each do |file|
404
- if file.is_a? String
405
- begin
406
- entry = {
407
- :path => file,
408
- :name => File.basename(file),
409
- :time => File.mtime(file)
410
- }
411
- rescue => e
412
- next
413
- end
414
- elsif file.is_a? Hash
415
- next unless file[:path] && file[:path].is_a?(String)
416
- entry = file
417
- path = entry[:path]
418
- entry[:name] = File.basename(path) unless entry[:name] && entry[:name].is_a?(String)
419
- entry[:time] = File.mtime(path) unless entry[:time] && entry[:time].is_a?(Time)
420
- else
421
- next
422
- end
423
- entries << entry
424
- end
425
- return if entries.empty?
269
+ class Writer
426
270
 
427
- # prepare to measure stream size
428
- o = StreamMeter.new(outputStream)
271
+ def initialize(output_stream, file_division_size)
272
+ @output_stream = output_stream
273
+ raise Error, 'Specified output stream does not support `<<\' method.' unless @output_stream.respond_to? :<<
274
+ @o = StreamMeter.new output_stream
275
+ @s = file_division_size
276
+ raise Error, 'Bad file_division_size' unless @s.is_a?(Integer) && @s > 0
277
+ @dps = []
278
+ @entries = []
279
+ end
429
280
 
430
- # container to collect header info
431
- data_positions = []
281
+ ### Data interface
432
282
 
433
- # compress entries
434
- self.compress_entries(o, entries, data_positions, usesjis, file_division_size)
283
+ def load_entity(entity)
284
+ @fixed_entity = entity
285
+ @entries << entity[:zip_path]
286
+ end
435
287
 
436
- # measure stream size
437
- pk0102_offset = o.size
288
+ def path_exists?(abs_path)
289
+ @entries.include? abs_path
290
+ end
438
291
 
439
- # prepare to measure header size
440
- m = StreamMeter.new(o)
292
+ ### Write operations
441
293
 
442
- # write headers
443
- self.create_central_dir_headers(m, data_positions)
294
+ def write_file_entry
295
+ write_entry do |path, filetime, name|
296
+ write PKHeader.pk0304(filetime, name.length, true).pack('VvvvvvVVVvv'), name
297
+ ret = deflate_file path
298
+ write PKHeader.pk0708(ret[:crc], ret[:complen], ret[:uncomplen]).pack('VVVV')
299
+ ret
300
+ end
301
+ end
444
302
 
445
- # measure header size
446
- pk0102_size = m.size
303
+ def write_directory_entry
304
+ write_entry_without_compress '', :dir
305
+ end
447
306
 
448
- # write tail header
449
- self.create_end_cent_dir_header(o, data_positions.length, pk0102_size, pk0102_offset)
450
- rescue => e
307
+ def write_symbolic_link_entry
308
+ write_entry_without_compress File.readlink(@fixed_entity[:path]), :symlink
451
309
  end
452
- end
453
310
 
454
- # Pack files and folders into zip.
455
- # All files in folders are also packed into zip, and the structure inside folders are conserved.
456
- def self.create_zip_file_with_files_and_directories(outputStream, target_files, usesjis = true, file_division_size = 1048576)
457
- begin
311
+ def close
312
+ # start of central directories
313
+ @header_offset = current_position
314
+ write_central_dir_headers
458
315
 
459
- target_files = [target_files] unless target_files.instance_of? Array
460
- return if target_files.empty?
316
+ # total size of central directories
317
+ @header_size = current_position - @header_offset
318
+ write_end_central_dir_header
319
+ end
461
320
 
462
- dirs = []
463
- o = StreamMeter.new(outputStream)
464
- data_positions = []
321
+ ### Internal methods
322
+ private
323
+
324
+ def write(*args)
325
+ args.each do |content|
326
+ @o << content
327
+ end
328
+ end
465
329
 
466
- self.compress_file_list(o, target_files, "", dirs, data_positions, usesjis, file_division_size)
330
+ def current_position
331
+ @o.size
332
+ end
467
333
 
468
- while dirs.length > 0 do
469
- current_directory = dirs.shift
470
- dir_path = current_directory[0]
471
- dir = current_directory[1]
472
- dir << '/'
473
- self.create_directory_entry(o, dir_path, dir, data_positions, usesjis, file_division_size)
334
+ def pipe_little_by_little(o, i)
335
+ o << i.read(@s) until i.eof?
336
+ end
474
337
 
475
- files = Dir.glob(dir_path << '/*')
476
- self.compress_file_list(o, files, dir, dirs, data_positions, usesjis, file_division_size)
338
+ def deflate_file(path)
339
+ meter = StreamMeter.new @o
340
+ deflater = Zip::Deflater.new meter
341
+ File.open(path, 'rb'){|f| pipe_little_by_little deflater, f}
342
+ deflater.finish
343
+ {
344
+ crc: deflater.crc,
345
+ complen: meter.size,
346
+ uncomplen: deflater.size,
347
+ deflated: true
348
+ }
349
+ end
477
350
 
351
+ def write_entry_without_compress(data, type)
352
+ write_entry do |path, filetime, name|
353
+ crc = Zlib.crc32 data
354
+ write PKHeader.pk0304(filetime, name.length, false, crc, data.size, data.size).pack('VvvvvvVVVvv'), name, data
355
+ {
356
+ type: type,
357
+ crc: crc,
358
+ complen: data.size,
359
+ uncomplen: data.size
360
+ }
478
361
  end
362
+ end
479
363
 
480
- pk0102_offset = o.size
481
- m = StreamMeter.new(o)
482
- self.create_central_dir_headers(m, data_positions)
364
+ def write_entry
365
+ remember_entry_info current_position, yield(@fixed_entity[:path], @fixed_entity[:filetime], @fixed_entity[:binary_name])
366
+ end
483
367
 
484
- self.create_end_cent_dir_header(o, data_positions.length, m.size, pk0102_offset)
485
- rescue => e
368
+ def remember_entry_info(offset, write_result)
369
+ @dps << @fixed_entity.merge!(write_result).merge!({offset: offset})
486
370
  end
487
- end
488
371
 
489
- # Get file name as ASCII-8bits.
490
- # If needed, also convert encoding.
491
- def self.get_binary_fname(str, usesjis)
492
- if usesjis
493
- begin
494
- str = str.encode 'Shift_JIS', :invalid => :replace, :replace => '??'
495
- rescue => e
372
+ def write_central_dir_headers
373
+ @dps.each do |dp|
374
+ write PKHeader.pk0102(dp).pack('VvvvvvvVVVvvvvvVV'), dp[:binary_name]
496
375
  end
497
376
  end
498
- return [str].pack('a*')
499
- end
500
377
 
501
- # Write directory entry.
502
- #
503
- def self.create_directory_entry(o, dir_path, dir, data_positions, usesjis = false, file_division_size = 1048576)
504
- bdir = self.get_binary_fname(dir, usesjis)
505
- data_offset = o.size
506
- filetime = Zip::DOSTime.at(File.mtime(dir_path))
507
- filesize = 0
508
- pk = [
509
- ZS_c_pk0304,
510
- ZS_c_ver_dir,
511
- ZS_c_opt_none,
512
- ZS_c_comp_deflate,
513
- filetime.to_binary_dos_time,
514
- filetime.to_binary_dos_date,
515
- ZS_c_int4_zero,
516
- ZS_c_int4_zero,
517
- ZS_c_int4_zero,
518
- bdir.length,
519
- ZS_c_int2_zero
520
- ]
521
- bin = pk.pack('VvvvvvVVVvv')
522
- o << bin
523
- bin = bdir
524
- o << bin
525
- data_positions << {
526
- :folder => true,
527
- :file => dir,
528
- :file_dos_time => filetime.to_binary_dos_time,
529
- :file_dos_date => filetime.to_binary_dos_date,
530
- :binary_fname => bdir,
531
- :offset => data_offset,
532
- :crc => ZS_c_int4_zero,
533
- :complen => ZS_c_int4_zero,
534
- :uncomplen => ZS_c_int4_zero
535
- }
378
+ def write_end_central_dir_header
379
+ write PKHeader.pk0506(@dps.length, @header_size, @header_offset).pack('VvvvvVVv')
380
+ end
536
381
  end
537
382
 
538
- def self.compress_entries(o, entries, data_positions, usesjis = false, file_division_size = 1048576)
539
- # `o' must be an output stream which has `size' method
540
-
541
- entries.each do |entry|
542
- path = entry[:path]
543
- name = entry[:name]
544
- mtime = entry[:time]
383
+ class Entity
384
+ def self.entities_from(files)
385
+ if files.is_a? Array
386
+ entities_from_array files
387
+ else
388
+ [entity_from(files)]
389
+ end
390
+ end
545
391
 
546
- next if File.directory? path
547
- next unless File.file? path
548
- data_offset = o.size
549
- b_name = self.get_binary_fname(name, usesjis)
550
- filesize = File.size(path)
551
- pk = [
552
- ZS_c_pk0304,
553
- ZS_c_ver_file,
554
- ZS_c_opt_nosize,
555
- ZS_c_comp_deflate,
556
- mtime.to_binary_dos_time,
557
- mtime.to_binary_dos_date,
558
- ZS_c_int4_zero,
559
- ZS_c_int4_zero,
560
- ZS_c_int4_zero,
561
- b_name.length,
562
- ZS_c_int2_zero
563
- ]
564
- bin = pk.pack('VvvvvvVVVvv')
565
- o << bin
566
- bin = b_name
567
- o << bin
568
-
569
- m = StreamMeter.new(o)
570
- d = Zip::Deflater.new(m)
571
- File.open(path) do |f|
572
- cur_filesize = filesize
573
- while cur_filesize > 0
574
- if cur_filesize >= file_division_size
575
- d << f.read(file_division_size)
576
- cur_filesize -= file_division_size
577
- else
578
- d << f.read(cur_filesize)
579
- cur_filesize = 0
580
- end
581
- end
392
+ # Create an entity Hash with a path String
393
+ def self.entity_from(ent)
394
+ if ent.is_a?(Hash) && File.exists?(ent[:path])
395
+ ent
396
+ elsif ent.is_a?(String) && File.exists?(ent)
397
+ entity_from_path ent
582
398
  end
583
- d.finish
399
+ end
584
400
 
585
- pk = [
586
- ZS_c_pk0708,
587
- d.crc,
588
- m.size,
589
- d.size
590
- ]
591
- bin = pk.pack('VVVV')
592
- o << bin
593
- data_positions << {
594
- :folder => false,
595
- :file => name,
596
- :file_dos_time => filetime.to_binary_dos_time,
597
- :file_dos_date => filetime.to_binary_dos_date,
598
- :binary_fname => b_name,
599
- :offset => data_offset,
600
- :crc => d.crc,
601
- :complen => m.size,
602
- :uncomplen => d.size
401
+ def self.entities_from_array(arr)
402
+ arr.map{|ent| entity_from ent}.delete_if(&:nil?)
403
+ end
404
+
405
+ def self.entity_from_path(path)
406
+ {
407
+ :path => path,
408
+ :name => File.basename(path),
409
+ :time => File.mtime(path)
603
410
  }
604
411
  end
605
412
 
413
+ def self.linked_path(abs_path, link)
414
+ (Pathname.new(abs_path).parent + link).expand_path('/').to_s[1..-1]
415
+ end
606
416
  end
607
417
 
608
- def self.compress_file_list(o, files, dir, dirs, data_positions, usesjis = false, file_division_size = 1048576)
609
- files.each do |f|
610
- if File.directory? f
611
- dirs << [f, dir.clone << File.basename(f)]
612
- next
613
- end
614
- next unless File.file? f
615
- data_offset = o.size
616
- file_path = f
617
- file = dir.clone << File.basename(file_path)
618
- bfile = self.get_binary_fname(file, usesjis)
619
- filetime = Zip::DOSTime.at(File.mtime(file_path))
620
- filesize = File.size(file_path)
621
- pk = [
418
+ module PKHeader
419
+ # 0102: central directory
420
+ def self.pk0102(dp)
421
+ # dp: info of an entry
422
+ [
423
+ ZS_c_pk0102,
424
+ ZS_c_ver_made,
425
+ (dp[:deflated] ? ZS_c_ver_nocomp : ZS_c_ver_deflate),
426
+ (dp[:deflated] ? ZS_c_opt_0708 : ZS_c_opt_none),
427
+ (dp[:deflated] ? ZS_c_comp_deflate : ZS_c_comp_none),
428
+ dp[:filetime].to_binary_dos_time,
429
+ dp[:filetime].to_binary_dos_date,
430
+ dp[:crc],
431
+ dp[:complen],
432
+ dp[:uncomplen],
433
+ dp[:binary_name].length,
434
+ ZS_c_int2_zero,
435
+ ZS_c_int2_zero,
436
+ ZS_c_int2_zero,
437
+ ZS_c_int2_zero,
438
+ (dp[:type] == :symlink ? ZS_c_oattr_symlink : dp[:type] == :dir ? ZS_c_oattr_dir : ZS_c_oattr_file),
439
+ dp[:offset]
440
+ ]
441
+ end
442
+
443
+ # 0304: header of entries
444
+ def self.pk0304(filetime, namelen, deflated, crc = nil, compsize = nil, uncompsize = nil)
445
+ [
622
446
  ZS_c_pk0304,
623
- ZS_c_ver_file,
624
- ZS_c_opt_nosize,
625
- ZS_c_comp_deflate,
447
+ (deflated ? ZS_c_ver_deflate : ZS_c_ver_nocomp),
448
+ (deflated ? ZS_c_opt_0708 : ZS_c_opt_none),
449
+ (deflated ? ZS_c_comp_deflate : ZS_c_comp_none),
626
450
  filetime.to_binary_dos_time,
627
451
  filetime.to_binary_dos_date,
628
- ZS_c_int4_zero,
629
- ZS_c_int4_zero,
630
- ZS_c_int4_zero,
631
- bfile.length,
452
+ (crc || ZS_c_int4_zero),
453
+ (compsize || ZS_c_int4_zero),
454
+ (uncompsize || ZS_c_int4_zero),
455
+ namelen,
632
456
  ZS_c_int2_zero
633
457
  ]
634
- bin = pk.pack('VvvvvvVVVvv')
635
- o << bin
636
- bin = bfile
637
- o << bin
638
-
639
- m = StreamMeter.new(o)
640
- d = Zip::Deflater.new(m)
641
- File.open(file_path) do |f|
642
- cur_filesize = filesize
643
- while cur_filesize > 0
644
- if cur_filesize >= file_division_size
645
- d << f.read(file_division_size)
646
- cur_filesize -= file_division_size
647
- else
648
- d << f.read(cur_filesize)
649
- cur_filesize = 0
650
- end
651
- end
652
- end
653
- d.finish
654
-
655
- pk = [
656
- ZS_c_pk0708,
657
- d.crc,
658
- m.size,
659
- d.size
660
- ]
661
- bin = pk.pack('VVVV')
662
- o << bin
663
- data_positions << {
664
- :folder => false,
665
- :file => file,
666
- :file_dos_time => filetime.to_binary_dos_time,
667
- :file_dos_date => filetime.to_binary_dos_date,
668
- :binary_fname => bfile,
669
- :offset => data_offset,
670
- :crc => d.crc,
671
- :complen => m.size,
672
- :uncomplen => d.size
673
- }
674
458
  end
675
459
 
676
- end
677
-
678
- def self.create_central_dir_headers(o, data_positions)
679
- data_positions.each do |dp|
680
- pk = [
681
- ZS_c_pk0102,
682
- ZS_c_ver_made,
683
- (dp[:folder] ? ZS_c_ver_dir : ZS_c_ver_file),
684
- ZS_c_opt_nosize,
685
- ZS_c_comp_deflate,
686
- dp[:file_dos_time],
687
- dp[:file_dos_date],
688
- dp[:crc],
689
- dp[:complen],
690
- dp[:uncomplen],
691
- dp[:binary_fname].length,
692
- ZS_c_int2_zero,
693
- ZS_c_int2_zero,
694
- ZS_c_int2_zero,
695
- ZS_c_int2_zero,
696
- (dp[:folder] ? ZS_c_oattr_dir : ZS_c_oattr_file),
697
- dp[:offset]
698
- ]
699
- bin = pk.pack('VvvvvvvVVVvvvvvVV')
700
- o << bin
701
- bin = dp[:binary_fname]
702
- o << bin
703
- end
704
- end
705
-
706
- def self.create_end_cent_dir_header(o, entry_count, dirsize, offset)
707
- pk = [
460
+ # 0506: end of central directory
461
+ def self.pk0506(entry_count, dirsize, offset)
462
+ # entry_count: count of entries in zip
463
+ # dirsize: sum of central directory sizes
464
+ # offset: byte offset of the first central directory
465
+ [
708
466
  ZS_c_pk0506,
709
467
  ZS_c_int2_zero,
710
468
  ZS_c_int2_zero,
@@ -714,6 +472,19 @@ module Zipping
714
472
  offset,
715
473
  ZS_c_int2_zero
716
474
  ]
717
- o << pk.pack('VvvvvVVv')
475
+ end
476
+
477
+ # 0708: optional trailer of entries
478
+ def self.pk0708(crc, compsize, uncompsize)
479
+ # crc: CRC32 for uncompressed data
480
+ # compsize: size of compressed data
481
+ # uncompsize: size of uncompressed data
482
+ [
483
+ ZS_c_pk0708,
484
+ crc,
485
+ compsize,
486
+ uncompsize
487
+ ]
488
+ end
718
489
  end
719
- end
490
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zipping
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.2.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shuntaro Shitasako
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-01-14 00:00:00.000000000 Z
11
+ date: 2014-07-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -114,3 +114,4 @@ summary: Compress files as a zip and output it to a stream.
114
114
  test_files:
115
115
  - spec/spec_helper.rb
116
116
  - spec/zipping_spec.rb
117
+ has_rdoc: