rack-raw-upload 0.1.0
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.
- data/Gemfile +9 -0
- data/Gemfile.lock +18 -0
- data/LICENSE +20 -0
- data/README.md +53 -0
- data/lib/rack/raw_upload.rb +80 -0
- data/test/raw_upload_test.rb +185 -0
- metadata +130 -0
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010 New Bamboo Web Development Ltd
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
# Rack Raw Upload middleware
|
2
|
+
|
3
|
+
Rack::RawUpload converts raw file uploads into normal form input, so Rack applications can read these as normal (using `params` for example), rather than from `env['rack.input']` or similar.
|
4
|
+
|
5
|
+
Rack::RawUpload know that a request is such an upload when the mimetype **is not** one of the following:
|
6
|
+
|
7
|
+
* application/x-www-form-urlencoded
|
8
|
+
* multipart/form-data
|
9
|
+
|
10
|
+
Additionally, it can be told explicitly to perform the conversion, using the header `X-File-Upload`. See below for details.
|
11
|
+
|
12
|
+
## Assumptions
|
13
|
+
|
14
|
+
Rack::RawUpload expects that requests will:
|
15
|
+
|
16
|
+
1. be POST requests
|
17
|
+
2. set the mimetype `application/octet-stream`
|
18
|
+
|
19
|
+
|
20
|
+
## Configuration
|
21
|
+
|
22
|
+
The simpler case:
|
23
|
+
|
24
|
+
use Rack::RawUpload
|
25
|
+
|
26
|
+
If you want to limit the conversion to a few known paths, do:
|
27
|
+
|
28
|
+
use Rack::RawUpload, :paths => ['/upload/path', '/alternative/path.*']
|
29
|
+
|
30
|
+
You can also make it so that the conversion only happens when explicitly required by the client using a header. This would be `X-File-Upload: true` to make the conversion regardless of the content type. A value of `X-File-Upload: smart` would ask for the normal detection to be performed. For this, use the following setting:
|
31
|
+
|
32
|
+
use Rack::RawUpload, :explicit => true
|
33
|
+
|
34
|
+
|
35
|
+
## More options
|
36
|
+
|
37
|
+
### Specifying the file name of the upload
|
38
|
+
|
39
|
+
Raw uploads, due to their own nature, don't include the name of the file being uploaded. You can work around this limitation by specifying the filename as an HTTP header.
|
40
|
+
|
41
|
+
When present, Rack::RawUpload will assume that the header ***`X-File-Name`*** will contain the filename.
|
42
|
+
|
43
|
+
### Additional query parameters
|
44
|
+
|
45
|
+
Again, the nature of raw uploads prevents us from sending additional parameters along with the file. As a workaround, you can specify there as a header too. They will be made available as normal parameters.
|
46
|
+
|
47
|
+
When present, Rack::RawUpload will assume that the header ***`X-Query-Params`*** contains these additional parameters. The values are expected to be in the form of a **JSON** hash.
|
48
|
+
|
49
|
+
## Additional info
|
50
|
+
|
51
|
+
A blog post on HTML5 uploads, which are raw uploads, and can be greatly simplified with this middleware:
|
52
|
+
|
53
|
+
* [http://blog.new-bamboo.co.uk/2010/7/30/html5-powered-ajax-file-uploads](http://blog.new-bamboo.co.uk/2010/7/30/html5-powered-ajax-file-uploads)
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module Rack
|
2
|
+
class RawUpload
|
3
|
+
|
4
|
+
VERSION = '0.1.0'
|
5
|
+
|
6
|
+
def initialize(app, opts = {})
|
7
|
+
@app = app
|
8
|
+
@paths = opts[:paths]
|
9
|
+
@explicit = opts[:explicit]
|
10
|
+
@tmpdir = opts[:tmpdir] || Dir::tmpdir
|
11
|
+
@paths = [@paths] if @paths.kind_of?(String)
|
12
|
+
end
|
13
|
+
|
14
|
+
def call(env)
|
15
|
+
kick_in?(env) ? convert_and_pass_on(env) : @app.call(env)
|
16
|
+
end
|
17
|
+
|
18
|
+
def upload_path?(request_path)
|
19
|
+
return true if @paths.nil?
|
20
|
+
|
21
|
+
@paths.any? do |candidate|
|
22
|
+
literal_path_match?(request_path, candidate) || wildcard_path_match?(request_path, candidate)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def convert_and_pass_on(env)
|
30
|
+
tempfile = Tempfile.new('raw-upload.', @tmpdir)
|
31
|
+
tempfile = open(tempfile.path, "r+:BINARY")
|
32
|
+
tempfile << env['rack.input'].read
|
33
|
+
tempfile.flush
|
34
|
+
tempfile.rewind
|
35
|
+
fake_file = {
|
36
|
+
:filename => env['HTTP_X_FILE_NAME'],
|
37
|
+
:type => env['CONTENT_TYPE'],
|
38
|
+
:tempfile => tempfile,
|
39
|
+
}
|
40
|
+
env['rack.request.form_input'] = env['rack.input']
|
41
|
+
env['rack.request.form_hash'] ||= {}
|
42
|
+
env['rack.request.query_hash'] ||= {}
|
43
|
+
env['rack.request.form_hash']['file'] = fake_file
|
44
|
+
env['rack.request.query_hash']['file'] = fake_file
|
45
|
+
if query_params = env['HTTP_X_QUERY_PARAMS']
|
46
|
+
require 'json'
|
47
|
+
params = JSON.parse(query_params)
|
48
|
+
env['rack.request.form_hash'].merge!(params)
|
49
|
+
env['rack.request.query_hash'].merge!(params)
|
50
|
+
end
|
51
|
+
@app.call(env)
|
52
|
+
end
|
53
|
+
|
54
|
+
def kick_in?(env)
|
55
|
+
env['HTTP_X_FILE_UPLOAD'] == 'true' ||
|
56
|
+
! @explicit && env['HTTP_X_FILE_UPLOAD'] != 'false' && raw_file_post?(env) ||
|
57
|
+
env.has_key?('HTTP_X_FILE_UPLOAD') && env['HTTP_X_FILE_UPLOAD'] != 'false' && raw_file_post?(env)
|
58
|
+
end
|
59
|
+
|
60
|
+
def raw_file_post?(env)
|
61
|
+
upload_path?(env['PATH_INFO']) &&
|
62
|
+
env['REQUEST_METHOD'] == 'POST' &&
|
63
|
+
content_type_of_raw_file?(env['CONTENT_TYPE'])
|
64
|
+
end
|
65
|
+
|
66
|
+
def literal_path_match?(request_path, candidate)
|
67
|
+
candidate == request_path
|
68
|
+
end
|
69
|
+
|
70
|
+
def wildcard_path_match?(request_path, candidate)
|
71
|
+
return false unless candidate.include?('*')
|
72
|
+
regexp = '^' + candidate.gsub('.', '\.').gsub('*', '[^/]*') + '$'
|
73
|
+
!! (Regexp.new(regexp) =~ request_path)
|
74
|
+
end
|
75
|
+
|
76
|
+
def content_type_of_raw_file?(content_type)
|
77
|
+
! %w{application/x-www-form-urlencoded multipart/form-data}.include?(content_type)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,185 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rack/test'
|
3
|
+
require 'shoulda'
|
4
|
+
require 'rack/raw_upload'
|
5
|
+
require 'json'
|
6
|
+
|
7
|
+
class RawUploadTest < Test::Unit::TestCase
|
8
|
+
include Rack::Test::Methods
|
9
|
+
|
10
|
+
def app
|
11
|
+
opts = @middleware_opts
|
12
|
+
Rack::Builder.new do
|
13
|
+
use Rack::RawUpload, opts
|
14
|
+
run Proc.new { |env| [200, {'Content-Type' => 'text/html'}, ['success']] }
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def setup
|
19
|
+
@middleware_opts = {}
|
20
|
+
@path = __FILE__
|
21
|
+
@filename = File.basename(@path)
|
22
|
+
@file = File.open(@path)
|
23
|
+
end
|
24
|
+
|
25
|
+
def upload(env = {})
|
26
|
+
env = {
|
27
|
+
'REQUEST_METHOD' => 'POST',
|
28
|
+
'CONTENT_TYPE' => 'application/octet-stream',
|
29
|
+
'PATH_INFO' => '/some/path',
|
30
|
+
'rack.input' => @file,
|
31
|
+
}.merge(env)
|
32
|
+
request(env['PATH_INFO'], env)
|
33
|
+
end
|
34
|
+
|
35
|
+
context "raw file upload" do
|
36
|
+
should "work with Content-Type 'application/octet-stream'" do
|
37
|
+
upload('CONTENT_TYPE' => 'application/octet-stream')
|
38
|
+
assert_file_uploaded_as 'application/octet-stream'
|
39
|
+
end
|
40
|
+
|
41
|
+
should "work with Content-Type 'image/jpeg'" do
|
42
|
+
upload('CONTENT_TYPE' => 'image/jpeg')
|
43
|
+
assert_file_uploaded_as 'image/jpeg'
|
44
|
+
end
|
45
|
+
|
46
|
+
should "not work with Content-Type 'application/x-www-form-urlencoded'" do
|
47
|
+
upload('CONTENT_TYPE' => 'application/x-www-form-urlencoded')
|
48
|
+
assert_successful_non_upload
|
49
|
+
end
|
50
|
+
|
51
|
+
should "not work with Content-Type 'multipart/form-data'" do
|
52
|
+
upload('CONTENT_TYPE' => 'multipart/form-data')
|
53
|
+
assert_successful_non_upload
|
54
|
+
end
|
55
|
+
|
56
|
+
should "be forced to perform a file upload if `X-File-Upload: true`" do
|
57
|
+
upload('CONTENT_TYPE' => 'multipart/form-data', 'HTTP_X_FILE_UPLOAD' => 'true')
|
58
|
+
assert_file_uploaded_as 'multipart/form-data'
|
59
|
+
end
|
60
|
+
|
61
|
+
should "not perform a file upload if `X-File-Upload: false`" do
|
62
|
+
upload('CONTENT_TYPE' => 'image/jpeg', 'HTTP_X_FILE_UPLOAD' => 'false')
|
63
|
+
assert_successful_non_upload
|
64
|
+
end
|
65
|
+
|
66
|
+
context "with X-File-Upload: smart" do
|
67
|
+
should "perform a file upload if appropriate" do
|
68
|
+
upload('CONTENT_TYPE' => 'multipart/form-data', 'HTTP_X_FILE_UPLOAD' => 'smart')
|
69
|
+
assert_successful_non_upload
|
70
|
+
end
|
71
|
+
|
72
|
+
should "not perform a file upload if not appropriate" do
|
73
|
+
upload('CONTENT_TYPE' => 'image/jpeg', 'HTTP_X_FILE_UPLOAD' => 'smart')
|
74
|
+
assert_file_uploaded_as 'image/jpeg'
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
context "with :explicit => true" do
|
79
|
+
setup do
|
80
|
+
@middleware_opts = { :explicit => true }
|
81
|
+
end
|
82
|
+
|
83
|
+
should "not be triggered by an appropriate Content-Type" do
|
84
|
+
upload('CONTENT_TYPE' => 'image/jpeg')
|
85
|
+
assert_successful_non_upload
|
86
|
+
end
|
87
|
+
|
88
|
+
should "be triggered by `X-File-Upload: true`" do
|
89
|
+
upload('CONTENT_TYPE' => 'image/jpeg', 'HTTP_X_FILE_UPLOAD' => 'true')
|
90
|
+
assert_file_uploaded_as 'image/jpeg'
|
91
|
+
end
|
92
|
+
|
93
|
+
should "kick in when `X-File-Upload: smart` and the request is an upload" do
|
94
|
+
upload('CONTENT_TYPE' => 'image/jpeg', 'HTTP_X_FILE_UPLOAD' => 'smart')
|
95
|
+
assert_file_uploaded_as 'image/jpeg'
|
96
|
+
end
|
97
|
+
|
98
|
+
should "stay put when `X-File-Upload: smart` and the request is not an upload" do
|
99
|
+
upload('CONTENT_TYPE' => 'multipart/form-data', 'HTTP_X_FILE_UPLOAD' => 'smart')
|
100
|
+
assert_successful_non_upload
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
context "with a given :tmpdir" do
|
105
|
+
setup do
|
106
|
+
@tmp_path = File.join(Dir::tmpdir, 'rack-raw-upload/some-dir')
|
107
|
+
FileUtils.mkdir_p(@tmp_path)
|
108
|
+
@middleware_opts = { :tmpdir => @tmp_path }
|
109
|
+
end
|
110
|
+
|
111
|
+
should "use it as temporary file store" do
|
112
|
+
upload
|
113
|
+
assert Dir.entries(@tmp_path).any?{|node| node =~ /raw-upload/ }
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
context "with query parameters" do
|
118
|
+
setup do
|
119
|
+
upload('HTTP_X_QUERY_PARAMS' => JSON.generate({
|
120
|
+
:argument => 'value1',
|
121
|
+
'argument with spaces' => 'value 2'
|
122
|
+
}))
|
123
|
+
end
|
124
|
+
|
125
|
+
should "convert these into arguments" do
|
126
|
+
assert_equal last_request.POST['argument'], 'value1'
|
127
|
+
assert_equal last_request.POST['argument with spaces'], 'value 2'
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
context "with filename" do
|
132
|
+
setup do
|
133
|
+
upload('HTTP_X_FILE_NAME' => @filename)
|
134
|
+
end
|
135
|
+
|
136
|
+
should "be transformed into a normal form upload" do
|
137
|
+
assert_equal @filename, last_request.POST["file"][:filename]
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
context "path matcher" do
|
143
|
+
should "accept any path by default" do
|
144
|
+
rru = Rack::RawUpload.new(nil)
|
145
|
+
assert rru.upload_path?('/')
|
146
|
+
assert rru.upload_path?('/resources.json')
|
147
|
+
assert rru.upload_path?('/resources/stuff.json')
|
148
|
+
end
|
149
|
+
|
150
|
+
should "accept literal paths" do
|
151
|
+
rru = Rack::RawUpload.new nil, :paths => '/resources.json'
|
152
|
+
assert rru.upload_path?('/resources.json')
|
153
|
+
assert ! rru.upload_path?('/resources.html')
|
154
|
+
end
|
155
|
+
|
156
|
+
should "accept paths with wildcards" do
|
157
|
+
rru = Rack::RawUpload.new nil, :paths => '/resources.*'
|
158
|
+
assert rru.upload_path?('/resources.json')
|
159
|
+
assert rru.upload_path?('/resources.*')
|
160
|
+
assert ! rru.upload_path?('/resource.json')
|
161
|
+
assert ! rru.upload_path?('/resourcess.json')
|
162
|
+
assert ! rru.upload_path?('/resources.json/blah')
|
163
|
+
end
|
164
|
+
|
165
|
+
should "accept several entries" do
|
166
|
+
rru = Rack::RawUpload.new nil, :paths => ['/resources.*', '/uploads']
|
167
|
+
assert rru.upload_path?('/uploads')
|
168
|
+
assert rru.upload_path?('/resources.*')
|
169
|
+
assert ! rru.upload_path?('/upload')
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
def assert_file_uploaded_as(file_type)
|
174
|
+
file = File.open(@path)
|
175
|
+
received = last_request.POST["file"]
|
176
|
+
assert_equal file.gets, received[:tempfile].gets
|
177
|
+
assert_equal file_type, received[:type]
|
178
|
+
assert last_response.ok?
|
179
|
+
end
|
180
|
+
|
181
|
+
def assert_successful_non_upload
|
182
|
+
assert ! last_request.POST.has_key?('file')
|
183
|
+
assert last_response.ok?
|
184
|
+
end
|
185
|
+
end
|
metadata
ADDED
@@ -0,0 +1,130 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rack-raw-upload
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 27
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
- 0
|
10
|
+
version: 0.1.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Pablo Brasero
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-01-09 00:00:00 +00:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: json
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 3
|
30
|
+
segments:
|
31
|
+
- 0
|
32
|
+
version: "0"
|
33
|
+
type: :runtime
|
34
|
+
version_requirements: *id001
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: rake
|
37
|
+
prerelease: false
|
38
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
hash: 3
|
44
|
+
segments:
|
45
|
+
- 0
|
46
|
+
version: "0"
|
47
|
+
type: :development
|
48
|
+
version_requirements: *id002
|
49
|
+
- !ruby/object:Gem::Dependency
|
50
|
+
name: rack-test
|
51
|
+
prerelease: false
|
52
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
53
|
+
none: false
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
hash: 3
|
58
|
+
segments:
|
59
|
+
- 0
|
60
|
+
version: "0"
|
61
|
+
type: :development
|
62
|
+
version_requirements: *id003
|
63
|
+
- !ruby/object:Gem::Dependency
|
64
|
+
name: shoulda
|
65
|
+
prerelease: false
|
66
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
67
|
+
none: false
|
68
|
+
requirements:
|
69
|
+
- - ">="
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
hash: 3
|
72
|
+
segments:
|
73
|
+
- 0
|
74
|
+
version: "0"
|
75
|
+
type: :development
|
76
|
+
version_requirements: *id004
|
77
|
+
description: Middleware that converts files uploaded with mimetype application/octet-stream into normal form input, so Rack applications can read these as normal, rather than as raw input.
|
78
|
+
email: pablobm@gmail.com
|
79
|
+
executables: []
|
80
|
+
|
81
|
+
extensions: []
|
82
|
+
|
83
|
+
extra_rdoc_files:
|
84
|
+
- LICENSE
|
85
|
+
- README.md
|
86
|
+
files:
|
87
|
+
- lib/rack/raw_upload.rb
|
88
|
+
- test/raw_upload_test.rb
|
89
|
+
- LICENSE
|
90
|
+
- README.md
|
91
|
+
- Gemfile
|
92
|
+
- Gemfile.lock
|
93
|
+
has_rdoc: true
|
94
|
+
homepage: https://github.com/newbamboo/rack-raw-upload
|
95
|
+
licenses: []
|
96
|
+
|
97
|
+
post_install_message:
|
98
|
+
rdoc_options:
|
99
|
+
- --charset=UTF-8
|
100
|
+
- --main
|
101
|
+
- README.rdoc
|
102
|
+
require_paths:
|
103
|
+
- lib
|
104
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ">="
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
hash: 3
|
110
|
+
segments:
|
111
|
+
- 0
|
112
|
+
version: "0"
|
113
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
114
|
+
none: false
|
115
|
+
requirements:
|
116
|
+
- - ">="
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
hash: 3
|
119
|
+
segments:
|
120
|
+
- 0
|
121
|
+
version: "0"
|
122
|
+
requirements: []
|
123
|
+
|
124
|
+
rubyforge_project:
|
125
|
+
rubygems_version: 1.3.7
|
126
|
+
signing_key:
|
127
|
+
specification_version: 3
|
128
|
+
summary: Rack Raw Upload middleware
|
129
|
+
test_files: []
|
130
|
+
|