butler_static 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/Guardfile +14 -0
- data/LICENSE.txt +22 -0
- data/README.md +29 -0
- data/Rakefile +12 -0
- data/butler.gemspec +31 -0
- data/lib/butler/asset.rb +147 -0
- data/lib/butler/handler.rb +92 -0
- data/lib/butler/railtie.rb +6 -0
- data/lib/butler/static.rb +97 -0
- data/lib/butler/version.rb +3 -0
- data/lib/butler.rb +9 -0
- data/spec/butler/asset_spec.rb +226 -0
- data/spec/butler/files/index.html +1 -0
- data/spec/minitest_helper.rb +17 -0
- data/test/butler/fixtures/public/.gitignore +1 -0
- data/test/butler/fixtures/public/404.html +1 -0
- data/test/butler/fixtures/public/500.da.html +1 -0
- data/test/butler/fixtures/public/500.html +1 -0
- data/test/butler/fixtures/public/elsewhere/cools.js +1 -0
- data/test/butler/fixtures/public/elsewhere/file.css +1 -0
- data/test/butler/fixtures/public/foo/bar.html +1 -0
- data/test/butler/fixtures/public/foo/baz.css +3 -0
- data/test/butler/fixtures/public/foo/index.html +1 -0
- data/test/butler/fixtures/public/foo//343/201/223/343/202/223/343/201/253/343/201/241/343/201/257.html +1 -0
- data/test/butler/fixtures/public/images/rails.png +0 -0
- data/test/butler/fixtures/public/index.html +1 -0
- data/test/butler/fixtures/public/javascripts/application.js +1 -0
- data/test/butler/fixtures/public/javascripts/bank.js +1 -0
- data/test/butler/fixtures/public/javascripts/common.javascript +1 -0
- data/test/butler/fixtures/public/javascripts/controls.js +1 -0
- data/test/butler/fixtures/public/javascripts/dragdrop.js +1 -0
- data/test/butler/fixtures/public/javascripts/effects.js +1 -0
- data/test/butler/fixtures/public/javascripts/prototype.js +1 -0
- data/test/butler/fixtures/public/javascripts/robber.js +1 -0
- data/test/butler/fixtures/public/javascripts/subdir/subdir.js +1 -0
- data/test/butler/fixtures/public/javascripts/version.1.0.js +1 -0
- data/test/butler/fixtures/public/stylesheets/bank.css +1 -0
- data/test/butler/fixtures/public/stylesheets/random.styles +1 -0
- data/test/butler/fixtures/public/stylesheets/robber.css +1 -0
- data/test/butler/fixtures/public/stylesheets/subdir/subdir.css +1 -0
- data/test/butler/fixtures/public/stylesheets/version.1.0.css +1 -0
- data/test/butler/fixtures/public/stylesheets/webfont.eot +1 -0
- data/test/butler/static_test.rb +195 -0
- metadata +221 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# A sample Guardfile
|
2
|
+
# More info at https://github.com/guard/guard#readme
|
3
|
+
|
4
|
+
guard 'minitest' do
|
5
|
+
# with Minitest::Unit
|
6
|
+
watch(%r|^test/(.*)test\.rb|)
|
7
|
+
watch(%r|^lib/(.*)([^/]+)\.rb|) { |m| "test/#{m[1]}#{m[2]}_test.rb" }
|
8
|
+
watch(%r|^test/test_helper\.rb|) { "test" }
|
9
|
+
|
10
|
+
# with Minitest::Spec
|
11
|
+
watch(%r|^spec/(.*)_spec\.rb|)
|
12
|
+
watch(%r|^lib/(.*)([^/]+)\.rb|) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }
|
13
|
+
watch(%r|^spec/spec_helper\.rb|) { "spec" }
|
14
|
+
end
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Thomas Klemm
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# Butler
|
2
|
+
|
3
|
+
TODO: Write a gem description
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'butler_static'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install butler_static
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
TODO: Write usage instructions here
|
22
|
+
|
23
|
+
## Contributing
|
24
|
+
|
25
|
+
1. Fork it
|
26
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
27
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
28
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
29
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/butler.gemspec
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'butler/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "butler_static"
|
8
|
+
gem.version = Butler::VERSION
|
9
|
+
gem.authors = ["Thomas Klemm"]
|
10
|
+
gem.email = ["github@tklemm.eu"]
|
11
|
+
gem.description = "Butler is a Rack Middleware that serves static assets for your Rails app."
|
12
|
+
gem.summary = "Butler is a Rack Middleware that serves static assets for your Rails app. It allows you to set HTTP headers for individual files or folders based on rules."
|
13
|
+
gem.homepage = "https://github.com/thomasklemm/butler"
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($/)
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
|
+
gem.require_paths = ["lib"]
|
19
|
+
|
20
|
+
# Dependencies
|
21
|
+
gem.add_dependency 'rack'
|
22
|
+
|
23
|
+
# Tests
|
24
|
+
gem.add_development_dependency 'minitest'
|
25
|
+
gem.add_development_dependency 'guard-minitest'
|
26
|
+
gem.add_development_dependency 'turn'
|
27
|
+
# Only ActiveSupport and ActionController nescessary
|
28
|
+
# gem.add_development_dependency 'rails', '~> 3.2.8'
|
29
|
+
gem.add_development_dependency 'activesupport', '~> 3.2.8'
|
30
|
+
gem.add_development_dependency 'actionpack', '~> 3.2.8'
|
31
|
+
end
|
data/lib/butler/asset.rb
ADDED
@@ -0,0 +1,147 @@
|
|
1
|
+
require 'time'
|
2
|
+
require 'rack/utils'
|
3
|
+
require 'rack/mime'
|
4
|
+
|
5
|
+
module Butler
|
6
|
+
#
|
7
|
+
# Butler::Asset
|
8
|
+
#
|
9
|
+
# Sends a single requested file out to the user
|
10
|
+
# The code has been largely taken from Rack::File
|
11
|
+
# but adjusted to support sending custuom HTTP headers
|
12
|
+
#
|
13
|
+
# Usage:
|
14
|
+
# Butler::Asset.new(root_directory, headers: {
|
15
|
+
# 'Cache-Control' => 'public, max-age=31536000',
|
16
|
+
# 'Some custom header' => 'Content for some custom header'
|
17
|
+
# })
|
18
|
+
#
|
19
|
+
class Asset
|
20
|
+
SEPS = Regexp.union(*[File::SEPARATOR, File::ALT_SEPARATOR].compact)
|
21
|
+
ALLOWED_VERBS = %w[GET HEAD]
|
22
|
+
|
23
|
+
attr_accessor :root, :path
|
24
|
+
alias :to_path :path
|
25
|
+
|
26
|
+
def initialize(root, options={})
|
27
|
+
@root = root
|
28
|
+
@headers = options[:headers] || {}
|
29
|
+
end
|
30
|
+
|
31
|
+
def call(env)
|
32
|
+
dup._call(env)
|
33
|
+
end
|
34
|
+
|
35
|
+
F = File
|
36
|
+
|
37
|
+
def _call(env)
|
38
|
+
unless ALLOWED_VERBS.include? env["REQUEST_METHOD"]
|
39
|
+
return fail(405, "Method Not Allowed")
|
40
|
+
end
|
41
|
+
|
42
|
+
path_info = Rack::Utils.unescape(env["PATH_INFO"])
|
43
|
+
parts = path_info.split SEPS
|
44
|
+
|
45
|
+
parts.inject(0) do |depth, part|
|
46
|
+
case part
|
47
|
+
when '', '.'
|
48
|
+
depth
|
49
|
+
when '..'
|
50
|
+
return fail(404, "Not Found") if depth - 1 < 0
|
51
|
+
depth - 1
|
52
|
+
else
|
53
|
+
depth + 1
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
@path = F.join(@root, *parts)
|
58
|
+
|
59
|
+
available = begin
|
60
|
+
F.file?(@path) && F.readable?(@path)
|
61
|
+
rescue SystemCallError
|
62
|
+
false
|
63
|
+
end
|
64
|
+
|
65
|
+
if available
|
66
|
+
serving(env)
|
67
|
+
else
|
68
|
+
fail(404, "File not found: #{path_info}")
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def serving(env)
|
73
|
+
last_modified = F.mtime(@path).httpdate
|
74
|
+
return [304, {}, []] if env['HTTP_IF_MODIFIED_SINCE'] == last_modified
|
75
|
+
response = [
|
76
|
+
200,
|
77
|
+
{
|
78
|
+
"Last-Modified" => last_modified,
|
79
|
+
"Content-Type" => Rack::Mime.mime_type(F.extname(@path), 'text/plain')
|
80
|
+
},
|
81
|
+
env["REQUEST_METHOD"] == "HEAD" ? [] : self
|
82
|
+
]
|
83
|
+
|
84
|
+
# Set headers
|
85
|
+
@headers.each { |field, content| response[1][field] = content } if @headers
|
86
|
+
|
87
|
+
# NOTE:
|
88
|
+
# We check via File::size? whether this file provides size info
|
89
|
+
# via stat (e.g. /proc files often don't), otherwise we have to
|
90
|
+
# figure it out by reading the whole file into memory.
|
91
|
+
size = F.size?(@path) || Utils.bytesize(F.read(@path))
|
92
|
+
|
93
|
+
ranges = Rack::Utils.byte_ranges(env, size)
|
94
|
+
if ranges.nil? || ranges.length > 1
|
95
|
+
# No ranges, or multiple ranges (which we don't support):
|
96
|
+
# TODO: Support multiple byte-ranges
|
97
|
+
response[0] = 200
|
98
|
+
@range = 0..size-1
|
99
|
+
elsif ranges.empty?
|
100
|
+
# Unsatisfiable. Return error, and file size:
|
101
|
+
response = fail(416, "Byte range unsatisfiable")
|
102
|
+
response[1]["Content-Range"] = "bytes */#{size}"
|
103
|
+
return response
|
104
|
+
else
|
105
|
+
# Partial content:
|
106
|
+
@range = ranges[0]
|
107
|
+
response[0] = 206
|
108
|
+
response[1]["Content-Range"] =
|
109
|
+
"bytes #{@range.begin}-#{@range.end}/#{size}"
|
110
|
+
size = @range.end - @range.begin + 1
|
111
|
+
end
|
112
|
+
|
113
|
+
response[1]["Content-Length"] = size.to_s
|
114
|
+
response
|
115
|
+
end
|
116
|
+
|
117
|
+
def each
|
118
|
+
F.open(@path, "rb") do |file|
|
119
|
+
file.seek(@range.begin)
|
120
|
+
remaining_len = @range.end-@range.begin+1
|
121
|
+
while remaining_len > 0
|
122
|
+
part = file.read([8192, remaining_len].min)
|
123
|
+
break unless part
|
124
|
+
remaining_len -= part.length
|
125
|
+
|
126
|
+
yield part
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
private
|
132
|
+
|
133
|
+
def fail(status, body)
|
134
|
+
body += "\n"
|
135
|
+
[
|
136
|
+
status,
|
137
|
+
{
|
138
|
+
"Content-Type" => "text/plain",
|
139
|
+
"Content-Length" => body.size.to_s,
|
140
|
+
"X-Cascade" => "pass"
|
141
|
+
},
|
142
|
+
[body]
|
143
|
+
]
|
144
|
+
end
|
145
|
+
|
146
|
+
end
|
147
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'rack/utils'
|
2
|
+
require 'active_support/core_ext/uri'
|
3
|
+
require 'butler/asset'
|
4
|
+
|
5
|
+
module Butler
|
6
|
+
#
|
7
|
+
# Butler::Handler
|
8
|
+
#
|
9
|
+
# The link between Butler::Static and Butler::Asset
|
10
|
+
# Transforms HTTP header rules into actual HTTP headers
|
11
|
+
# for a single file to be set by Butler::Asset
|
12
|
+
#
|
13
|
+
# Code adapted from Rails' ActionDispatch::FileHandler
|
14
|
+
#
|
15
|
+
class Handler
|
16
|
+
def initialize(root, options={})
|
17
|
+
@root = root.chomp('/')
|
18
|
+
@compiled_root = /^#{Regexp.escape(root)}/
|
19
|
+
@headers = {}
|
20
|
+
@header_rules = options[:header_rules] || {}
|
21
|
+
@file_server = Butler::Asset.new(@root, headers: @headers)
|
22
|
+
end
|
23
|
+
|
24
|
+
def match?(path)
|
25
|
+
path = path.dup
|
26
|
+
|
27
|
+
full_path = path.empty? ? @root : File.join(@root,
|
28
|
+
escape_glob_chars(unescape_path(path)))
|
29
|
+
paths = "#{full_path}#{ext}"
|
30
|
+
|
31
|
+
matches = Dir[paths]
|
32
|
+
match = matches.detect { |m| File.file?(m) }
|
33
|
+
if match
|
34
|
+
match.sub!(@compiled_root, '')
|
35
|
+
::Rack::Utils.escape(match)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def call(env)
|
40
|
+
@path = env['PATH_INFO']
|
41
|
+
set_headers
|
42
|
+
@file_server.call(env)
|
43
|
+
end
|
44
|
+
|
45
|
+
def ext
|
46
|
+
@ext ||= begin
|
47
|
+
ext = ::ActionController::Base.page_cache_extension
|
48
|
+
"{,#{ext},/index#{ext}}"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def unescape_path(path)
|
53
|
+
URI.parser.unescape(path)
|
54
|
+
end
|
55
|
+
|
56
|
+
def escape_glob_chars(path)
|
57
|
+
path.force_encoding('binary') if path.respond_to? :force_encoding
|
58
|
+
path.gsub(/[*?{}\[\]]/, "\\\\\\&")
|
59
|
+
end
|
60
|
+
|
61
|
+
# Convert header rules to headers
|
62
|
+
def set_headers
|
63
|
+
@header_rules.each do |rule, result|
|
64
|
+
case rule
|
65
|
+
when :global
|
66
|
+
set_header(result)
|
67
|
+
when :fonts
|
68
|
+
set_header(result) if @path.match(%r{\.(?:ttf|otf|eot|woff|svg)\z})
|
69
|
+
when Regexp
|
70
|
+
set_header(result) if @path.match(rule)
|
71
|
+
when Array
|
72
|
+
# Extensions
|
73
|
+
extensions = rule.join('|')
|
74
|
+
set_header(result) if @path.match(%r{\.(#{extensions})\z})
|
75
|
+
when String
|
76
|
+
# Folder
|
77
|
+
path = ::Rack::Utils.unescape(@path)
|
78
|
+
set_header(result) if
|
79
|
+
(path.start_with?(rule) || path.start_with?('/' + rule))
|
80
|
+
else
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def set_header(result)
|
86
|
+
result.each do |field, content|
|
87
|
+
@headers[field] = content
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'rack/utils'
|
2
|
+
require 'butler/handler'
|
3
|
+
|
4
|
+
module Butler
|
5
|
+
#
|
6
|
+
# Butler::Static
|
7
|
+
#
|
8
|
+
# is a Static Web Server based on ActionDispatch::Static.
|
9
|
+
# It is intended to be used with Rails in an environment
|
10
|
+
# where serving static assets through a specialized web
|
11
|
+
# server such as nginx is technically not an option.
|
12
|
+
#
|
13
|
+
# Butler::Static extends ActionDispatch::Static's func-
|
14
|
+
# tionality to allow a the developer to set custom rules
|
15
|
+
# for the HTTP headers that the files sent should carry.
|
16
|
+
#
|
17
|
+
# The author's intent was to allow Rails to serve static
|
18
|
+
# assets with custom HTTP Headers.
|
19
|
+
# This allows for example serving webfonts or icon fonts
|
20
|
+
# carrying the appropriate HTTP headers via a Content
|
21
|
+
# Delivery Network (CDN) such as Amazon's Cloudfront.
|
22
|
+
# Files requested by the CDN will need to carry the headers
|
23
|
+
# the file should be sent with to the visitor's browser
|
24
|
+
# already when being sent from the web server.
|
25
|
+
#
|
26
|
+
# ActionDispatch::Static, Rack::Static et al. don't allow
|
27
|
+
# (yet) to add custom HTTP headers to files because the
|
28
|
+
# underlying Rack::File implementation only allows for
|
29
|
+
# setting a static 'Cache-Control' header.
|
30
|
+
#
|
31
|
+
# Code adapted from Rails' ActionDispatch::Static.
|
32
|
+
#
|
33
|
+
# If no matching file can be found in the precompiled assets
|
34
|
+
# the request will bubble up to the Rails stack to decide
|
35
|
+
# how to handle it
|
36
|
+
#
|
37
|
+
# Usage:
|
38
|
+
# config.middleware.delete ActionDispatch::Static
|
39
|
+
# config.middleware.insert_before Rack::Cache, Butler::Static
|
40
|
+
#
|
41
|
+
# # Provide the header rules like so
|
42
|
+
# config.assets.header_rules = {
|
43
|
+
# rule => { header_field => content },
|
44
|
+
# another_rule => { header_field => content }
|
45
|
+
# }
|
46
|
+
#
|
47
|
+
# # These rules are shipped along
|
48
|
+
# # 1) Global
|
49
|
+
# :global => Matches every file
|
50
|
+
#
|
51
|
+
# # 2) Folders
|
52
|
+
# '/folder' => Matches all files in a certain folder
|
53
|
+
# '/folder/subfolder' => ...
|
54
|
+
# Note: Provide the folder as a string,
|
55
|
+
# with or without the starting slash
|
56
|
+
#
|
57
|
+
# # 3) File Extensions
|
58
|
+
# ['css', 'js'] => Will match all files ending in .css or .js
|
59
|
+
# %w(css js) => ...
|
60
|
+
# Note: Provide the file extensions in an array,
|
61
|
+
# use any ruby syntax you like to set that array up
|
62
|
+
#
|
63
|
+
# # 4) Regular Expressions / Regexp
|
64
|
+
# %r{\.(?:css|js)\z} => will match all files ending in .css or .js
|
65
|
+
# /\.(?:eot|ttf|otf|woff|svg)\z/ => will match all files ending
|
66
|
+
# in the most common web font formats
|
67
|
+
#
|
68
|
+
# # 5) Shortcuts
|
69
|
+
# There is currently only one shortcut defined.
|
70
|
+
# :fonts => will match all files ending in eot, ttf, otf, woff, svg
|
71
|
+
# using the Regexp stated above
|
72
|
+
#
|
73
|
+
# Note: The rules will be applied in the order the are listed,
|
74
|
+
# thus more special rules further down below can override
|
75
|
+
# general global HTTP header settings
|
76
|
+
#
|
77
|
+
class Static
|
78
|
+
def initialize(app, path, options={})
|
79
|
+
@app = app
|
80
|
+
@header_rules = options[:header_rules] || {}
|
81
|
+
@file_handler = Butler::Handler.new(path, header_rules: @header_rules)
|
82
|
+
end
|
83
|
+
|
84
|
+
def call(env)
|
85
|
+
case env['REQUEST_METHOD']
|
86
|
+
when 'GET', 'HEAD'
|
87
|
+
path = env['PATH_INFO'].chomp('/')
|
88
|
+
if match = @file_handler.match?(path)
|
89
|
+
env['PATH_INFO'] = match
|
90
|
+
return @file_handler.call(env)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
@app.call(env)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
data/lib/butler.rb
ADDED
@@ -0,0 +1,226 @@
|
|
1
|
+
require 'minitest_helper'
|
2
|
+
require 'butler/asset'
|
3
|
+
require 'rack/mock'
|
4
|
+
|
5
|
+
describe Butler::Asset do
|
6
|
+
DOCROOT = File.expand_path(File.dirname(__FILE__))
|
7
|
+
|
8
|
+
def file(*args)
|
9
|
+
Butler::Asset.new(*args)
|
10
|
+
end
|
11
|
+
|
12
|
+
before do
|
13
|
+
@file = Rack::MockRequest.new(file(DOCROOT))
|
14
|
+
end
|
15
|
+
|
16
|
+
describe 'when serving files' do
|
17
|
+
#
|
18
|
+
# Serve Files
|
19
|
+
#
|
20
|
+
it 'should serve available files' do
|
21
|
+
res = @file.get('files/index.html')
|
22
|
+
|
23
|
+
res.status.must_equal 200
|
24
|
+
res.body.must_equal '<html>Index</html>'
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'should return status 404 if file cannot be found' do
|
28
|
+
res = @file.get('files/unknown.html')
|
29
|
+
|
30
|
+
res.status.must_equal 404
|
31
|
+
end
|
32
|
+
|
33
|
+
#
|
34
|
+
# Request Methods
|
35
|
+
#
|
36
|
+
it 'should only support GET and HEAD requests' do
|
37
|
+
file = 'files/index.html'
|
38
|
+
|
39
|
+
allowed = %w(get head)
|
40
|
+
allowed.each do |method|
|
41
|
+
res = @file.send(method, file)
|
42
|
+
res.status.must_equal 200
|
43
|
+
end
|
44
|
+
|
45
|
+
forbidden = %w(post put delete) # patch is breaking
|
46
|
+
forbidden.each do |method|
|
47
|
+
res = @file.send(method, file)
|
48
|
+
res.status.must_equal 405
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
#
|
53
|
+
# Headers
|
54
|
+
#
|
55
|
+
it 'should set header Last-Modified' do
|
56
|
+
file = 'files/index.html'
|
57
|
+
path = File.join(DOCROOT, file)
|
58
|
+
res = @file.get(file)
|
59
|
+
|
60
|
+
res.status.must_equal 200
|
61
|
+
res['Last-Modified'].must_equal File.mtime(path).httpdate
|
62
|
+
end
|
63
|
+
it "should set Content-Length correctly for HEAD requests" do
|
64
|
+
res = @file.head('files/index.html')
|
65
|
+
|
66
|
+
res.status.must_equal 200
|
67
|
+
res['Content-Length'].must_equal '18'
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'should set provided headers' do
|
71
|
+
file = Butler::Asset.new(DOCROOT,
|
72
|
+
headers: {'Cache-Control' => 'public, max-age=42'})
|
73
|
+
req = Rack::MockRequest.new(file)
|
74
|
+
|
75
|
+
res = req.get('files/index.html')
|
76
|
+
|
77
|
+
res.status.must_equal 200
|
78
|
+
res['Cache-Control'].must_equal 'public, max-age=42'
|
79
|
+
end
|
80
|
+
|
81
|
+
#
|
82
|
+
# Modified / unmodified Files (304 / 200)
|
83
|
+
#
|
84
|
+
it 'should return status 304 if file is not modified since last serve' do
|
85
|
+
file = 'files/index.html'
|
86
|
+
path = File.join(DOCROOT, file)
|
87
|
+
time = File.mtime(path).httpdate
|
88
|
+
res = @file.get(file, 'HTTP_IF_MODIFIED_SINCE' => time)
|
89
|
+
|
90
|
+
res.status.must_equal 304
|
91
|
+
res.body.must_be_empty
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'should serve the file if it is modified since last serve' do
|
95
|
+
file = 'files/index.html'
|
96
|
+
path = File.join(DOCROOT, file)
|
97
|
+
time = (File.mtime(path) - 60).httpdate
|
98
|
+
res = @file.get(file, 'HTTP_IF_MODIFIED_SINCE' => time)
|
99
|
+
|
100
|
+
res.status.must_equal 200
|
101
|
+
res.body.must_equal '<html>Index</html>'
|
102
|
+
end
|
103
|
+
|
104
|
+
#
|
105
|
+
# URL-encoded filenames
|
106
|
+
#
|
107
|
+
it 'should serve files with URL encoded filenames' do
|
108
|
+
res = @file.get('files/%69%6E%64%65%78%2Ehtml')
|
109
|
+
|
110
|
+
res.status.must_equal 200
|
111
|
+
res.body.must_equal '<html>Index</html>'
|
112
|
+
end
|
113
|
+
|
114
|
+
it 'should allow safe directory traversal' do
|
115
|
+
res = @file.get('files/../asset_spec.rb')
|
116
|
+
res.status.must_equal 200
|
117
|
+
|
118
|
+
res = @file.get('files/../files/index.html')
|
119
|
+
res.status.must_equal 200
|
120
|
+
|
121
|
+
res = @file.get('.')
|
122
|
+
res.status.must_equal 404
|
123
|
+
|
124
|
+
res = @file.get('files/..')
|
125
|
+
res.status.must_equal 404
|
126
|
+
end
|
127
|
+
|
128
|
+
#
|
129
|
+
# Directory Traversal
|
130
|
+
#
|
131
|
+
it 'should not allow unsafe directory traversal' do
|
132
|
+
res = @file.get('../minitest_helper.rb')
|
133
|
+
res.status.must_equal 404
|
134
|
+
|
135
|
+
res = @file.get('/../minitest_helper.rb')
|
136
|
+
res.status.must_equal 404
|
137
|
+
|
138
|
+
res = @file.get('../butler/files/index.html')
|
139
|
+
res.status.must_equal 404
|
140
|
+
|
141
|
+
# Ensure file existance
|
142
|
+
res = @file.get('files/index.html')
|
143
|
+
res.status.must_equal 200
|
144
|
+
end
|
145
|
+
|
146
|
+
it 'should allow safe directory traversal with encoded periods' do
|
147
|
+
res = @file.get('files/%2E%2E/asset_spec.rb')
|
148
|
+
res.status.must_equal 200
|
149
|
+
|
150
|
+
res = @file.get('files/%2E%2E/files/index.html')
|
151
|
+
res.status.must_equal 200
|
152
|
+
|
153
|
+
res = @file.get('%2E')
|
154
|
+
res.status.must_equal 404
|
155
|
+
|
156
|
+
res = @file.get('files/%2E')
|
157
|
+
res.status.must_equal 404
|
158
|
+
end
|
159
|
+
|
160
|
+
it 'should not allow unsafe directory traversal with encoded periods' do
|
161
|
+
res = @file.get('%2E%2E/minitest_helper.rb')
|
162
|
+
res.status.must_equal 404
|
163
|
+
|
164
|
+
res = @file.get('/%2E%2E/minitest_helper.rb')
|
165
|
+
res.status.must_equal 404
|
166
|
+
end
|
167
|
+
|
168
|
+
it 'should allow files with .. in their name' do
|
169
|
+
res = @file.get('files/..test')
|
170
|
+
res.status.must_equal 404
|
171
|
+
|
172
|
+
res = @file.get('files/test..')
|
173
|
+
res.status.must_equal 404
|
174
|
+
|
175
|
+
res = @file.get('files../test..')
|
176
|
+
res.status.must_equal 404
|
177
|
+
end
|
178
|
+
|
179
|
+
it 'should detect SystemCallErrors' do
|
180
|
+
res = @file.get('/cgi')
|
181
|
+
res.status.must_equal 404
|
182
|
+
|
183
|
+
res = @file.get('cgi')
|
184
|
+
res.status.must_equal 404
|
185
|
+
|
186
|
+
res = @file.get('/files')
|
187
|
+
res.status.must_equal 404
|
188
|
+
|
189
|
+
res = @file.get('files')
|
190
|
+
res.status.must_equal 404
|
191
|
+
end
|
192
|
+
|
193
|
+
it 'should return bodies that respond to #to_path' do
|
194
|
+
file = 'files/index.html'
|
195
|
+
path = File.join(DOCROOT, file)
|
196
|
+
env = Rack::MockRequest.env_for(file)
|
197
|
+
status, _, body = Butler::Asset.new(DOCROOT).call(env)
|
198
|
+
|
199
|
+
status.must_equal 200
|
200
|
+
body.must_respond_to :to_path
|
201
|
+
body.to_path.must_equal path
|
202
|
+
end
|
203
|
+
|
204
|
+
#
|
205
|
+
# Byte Ranges
|
206
|
+
#
|
207
|
+
it 'should return correct byte range in body' do
|
208
|
+
res = @file.get('files/index.html', 'HTTP_RANGE' => 'bytes=2-12')
|
209
|
+
|
210
|
+
res.status.must_equal 206
|
211
|
+
res.body.must_equal 'tml>Index</'
|
212
|
+
|
213
|
+
res['Content-Length'].must_equal '11'
|
214
|
+
# before: res['Content-Range'].must_equal 'bytes=2-12'
|
215
|
+
res['Content-Range'].must_equal 'bytes 2-12/18'
|
216
|
+
end
|
217
|
+
|
218
|
+
it 'should return error for unsatisfiable byte range' do
|
219
|
+
res = @file.get('files/index.html', 'HTTP_RANGE' => 'bytes=1234-5678')
|
220
|
+
|
221
|
+
res.status.must_equal 416
|
222
|
+
res['Content-Range'].must_equal 'bytes */18'
|
223
|
+
end
|
224
|
+
|
225
|
+
end
|
226
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
<html>Index</html>
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'turn'
|
3
|
+
|
4
|
+
Turn.config do |c|
|
5
|
+
# use one of output formats:
|
6
|
+
# :outline - turn's original case/test outline mode [default]
|
7
|
+
# :progress - indicates progress with progress bar
|
8
|
+
# :dotted - test/unit's traditional dot-progress mode
|
9
|
+
# :pretty - new pretty reporter
|
10
|
+
# :marshal - dump output as YAML (normal run mode only)
|
11
|
+
# :cue - interactive testing
|
12
|
+
c.format = :pretty
|
13
|
+
# turn on invoke/execute tracing, enable full backtrace
|
14
|
+
c.trace = false
|
15
|
+
# use humanized test names (works only with :outline format)
|
16
|
+
c.natural = true
|
17
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
absolute/*
|
@@ -0,0 +1 @@
|
|
1
|
+
404 error fixture
|
@@ -0,0 +1 @@
|
|
1
|
+
500 localized error fixture
|
@@ -0,0 +1 @@
|
|
1
|
+
500 error fixture
|
@@ -0,0 +1 @@
|
|
1
|
+
// cools.js
|
@@ -0,0 +1 @@
|
|
1
|
+
/*file.css*/
|
@@ -0,0 +1 @@
|
|
1
|
+
/foo/bar.html
|
@@ -0,0 +1 @@
|
|
1
|
+
/foo/index.html
|
@@ -0,0 +1 @@
|
|
1
|
+
means hello in Japanese
|
Binary file
|
@@ -0,0 +1 @@
|
|
1
|
+
/index.html
|
@@ -0,0 +1 @@
|
|
1
|
+
// application js
|
@@ -0,0 +1 @@
|
|
1
|
+
// bank js
|
@@ -0,0 +1 @@
|
|
1
|
+
// common.javascript
|
@@ -0,0 +1 @@
|
|
1
|
+
// controls js
|
@@ -0,0 +1 @@
|
|
1
|
+
// dragdrop js
|
@@ -0,0 +1 @@
|
|
1
|
+
// effects js
|
@@ -0,0 +1 @@
|
|
1
|
+
// prototype js
|
@@ -0,0 +1 @@
|
|
1
|
+
// robber js
|
@@ -0,0 +1 @@
|
|
1
|
+
// subdir js
|
@@ -0,0 +1 @@
|
|
1
|
+
// version.1.0 js
|
@@ -0,0 +1 @@
|
|
1
|
+
/* bank.css */
|
@@ -0,0 +1 @@
|
|
1
|
+
/* random.styles */
|
@@ -0,0 +1 @@
|
|
1
|
+
/* robber.css */
|
@@ -0,0 +1 @@
|
|
1
|
+
/* subdir.css */
|
@@ -0,0 +1 @@
|
|
1
|
+
/* version.1.0.css */
|
@@ -0,0 +1 @@
|
|
1
|
+
/* Webfont */
|
@@ -0,0 +1,195 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'minitest/autorun'
|
3
|
+
require 'butler/static'
|
4
|
+
require 'rack/mock'
|
5
|
+
require 'action_controller'
|
6
|
+
|
7
|
+
FIXTURE_LOAD_PATH = File.join(File.dirname(__FILE__), 'fixtures')
|
8
|
+
|
9
|
+
module StaticTests
|
10
|
+
def test_serves_dynamic_content
|
11
|
+
assert_equal "Hello, World!", get("/nofile").body
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_handles_urls_with_bad_encoding
|
15
|
+
assert_equal "Hello, World!", get("/doorkeeper%E3E4").body
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_serves_static_index_at_root
|
19
|
+
assert_html "/index.html", get("/index.html")
|
20
|
+
assert_html "/index.html", get("/index")
|
21
|
+
assert_html "/index.html", get("/")
|
22
|
+
assert_html "/index.html", get("")
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_serves_static_file_in_directory
|
26
|
+
assert_html "/foo/bar.html", get("/foo/bar.html")
|
27
|
+
assert_html "/foo/bar.html", get("/foo/bar/")
|
28
|
+
assert_html "/foo/bar.html", get("/foo/bar")
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_serves_static_index_file_in_directory
|
32
|
+
assert_html "/foo/index.html", get("/foo/index.html")
|
33
|
+
assert_html "/foo/index.html", get("/foo/")
|
34
|
+
assert_html "/foo/index.html", get("/foo")
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_served_static_file_with_non_english_filename
|
38
|
+
assert_html "means hello in Japanese\n", get("/foo/#{Rack::Utils.escape("こんにちは.html")}")
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
def test_serves_static_file_with_exclamation_mark_in_filename
|
43
|
+
with_static_file "/foo/foo!bar.html" do |file|
|
44
|
+
assert_html file, get("/foo/foo%21bar.html")
|
45
|
+
assert_html file, get("/foo/foo!bar.html")
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def test_serves_static_file_with_dollar_sign_in_filename
|
50
|
+
with_static_file "/foo/foo$bar.html" do |file|
|
51
|
+
assert_html file, get("/foo/foo%24bar.html")
|
52
|
+
assert_html file, get("/foo/foo$bar.html")
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def test_serves_static_file_with_ampersand_in_filename
|
57
|
+
with_static_file "/foo/foo&bar.html" do |file|
|
58
|
+
assert_html file, get("/foo/foo%26bar.html")
|
59
|
+
assert_html file, get("/foo/foo&bar.html")
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def test_serves_static_file_with_apostrophe_in_filename
|
64
|
+
with_static_file "/foo/foo'bar.html" do |file|
|
65
|
+
assert_html file, get("/foo/foo%27bar.html")
|
66
|
+
assert_html file, get("/foo/foo'bar.html")
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def test_serves_static_file_with_parentheses_in_filename
|
71
|
+
with_static_file "/foo/foo(bar).html" do |file|
|
72
|
+
assert_html file, get("/foo/foo%28bar%29.html")
|
73
|
+
assert_html file, get("/foo/foo(bar).html")
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def test_serves_static_file_with_plus_sign_in_filename
|
78
|
+
with_static_file "/foo/foo+bar.html" do |file|
|
79
|
+
assert_html file, get("/foo/foo%2Bbar.html")
|
80
|
+
assert_html file, get("/foo/foo+bar.html")
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def test_serves_static_file_with_comma_in_filename
|
85
|
+
with_static_file "/foo/foo,bar.html" do |file|
|
86
|
+
assert_html file, get("/foo/foo%2Cbar.html")
|
87
|
+
assert_html file, get("/foo/foo,bar.html")
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def test_serves_static_file_with_semi_colon_in_filename
|
92
|
+
with_static_file "/foo/foo;bar.html" do |file|
|
93
|
+
assert_html file, get("/foo/foo%3Bbar.html")
|
94
|
+
assert_html file, get("/foo/foo;bar.html")
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def test_serves_static_file_with_at_symbol_in_filename
|
99
|
+
with_static_file "/foo/foo@bar.html" do |file|
|
100
|
+
assert_html file, get("/foo/foo%40bar.html")
|
101
|
+
assert_html file, get("/foo/foo@bar.html")
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# Windows doesn't allow \ / : * ? " < > | in filenames
|
106
|
+
unless RbConfig::CONFIG['host_os'] =~ /mswin|mingw/
|
107
|
+
def test_serves_static_file_with_colon
|
108
|
+
with_static_file "/foo/foo:bar.html" do |file|
|
109
|
+
assert_html file, get("/foo/foo%3Abar.html")
|
110
|
+
assert_html file, get("/foo/foo:bar.html")
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def test_serves_static_file_with_asterisk
|
115
|
+
with_static_file "/foo/foo*bar.html" do |file|
|
116
|
+
assert_html file, get("/foo/foo%2Abar.html")
|
117
|
+
assert_html file, get("/foo/foo*bar.html")
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
#
|
123
|
+
# Headers
|
124
|
+
#
|
125
|
+
def test_sets_headers_global_via_global_shortcut
|
126
|
+
response = get("/index.html")
|
127
|
+
assert_html "/index.html", response
|
128
|
+
assert_equal "public, max-age=100", response.headers["Cache-Control"]
|
129
|
+
end
|
130
|
+
|
131
|
+
def test_sets_headers_fonts_via_fonts_shortcut
|
132
|
+
response = get("/stylesheets/webfont.eot")
|
133
|
+
assert_equal "public, max-age=200", response.headers["Cache-Control"]
|
134
|
+
end
|
135
|
+
|
136
|
+
def test_set_headers_for_file_extensions_via_array
|
137
|
+
response = get("/images/rails.png")
|
138
|
+
assert_equal "public, max-age=300", response.headers["Cache-Control"]
|
139
|
+
end
|
140
|
+
|
141
|
+
def test_set_headers_for_folder_via_string
|
142
|
+
response = get("/javascripts/subdir/subdir.js")
|
143
|
+
assert_equal "public, max-age=400", response.headers["Cache-Control"]
|
144
|
+
|
145
|
+
response = get("/foo/bar.html")
|
146
|
+
assert_equal "public, max-age=500", response.headers["Cache-Control"]
|
147
|
+
end
|
148
|
+
|
149
|
+
def test_set_headers_flexibely_via_regexp
|
150
|
+
response = get("/elsewhere/file.css")
|
151
|
+
assert_equal "public, max-age=600", response.headers["Cache-Control"]
|
152
|
+
end
|
153
|
+
|
154
|
+
private
|
155
|
+
|
156
|
+
def assert_html(body, response)
|
157
|
+
assert_equal body, response.body
|
158
|
+
assert_equal "text/html", response.headers["Content-Type"]
|
159
|
+
end
|
160
|
+
|
161
|
+
def get(path)
|
162
|
+
Rack::MockRequest.new(@app).request("GET", path)
|
163
|
+
end
|
164
|
+
|
165
|
+
def with_static_file(file)
|
166
|
+
path = "#{FIXTURE_LOAD_PATH}/public" + file
|
167
|
+
File.open(path, "wb+") { |f| f.write(file) }
|
168
|
+
yield file
|
169
|
+
ensure
|
170
|
+
File.delete(path)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
class StaticTest < MiniTest::Unit::TestCase
|
175
|
+
DummyApp = lambda { |env|
|
176
|
+
[200, {"Content-Type" => "text/plain"}, ["Hello, World!"]]
|
177
|
+
}
|
178
|
+
|
179
|
+
header_rules = {
|
180
|
+
:global => {'Cache-Control' => 'public, max-age=100'},
|
181
|
+
:fonts => {'Cache-Control' => 'public, max-age=200'},
|
182
|
+
%w(png) => {'Cache-Control' => 'public, max-age=300'},
|
183
|
+
'javascripts/subdir' => {'Cache-Control' => 'public, max-age=400'},
|
184
|
+
'/foo' => {'Cache-Control' => 'public, max-age=500'},
|
185
|
+
/\.(css|erb)\z/ => {'Cache-Control' => 'public, max-age=600'},
|
186
|
+
}
|
187
|
+
|
188
|
+
App = Butler::Static.new(DummyApp, "#{FIXTURE_LOAD_PATH}/public", header_rules: header_rules)
|
189
|
+
|
190
|
+
def setup
|
191
|
+
@app = App
|
192
|
+
end
|
193
|
+
|
194
|
+
include StaticTests
|
195
|
+
end
|
metadata
ADDED
@@ -0,0 +1,221 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: butler_static
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.3
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Thomas Klemm
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-08-19 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rack
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: minitest
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: guard-minitest
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: turn
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: activesupport
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ~>
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: 3.2.8
|
86
|
+
type: :development
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ~>
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: 3.2.8
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: actionpack
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ~>
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: 3.2.8
|
102
|
+
type: :development
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ~>
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: 3.2.8
|
110
|
+
description: Butler is a Rack Middleware that serves static assets for your Rails
|
111
|
+
app.
|
112
|
+
email:
|
113
|
+
- github@tklemm.eu
|
114
|
+
executables: []
|
115
|
+
extensions: []
|
116
|
+
extra_rdoc_files: []
|
117
|
+
files:
|
118
|
+
- .gitignore
|
119
|
+
- Gemfile
|
120
|
+
- Guardfile
|
121
|
+
- LICENSE.txt
|
122
|
+
- README.md
|
123
|
+
- Rakefile
|
124
|
+
- butler.gemspec
|
125
|
+
- lib/butler.rb
|
126
|
+
- lib/butler/asset.rb
|
127
|
+
- lib/butler/handler.rb
|
128
|
+
- lib/butler/railtie.rb
|
129
|
+
- lib/butler/static.rb
|
130
|
+
- lib/butler/version.rb
|
131
|
+
- spec/butler/asset_spec.rb
|
132
|
+
- spec/butler/files/index.html
|
133
|
+
- spec/minitest_helper.rb
|
134
|
+
- test/butler/fixtures/public/.gitignore
|
135
|
+
- test/butler/fixtures/public/404.html
|
136
|
+
- test/butler/fixtures/public/500.da.html
|
137
|
+
- test/butler/fixtures/public/500.html
|
138
|
+
- test/butler/fixtures/public/elsewhere/cools.js
|
139
|
+
- test/butler/fixtures/public/elsewhere/file.css
|
140
|
+
- test/butler/fixtures/public/foo/bar.html
|
141
|
+
- test/butler/fixtures/public/foo/baz.css
|
142
|
+
- test/butler/fixtures/public/foo/index.html
|
143
|
+
- test/butler/fixtures/public/foo/こんにちは.html
|
144
|
+
- test/butler/fixtures/public/images/rails.png
|
145
|
+
- test/butler/fixtures/public/index.html
|
146
|
+
- test/butler/fixtures/public/javascripts/application.js
|
147
|
+
- test/butler/fixtures/public/javascripts/bank.js
|
148
|
+
- test/butler/fixtures/public/javascripts/common.javascript
|
149
|
+
- test/butler/fixtures/public/javascripts/controls.js
|
150
|
+
- test/butler/fixtures/public/javascripts/dragdrop.js
|
151
|
+
- test/butler/fixtures/public/javascripts/effects.js
|
152
|
+
- test/butler/fixtures/public/javascripts/prototype.js
|
153
|
+
- test/butler/fixtures/public/javascripts/robber.js
|
154
|
+
- test/butler/fixtures/public/javascripts/subdir/subdir.js
|
155
|
+
- test/butler/fixtures/public/javascripts/version.1.0.js
|
156
|
+
- test/butler/fixtures/public/stylesheets/bank.css
|
157
|
+
- test/butler/fixtures/public/stylesheets/random.styles
|
158
|
+
- test/butler/fixtures/public/stylesheets/robber.css
|
159
|
+
- test/butler/fixtures/public/stylesheets/subdir/subdir.css
|
160
|
+
- test/butler/fixtures/public/stylesheets/version.1.0.css
|
161
|
+
- test/butler/fixtures/public/stylesheets/webfont.eot
|
162
|
+
- test/butler/static_test.rb
|
163
|
+
homepage: https://github.com/thomasklemm/butler
|
164
|
+
licenses: []
|
165
|
+
post_install_message:
|
166
|
+
rdoc_options: []
|
167
|
+
require_paths:
|
168
|
+
- lib
|
169
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
170
|
+
none: false
|
171
|
+
requirements:
|
172
|
+
- - ! '>='
|
173
|
+
- !ruby/object:Gem::Version
|
174
|
+
version: '0'
|
175
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
176
|
+
none: false
|
177
|
+
requirements:
|
178
|
+
- - ! '>='
|
179
|
+
- !ruby/object:Gem::Version
|
180
|
+
version: '0'
|
181
|
+
requirements: []
|
182
|
+
rubyforge_project:
|
183
|
+
rubygems_version: 1.8.23
|
184
|
+
signing_key:
|
185
|
+
specification_version: 3
|
186
|
+
summary: Butler is a Rack Middleware that serves static assets for your Rails app.
|
187
|
+
It allows you to set HTTP headers for individual files or folders based on rules.
|
188
|
+
test_files:
|
189
|
+
- spec/butler/asset_spec.rb
|
190
|
+
- spec/butler/files/index.html
|
191
|
+
- spec/minitest_helper.rb
|
192
|
+
- test/butler/fixtures/public/.gitignore
|
193
|
+
- test/butler/fixtures/public/404.html
|
194
|
+
- test/butler/fixtures/public/500.da.html
|
195
|
+
- test/butler/fixtures/public/500.html
|
196
|
+
- test/butler/fixtures/public/elsewhere/cools.js
|
197
|
+
- test/butler/fixtures/public/elsewhere/file.css
|
198
|
+
- test/butler/fixtures/public/foo/bar.html
|
199
|
+
- test/butler/fixtures/public/foo/baz.css
|
200
|
+
- test/butler/fixtures/public/foo/index.html
|
201
|
+
- test/butler/fixtures/public/foo/こんにちは.html
|
202
|
+
- test/butler/fixtures/public/images/rails.png
|
203
|
+
- test/butler/fixtures/public/index.html
|
204
|
+
- test/butler/fixtures/public/javascripts/application.js
|
205
|
+
- test/butler/fixtures/public/javascripts/bank.js
|
206
|
+
- test/butler/fixtures/public/javascripts/common.javascript
|
207
|
+
- test/butler/fixtures/public/javascripts/controls.js
|
208
|
+
- test/butler/fixtures/public/javascripts/dragdrop.js
|
209
|
+
- test/butler/fixtures/public/javascripts/effects.js
|
210
|
+
- test/butler/fixtures/public/javascripts/prototype.js
|
211
|
+
- test/butler/fixtures/public/javascripts/robber.js
|
212
|
+
- test/butler/fixtures/public/javascripts/subdir/subdir.js
|
213
|
+
- test/butler/fixtures/public/javascripts/version.1.0.js
|
214
|
+
- test/butler/fixtures/public/stylesheets/bank.css
|
215
|
+
- test/butler/fixtures/public/stylesheets/random.styles
|
216
|
+
- test/butler/fixtures/public/stylesheets/robber.css
|
217
|
+
- test/butler/fixtures/public/stylesheets/subdir/subdir.css
|
218
|
+
- test/butler/fixtures/public/stylesheets/version.1.0.css
|
219
|
+
- test/butler/fixtures/public/stylesheets/webfont.eot
|
220
|
+
- test/butler/static_test.rb
|
221
|
+
has_rdoc:
|