rack-archive-zip-extract 0.0.3 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.markdown +25 -0
- data/MIT-LICENSE +7 -0
- data/README.markdown +61 -0
- data/Rakefile +22 -0
- data/lib/rack/archive/zip/extract.rb +21 -18
- data/test/fixtures/sample-ext/sample.txt +1 -0
- data/test/fixtures/sample-zip/sample.html +3 -0
- data/test/fixtures/sample-zip/sample.txt +1 -0
- data/test/fixtures/sample-zip/sample.xml +2 -0
- data/test/fixtures/sample-zip/subdir/sample.txt +1 -0
- data/test/test_rack-archive-zip-extract.rb +126 -0
- metadata +23 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8f335e345e79786674c599d49801cfab192613ec
|
4
|
+
data.tar.gz: 05268821dfdf5582e97e7580accbcf2cbe9a187b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0d32ed10d8c653fbc13463163c94198af348cfdfcd43b9e916cf92762def4eafe2419a47e40e1a2a618ffcfdd41d9f078be5a98202d649e3b3ce3a56511e7f96
|
7
|
+
data.tar.gz: 61a4b7dc23f899ce983976f0d334b8bca21459813a6aab5e43e3b69c7cf41789aefa9bda5f139640b7c455aa48174c7e343b05eb54d0ee016195dc7f99c75ae0
|
data/CHANGELOG.markdown
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
CHANGELOG
|
2
|
+
=========
|
3
|
+
|
4
|
+
0.0.4
|
5
|
+
-----
|
6
|
+
|
7
|
+
* Change method name `extract_content` -> `extract_file`
|
8
|
+
* [BUG FIX]Close body when content not modified
|
9
|
+
* Make buffer size for reading file in zip archive configuable
|
10
|
+
|
11
|
+
0.0.3
|
12
|
+
-----
|
13
|
+
|
14
|
+
* Chunk content body and use buffer rather than read body at a time
|
15
|
+
|
16
|
+
0.0.2
|
17
|
+
-----
|
18
|
+
|
19
|
+
* Use mtime of file in archive for Last-Modified header
|
20
|
+
* Don't read file body from zip archive when file is older than If-Modified-Since header
|
21
|
+
|
22
|
+
0.0.1
|
23
|
+
-----
|
24
|
+
|
25
|
+
* Initial release
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
Copyright (c) 2013, 2014 KITAITIMAKOTO <KitaitiMakoto@gmail.com>
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
4
|
+
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
6
|
+
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.markdown
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
Rack::Archive::Zip::Extract
|
2
|
+
===========================
|
3
|
+
|
4
|
+
Rack::Archive::Zip::Extract is a Rack application which serves files in ZIP archives.
|
5
|
+
|
6
|
+
Installation
|
7
|
+
------------
|
8
|
+
|
9
|
+
$ gem install rack-archive-zip-extract
|
10
|
+
|
11
|
+
Or add this line to your application's Gemfile:
|
12
|
+
|
13
|
+
$ gem 'rack-archive-zip-extract', :require => 'rack/archive/zip/extract'
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle install
|
18
|
+
|
19
|
+
Usage
|
20
|
+
-----
|
21
|
+
|
22
|
+
Write below in your config.ru:
|
23
|
+
|
24
|
+
require 'rack/archive/zip/extract'
|
25
|
+
|
26
|
+
run Rack::Archive::Zip::Extract.new('path/to/docroot')
|
27
|
+
|
28
|
+
`'path/to/docroot'` above should have some ZIP files like this:
|
29
|
+
|
30
|
+
$ tree path/to/docroot
|
31
|
+
some.zip
|
32
|
+
another.zip
|
33
|
+
andmore.zip
|
34
|
+
|
35
|
+
Then, run server:
|
36
|
+
|
37
|
+
$ rackup
|
38
|
+
|
39
|
+
Now you can see files in zip archives. For example, visit http://localhost:9292/some/inner-file.txt and then you can see the text in "inner-file.txt" file in "some.zip" archive file.
|
40
|
+
|
41
|
+
*Note that Rack::Archive::Zip::Extract doesn't serve ZIP file itself.*
|
42
|
+
|
43
|
+
### File extensions
|
44
|
+
|
45
|
+
By default, files with extension ".zip" is recognized as ZIP files.
|
46
|
+
You can tell the app extensions by option argument:
|
47
|
+
|
48
|
+
run Rack::Archive::Zip::Extract.new('path/to/docroot', extensions: ['.epub', '.zip'])
|
49
|
+
|
50
|
+
### Buffer size
|
51
|
+
|
52
|
+
Buffer size for reading file in zip archive is set to {Rack::Archive::Zip::Extract::ExtractedFile::BUFFER_SIZE 8192} bytes by default.
|
53
|
+
|
54
|
+
You can change it by passing `buffer_size` named argument when initailizig the app:
|
55
|
+
|
56
|
+
run Rack::Archive::Zip::Extract.new('path/to/docroot', buffer_size: 1024 * 1024)
|
57
|
+
|
58
|
+
License
|
59
|
+
-------
|
60
|
+
|
61
|
+
This program is distribuetd under the term of the MIT License. See MIT-LICENSE file for more info.
|
data/Rakefile
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'rake/clean'
|
2
|
+
require 'rake/testtask'
|
3
|
+
require 'rubygems/tasks'
|
4
|
+
|
5
|
+
task :default => :test
|
6
|
+
|
7
|
+
Rake::TestTask.new do |test|
|
8
|
+
test.options = '--no-show-detail-immediately --verbose'
|
9
|
+
end
|
10
|
+
|
11
|
+
namespace :test do
|
12
|
+
desc 'Build test fixtures'
|
13
|
+
task :build do
|
14
|
+
sh 'cd test/fixtures/sample-zip && zip -r ../../sample.zip .'
|
15
|
+
sh 'cd test/fixtures/sample-ext && zip -r ../../sample.ext .'
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
task :test => [:clean, 'test:build']
|
20
|
+
|
21
|
+
Gem::Tasks.new
|
22
|
+
Gem::Tasks::Sign::Checksum.new
|
@@ -19,8 +19,7 @@ module Rack::Archive
|
|
19
19
|
SEPS = Rack::File::SEPS
|
20
20
|
DOT = '.'.freeze
|
21
21
|
DOUBLE_DOT = '..'.freeze
|
22
|
-
|
23
|
-
ALLOWED_VERBS = Rack::File::ALLOWED_VERBS
|
22
|
+
ALLOWED_METHODS = Rack::File::ALLOWED_VERBS.join(', ').freeze
|
24
23
|
ALLOW = 'Allow'.freeze
|
25
24
|
CONTENT_TYPE = 'Content-Type'.freeze
|
26
25
|
CONTENT_LENGTH = 'Content-Length'.freeze
|
@@ -29,33 +28,31 @@ module Rack::Archive
|
|
29
28
|
REQUEST_METHOD = 'REQUEST_METHOD'.freeze
|
30
29
|
PATH_INFO = 'PATH_INFO'.freeze
|
31
30
|
|
32
|
-
attr_reader :root
|
33
|
-
|
34
31
|
# @param root [Pathname, #to_path, String] path to document root
|
35
32
|
# @param extensions [Array<String>] extensions which is recognized as a zip file
|
36
33
|
# @raise [ArgumentError] if +root+ is not a directory
|
37
|
-
def initialize(root, extensions: %w[.zip])
|
34
|
+
def initialize(root, extensions: %w[.zip], buffer_size: ExtractedFile::BUFFER_SIZE)
|
38
35
|
@root = root.kind_of?(Pathname) ? root : Pathname(root)
|
39
36
|
@root = @root.expand_path
|
40
|
-
@extensions = extensions
|
37
|
+
@extensions = extensions.each {|extention| extention.freeze}.lazy
|
38
|
+
@buffer_size = buffer_size
|
41
39
|
raise ArgumentError, "Not a directory: #{@root}" unless @root.directory?
|
42
40
|
end
|
43
41
|
|
44
42
|
def call(env)
|
45
|
-
return [status_code(:method_not_allowd), {ALLOW =>
|
43
|
+
return [status_code(:method_not_allowd), {ALLOW => ALLOWED_METHODS}, []] unless Rack::File::ALLOWED_VERBS.include? env[REQUEST_METHOD]
|
46
44
|
|
47
45
|
path_info = unescape(env[PATH_INFO])
|
48
|
-
file =
|
49
|
-
@extensions.each do |ext|
|
46
|
+
file = @extensions.map {|ext|
|
50
47
|
zip_file, inner_path = find_zip_file_and_inner_path(path_info, ext)
|
51
|
-
|
52
|
-
|
53
|
-
end
|
48
|
+
extract_file(zip_file, inner_path)
|
49
|
+
}.select {|file| file}.first
|
54
50
|
return [status_code(:not_found), {}, []] if file.nil?
|
55
51
|
|
56
52
|
if_modified_since = env[IF_MODIFIED_SINCE]
|
57
53
|
if_modified_since = Time.parse(if_modified_since) if if_modified_since
|
58
54
|
if if_modified_since and file.mtime <= if_modified_since
|
55
|
+
file.close
|
59
56
|
[status_code(:not_modified), {}, []]
|
60
57
|
else
|
61
58
|
[
|
@@ -89,14 +86,14 @@ module Rack::Archive
|
|
89
86
|
# @return [Zip::File]
|
90
87
|
# @return [nil] if +zip_file_path+ is nil or +inner_path+ is empty
|
91
88
|
# @return [nil] if +inner_path+ doesn't exist in zip archive
|
92
|
-
def
|
89
|
+
def extract_file(zip_file_path, inner_path)
|
93
90
|
return if zip_file_path.nil? or inner_path.empty?
|
94
91
|
archive = ::Zip::Archive.open(zip_file_path.to_path)
|
95
92
|
if archive.locate_name(inner_path) < 0
|
96
93
|
archive.close
|
97
94
|
nil
|
98
95
|
else
|
99
|
-
ExtractedFile.new(archive,
|
96
|
+
ExtractedFile.new(archive, inner_path, @buffer_size)
|
100
97
|
end
|
101
98
|
end
|
102
99
|
|
@@ -114,14 +111,20 @@ module Rack::Archive
|
|
114
111
|
end
|
115
112
|
|
116
113
|
class ExtractedFile
|
114
|
+
BUFFER_SIZE = 8192
|
115
|
+
|
117
116
|
# @param archive [Zip::Archive]
|
118
|
-
# @param
|
119
|
-
|
120
|
-
|
117
|
+
# @param path [String]
|
118
|
+
# @raise ArgumentError when +archive+ already closed
|
119
|
+
def initialize(archive, path, buffer_size=BUFFER_SIZE)
|
120
|
+
raise ArgumentError, 'archive already closed' unless archive.open?
|
121
|
+
@archive = archive
|
122
|
+
@file = @archive.fopen(path)
|
123
|
+
@buffer_size = buffer_size
|
121
124
|
end
|
122
125
|
|
123
126
|
def each
|
124
|
-
@file.read
|
127
|
+
while chunk = @file.read(@buffer_size)
|
125
128
|
yield chunk
|
126
129
|
end
|
127
130
|
end
|
@@ -0,0 +1 @@
|
|
1
|
+
This is a plain text file in sample.ext.
|
@@ -0,0 +1 @@
|
|
1
|
+
This is a plain text file.
|
@@ -0,0 +1 @@
|
|
1
|
+
This is a plain text file in subdirectory.
|
@@ -0,0 +1,126 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'test/unit/notify'
|
3
|
+
require 'rack/lint'
|
4
|
+
require 'rack/mock'
|
5
|
+
require 'rack/archive/zip/extract'
|
6
|
+
|
7
|
+
class TestRackArchiveZipExtract < Test::Unit::TestCase
|
8
|
+
def setup
|
9
|
+
@zip = Rack::Archive::Zip::Extract.new(__dir__)
|
10
|
+
end
|
11
|
+
|
12
|
+
def request(path, app=@zip, opts={})
|
13
|
+
Rack::MockRequest.new(app).get(path, opts)
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_request_to_file_in_zip_returns_content
|
17
|
+
response = request('/sample/sample.txt')
|
18
|
+
|
19
|
+
assert_equal "This is a plain text file.\n", response.body
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_request_to_file_in_zip_returns_last_modified
|
23
|
+
response = request('/sample/sample.txt')
|
24
|
+
expected = File.mtime(File.join(__dir__, 'fixtures', 'sample-zip', 'sample.txt'))
|
25
|
+
actual = Time.parse(response['Last-Modified'])
|
26
|
+
|
27
|
+
assert_in_delta expected, actual, 2
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_request_to_old_file_returns_not_modified
|
31
|
+
mtime = File.mtime(File.join(__dir__, 'fixtures', 'sample-zip', 'sample.txt'))
|
32
|
+
if_modified_since = mtime + 12
|
33
|
+
response = request('/sample/sample.txt', @zip, {'HTTP_IF_MODIFIED_SINCE' => if_modified_since.httpdate})
|
34
|
+
|
35
|
+
assert_equal 304, response.status
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_request_to_old_file_returns_no_content
|
39
|
+
mtime = File.mtime(File.join(__dir__, 'fixtures', 'sample-zip', 'sample.txt'))
|
40
|
+
if_modified_since = mtime + 12
|
41
|
+
response = request('/sample/sample.txt', @zip, {'HTTP_IF_MODIFIED_SINCE' => if_modified_since.httpdate})
|
42
|
+
|
43
|
+
assert_empty response.body
|
44
|
+
end
|
45
|
+
|
46
|
+
class TestStatusCode < self
|
47
|
+
data(
|
48
|
+
'file in zip' => [200, '/sample/sample.txt'],
|
49
|
+
'zip file itself' => [404, '/sample.zip'],
|
50
|
+
'non-existent file in zip' => [404, '/sample/non-existent'],
|
51
|
+
'file in subdirectory' => [200, '/sample/subdir/sample.txt']
|
52
|
+
)
|
53
|
+
|
54
|
+
def test_status_code(data)
|
55
|
+
status_code, path_info = data
|
56
|
+
|
57
|
+
assert_equal status_code, request(path_info).status
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
class TestContentType < self
|
62
|
+
data(
|
63
|
+
'text/plain' => %w[.txt text/plain],
|
64
|
+
'text/html' => %w[.html text/html],
|
65
|
+
'application/xml' => %w[.xml application/xml],
|
66
|
+
'unknown extension' => %w[.unknownextension application/octet-stream],
|
67
|
+
'no extension' => ['', 'application/octet-stream'],
|
68
|
+
)
|
69
|
+
def test_content_type(data)
|
70
|
+
extension, content_type = data
|
71
|
+
response = request("/sample/sample#{extension}")
|
72
|
+
|
73
|
+
assert_equal content_type, response['Content-Type']
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def test_extension_of_file_can_be_changed
|
78
|
+
ext = Rack::Archive::Zip::Extract.new(__dir__, extensions: %w[.ext])
|
79
|
+
response = request('/sample/sample.txt', ext)
|
80
|
+
|
81
|
+
assert_equal 200, response.status
|
82
|
+
assert_equal "This is a plain text file in sample.ext.\n", response.body
|
83
|
+
end
|
84
|
+
|
85
|
+
class TestMultipleExtensions < self
|
86
|
+
def setup
|
87
|
+
super
|
88
|
+
@multi = Rack::Archive::Zip::Extract.new(__dir__, extensions: %w[.ext .zip])
|
89
|
+
end
|
90
|
+
|
91
|
+
def test_multiple_extensions_for_zip_file_can_be_specified
|
92
|
+
response = request('/sample/sample.txt', @multi)
|
93
|
+
|
94
|
+
assert_equal 200, response.status
|
95
|
+
assert_equal "This is a plain text file in sample.ext.\n", response.body
|
96
|
+
end
|
97
|
+
|
98
|
+
def test_fallback_to_next_extension_when_file_not_exit_in_zip_archive
|
99
|
+
response = request('/sample/sample.html', @multi)
|
100
|
+
|
101
|
+
assert_equal 200, response.status
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
class TestExtractedFile < self
|
106
|
+
def setup
|
107
|
+
@zip_path = 'test/sample.zip'
|
108
|
+
@inner_path = 'sample.txt'
|
109
|
+
@file = Rack::Archive::Zip::Extract::ExtractedFile.new(Zip::Archive.open(@zip_path), @inner_path)
|
110
|
+
end
|
111
|
+
|
112
|
+
# If body of Rack application responds to method `to_path`,
|
113
|
+
# it is used to specify file path on file system when
|
114
|
+
# Rack::Senfile or so is stacked on Rack builder.
|
115
|
+
def test_not_respond_to_to_path
|
116
|
+
assert_false @file.respond_to? :to_path
|
117
|
+
end
|
118
|
+
|
119
|
+
def test_cannot_initialize_with_closed_archive
|
120
|
+
archive = Zip::Archive.open(@zip_path)
|
121
|
+
archive.close
|
122
|
+
assert_raise ArgumentError do Rack::Archive::Zip::Extract::ExtractedFile.new archive, @inner_path
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rack-archive-zip-extract
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- KITAITI Makoto
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-01-
|
11
|
+
date: 2014-01-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rack
|
@@ -98,9 +98,22 @@ description: Rack::Archive::Zip::Extract serves files in zip archives.
|
|
98
98
|
email: KitaitiMakoto@gmail.com
|
99
99
|
executables: []
|
100
100
|
extensions: []
|
101
|
-
extra_rdoc_files:
|
101
|
+
extra_rdoc_files:
|
102
|
+
- README.markdown
|
103
|
+
- MIT-LICENSE
|
104
|
+
- CHANGELOG.markdown
|
102
105
|
files:
|
106
|
+
- CHANGELOG.markdown
|
107
|
+
- MIT-LICENSE
|
108
|
+
- README.markdown
|
109
|
+
- Rakefile
|
103
110
|
- lib/rack/archive/zip/extract.rb
|
111
|
+
- test/fixtures/sample-ext/sample.txt
|
112
|
+
- test/fixtures/sample-zip/sample.html
|
113
|
+
- test/fixtures/sample-zip/sample.txt
|
114
|
+
- test/fixtures/sample-zip/sample.xml
|
115
|
+
- test/fixtures/sample-zip/subdir/sample.txt
|
116
|
+
- test/test_rack-archive-zip-extract.rb
|
104
117
|
homepage: https://github.com/KitaitiMakoto/rack-archive-zip-extract
|
105
118
|
licenses:
|
106
119
|
- MIT
|
@@ -125,4 +138,10 @@ rubygems_version: 2.2.0
|
|
125
138
|
signing_key:
|
126
139
|
specification_version: 4
|
127
140
|
summary: Zip file server
|
128
|
-
test_files:
|
141
|
+
test_files:
|
142
|
+
- test/test_rack-archive-zip-extract.rb
|
143
|
+
- test/fixtures/sample-ext/sample.txt
|
144
|
+
- test/fixtures/sample-zip/sample.html
|
145
|
+
- test/fixtures/sample-zip/sample.txt
|
146
|
+
- test/fixtures/sample-zip/sample.xml
|
147
|
+
- test/fixtures/sample-zip/subdir/sample.txt
|