rack-raw-upload 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source :rubygems
2
+
3
+ gem 'json'
4
+
5
+ group :development, :test do
6
+ gem 'rake'
7
+ gem 'rack-test'
8
+ gem 'shoulda'
9
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,18 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ json (1.4.6)
5
+ rack (1.2.1)
6
+ rack-test (0.5.6)
7
+ rack (>= 1.0)
8
+ rake (0.8.7)
9
+ shoulda (2.11.3)
10
+
11
+ PLATFORMS
12
+ ruby
13
+
14
+ DEPENDENCIES
15
+ json
16
+ rack-test
17
+ rake
18
+ shoulda
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
+