butler_static 0.0.3
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/.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:
|