devver-rack-contrib 0.9.3
Sign up to get free protection for your applications and to get access to all the features.
- data/COPYING +18 -0
- data/README.rdoc +80 -0
- data/Rakefile +90 -0
- data/lib/rack/contrib.rb +40 -0
- data/lib/rack/contrib/accept_format.rb +46 -0
- data/lib/rack/contrib/access.rb +85 -0
- data/lib/rack/contrib/backstage.rb +20 -0
- data/lib/rack/contrib/bounce_favicon.rb +16 -0
- data/lib/rack/contrib/callbacks.rb +37 -0
- data/lib/rack/contrib/config.rb +16 -0
- data/lib/rack/contrib/cookies.rb +50 -0
- data/lib/rack/contrib/csshttprequest.rb +39 -0
- data/lib/rack/contrib/deflect.rb +137 -0
- data/lib/rack/contrib/evil.rb +12 -0
- data/lib/rack/contrib/garbagecollector.rb +14 -0
- data/lib/rack/contrib/jsonp.rb +41 -0
- data/lib/rack/contrib/lighttpd_script_name_fix.rb +16 -0
- data/lib/rack/contrib/locale.rb +31 -0
- data/lib/rack/contrib/mailexceptions.rb +120 -0
- data/lib/rack/contrib/nested_params.rb +143 -0
- data/lib/rack/contrib/not_found.rb +18 -0
- data/lib/rack/contrib/post_body_content_type_parser.rb +40 -0
- data/lib/rack/contrib/proctitle.rb +30 -0
- data/lib/rack/contrib/profiler.rb +108 -0
- data/lib/rack/contrib/relative_redirect.rb +44 -0
- data/lib/rack/contrib/response_cache.rb +59 -0
- data/lib/rack/contrib/route_exceptions.rb +49 -0
- data/lib/rack/contrib/sendfile.rb +142 -0
- data/lib/rack/contrib/signals.rb +63 -0
- data/lib/rack/contrib/time_zone.rb +25 -0
- data/rack-contrib.gemspec +88 -0
- data/test/404.html +1 -0
- data/test/Maintenance.html +1 -0
- data/test/mail_settings.rb +12 -0
- data/test/spec_rack_accept_format.rb +72 -0
- data/test/spec_rack_access.rb +154 -0
- data/test/spec_rack_backstage.rb +26 -0
- data/test/spec_rack_callbacks.rb +65 -0
- data/test/spec_rack_config.rb +22 -0
- data/test/spec_rack_contrib.rb +8 -0
- data/test/spec_rack_csshttprequest.rb +66 -0
- data/test/spec_rack_deflect.rb +107 -0
- data/test/spec_rack_evil.rb +19 -0
- data/test/spec_rack_garbagecollector.rb +13 -0
- data/test/spec_rack_jsonp.rb +34 -0
- data/test/spec_rack_lighttpd_script_name_fix.rb +16 -0
- data/test/spec_rack_mailexceptions.rb +97 -0
- data/test/spec_rack_nested_params.rb +46 -0
- data/test/spec_rack_not_found.rb +17 -0
- data/test/spec_rack_post_body_content_type_parser.rb +32 -0
- data/test/spec_rack_proctitle.rb +26 -0
- data/test/spec_rack_profiler.rb +41 -0
- data/test/spec_rack_relative_redirect.rb +78 -0
- data/test/spec_rack_response_cache.rb +137 -0
- data/test/spec_rack_sendfile.rb +86 -0
- metadata +174 -0
@@ -0,0 +1,143 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
require 'strscan'
|
3
|
+
|
4
|
+
module Rack
|
5
|
+
# Rack middleware for parsing POST/PUT body data into nested parameters
|
6
|
+
class NestedParams
|
7
|
+
|
8
|
+
CONTENT_TYPE = 'CONTENT_TYPE'.freeze
|
9
|
+
POST_BODY = 'rack.input'.freeze
|
10
|
+
FORM_INPUT = 'rack.request.form_input'.freeze
|
11
|
+
FORM_HASH = 'rack.request.form_hash'.freeze
|
12
|
+
FORM_VARS = 'rack.request.form_vars'.freeze
|
13
|
+
|
14
|
+
# supported content type
|
15
|
+
URL_ENCODED = 'application/x-www-form-urlencoded'.freeze
|
16
|
+
|
17
|
+
def initialize(app)
|
18
|
+
@app = app
|
19
|
+
end
|
20
|
+
|
21
|
+
def call(env)
|
22
|
+
if form_vars = env[FORM_VARS]
|
23
|
+
env[FORM_HASH] = parse_query_parameters(form_vars)
|
24
|
+
elsif env[CONTENT_TYPE] == URL_ENCODED
|
25
|
+
post_body = env[POST_BODY]
|
26
|
+
env[FORM_INPUT] = post_body
|
27
|
+
env[FORM_HASH] = parse_query_parameters(post_body.read)
|
28
|
+
post_body.rewind if post_body.respond_to?(:rewind)
|
29
|
+
end
|
30
|
+
@app.call(env)
|
31
|
+
end
|
32
|
+
|
33
|
+
## the rest is nabbed from Rails ##
|
34
|
+
|
35
|
+
def parse_query_parameters(query_string)
|
36
|
+
return {} if query_string.nil? or query_string.empty?
|
37
|
+
|
38
|
+
pairs = query_string.split('&').collect do |chunk|
|
39
|
+
next if chunk.empty?
|
40
|
+
key, value = chunk.split('=', 2)
|
41
|
+
next if key.empty?
|
42
|
+
value = value.nil? ? nil : CGI.unescape(value)
|
43
|
+
[ CGI.unescape(key), value ]
|
44
|
+
end.compact
|
45
|
+
|
46
|
+
UrlEncodedPairParser.new(pairs).result
|
47
|
+
end
|
48
|
+
|
49
|
+
class UrlEncodedPairParser < StringScanner
|
50
|
+
attr_reader :top, :parent, :result
|
51
|
+
|
52
|
+
def initialize(pairs = [])
|
53
|
+
super('')
|
54
|
+
@result = {}
|
55
|
+
pairs.each { |key, value| parse(key, value) }
|
56
|
+
end
|
57
|
+
|
58
|
+
KEY_REGEXP = %r{([^\[\]=&]+)}
|
59
|
+
BRACKETED_KEY_REGEXP = %r{\[([^\[\]=&]+)\]}
|
60
|
+
|
61
|
+
# Parse the query string
|
62
|
+
def parse(key, value)
|
63
|
+
self.string = key
|
64
|
+
@top, @parent = result, nil
|
65
|
+
|
66
|
+
# First scan the bare key
|
67
|
+
key = scan(KEY_REGEXP) or return
|
68
|
+
key = post_key_check(key)
|
69
|
+
|
70
|
+
# Then scan as many nestings as present
|
71
|
+
until eos?
|
72
|
+
r = scan(BRACKETED_KEY_REGEXP) or return
|
73
|
+
key = self[1]
|
74
|
+
key = post_key_check(key)
|
75
|
+
end
|
76
|
+
|
77
|
+
bind(key, value)
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
# After we see a key, we must look ahead to determine our next action. Cases:
|
82
|
+
#
|
83
|
+
# [] follows the key. Then the value must be an array.
|
84
|
+
# = follows the key. (A value comes next)
|
85
|
+
# & or the end of string follows the key. Then the key is a flag.
|
86
|
+
# otherwise, a hash follows the key.
|
87
|
+
def post_key_check(key)
|
88
|
+
if scan(/\[\]/) # a[b][] indicates that b is an array
|
89
|
+
container(key, Array)
|
90
|
+
nil
|
91
|
+
elsif check(/\[[^\]]/) # a[b] indicates that a is a hash
|
92
|
+
container(key, Hash)
|
93
|
+
nil
|
94
|
+
else # End of key? We do nothing.
|
95
|
+
key
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# Add a container to the stack.
|
100
|
+
def container(key, klass)
|
101
|
+
type_conflict! klass, top[key] if top.is_a?(Hash) && top.key?(key) && ! top[key].is_a?(klass)
|
102
|
+
value = bind(key, klass.new)
|
103
|
+
type_conflict! klass, value unless value.is_a?(klass)
|
104
|
+
push(value)
|
105
|
+
end
|
106
|
+
|
107
|
+
# Push a value onto the 'stack', which is actually only the top 2 items.
|
108
|
+
def push(value)
|
109
|
+
@parent, @top = @top, value
|
110
|
+
end
|
111
|
+
|
112
|
+
# Bind a key (which may be nil for items in an array) to the provided value.
|
113
|
+
def bind(key, value)
|
114
|
+
if top.is_a? Array
|
115
|
+
if key
|
116
|
+
if top[-1].is_a?(Hash) && ! top[-1].key?(key)
|
117
|
+
top[-1][key] = value
|
118
|
+
else
|
119
|
+
top << {key => value}
|
120
|
+
end
|
121
|
+
push top.last
|
122
|
+
return top[key]
|
123
|
+
else
|
124
|
+
top << value
|
125
|
+
return value
|
126
|
+
end
|
127
|
+
elsif top.is_a? Hash
|
128
|
+
key = CGI.unescape(key)
|
129
|
+
parent << (@top = {}) if top.key?(key) && parent.is_a?(Array)
|
130
|
+
top[key] ||= value
|
131
|
+
return top[key]
|
132
|
+
else
|
133
|
+
raise ArgumentError, "Don't know what to do: top is #{top.inspect}"
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def type_conflict!(klass, value)
|
138
|
+
raise TypeError, "Conflicting types for parameter containers. Expected an instance of #{klass} but found an instance of #{value.class}. This can be caused by colliding Array and Hash parameters like qs[]=value&qs[key]=value. (The parameters received were #{value.inspect}.)"
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Rack
|
2
|
+
# Rack::NotFound is a default endpoint. Initialize with the path to
|
3
|
+
# your 404 page.
|
4
|
+
|
5
|
+
class NotFound
|
6
|
+
F = ::File
|
7
|
+
|
8
|
+
def initialize(path)
|
9
|
+
file = F.expand_path(path)
|
10
|
+
@content = F.read(file)
|
11
|
+
@length = @content.size.to_s
|
12
|
+
end
|
13
|
+
|
14
|
+
def call(env)
|
15
|
+
[404, {'Content-Type' => 'text/html', 'Content-Length' => @length}, [@content]]
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
begin
|
2
|
+
require 'json'
|
3
|
+
rescue LoadError => e
|
4
|
+
require 'json/pure'
|
5
|
+
end
|
6
|
+
|
7
|
+
module Rack
|
8
|
+
|
9
|
+
# A Rack middleware for parsing POST/PUT body data when Content-Type is
|
10
|
+
# not one of the standard supported types, like <tt>application/json</tt>.
|
11
|
+
#
|
12
|
+
# TODO: Find a better name.
|
13
|
+
#
|
14
|
+
class PostBodyContentTypeParser
|
15
|
+
|
16
|
+
# Constants
|
17
|
+
#
|
18
|
+
CONTENT_TYPE = 'CONTENT_TYPE'.freeze
|
19
|
+
POST_BODY = 'rack.input'.freeze
|
20
|
+
FORM_INPUT = 'rack.request.form_input'.freeze
|
21
|
+
FORM_HASH = 'rack.request.form_hash'.freeze
|
22
|
+
|
23
|
+
# Supported Content-Types
|
24
|
+
#
|
25
|
+
APPLICATION_JSON = 'application/json'.freeze
|
26
|
+
|
27
|
+
def initialize(app)
|
28
|
+
@app = app
|
29
|
+
end
|
30
|
+
|
31
|
+
def call(env)
|
32
|
+
case env[CONTENT_TYPE]
|
33
|
+
when APPLICATION_JSON
|
34
|
+
env.update(FORM_HASH => JSON.parse(env[POST_BODY].read), FORM_INPUT => env[POST_BODY])
|
35
|
+
end
|
36
|
+
@app.call(env)
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Rack
|
2
|
+
# Middleware to update the process title ($0) with information about the
|
3
|
+
# current request. Based loosely on:
|
4
|
+
# - http://purefiction.net/mongrel_proctitle/
|
5
|
+
# - http://github.com/grempe/thin-proctitle/tree/master
|
6
|
+
#
|
7
|
+
# NOTE: This will not work properly in a multi-threaded environment.
|
8
|
+
class ProcTitle
|
9
|
+
F = ::File
|
10
|
+
PROGNAME = F.basename($0)
|
11
|
+
|
12
|
+
def initialize(app)
|
13
|
+
@app = app
|
14
|
+
@appname = Dir.pwd.split('/').reverse.
|
15
|
+
find { |name| name !~ /^(\d+|current|releases)$/ } || PROGNAME
|
16
|
+
@requests = 0
|
17
|
+
$0 = "#{PROGNAME} [#{@appname}] init ..."
|
18
|
+
end
|
19
|
+
|
20
|
+
def call(env)
|
21
|
+
host, port = env['SERVER_NAME'], env['SERVER_PORT']
|
22
|
+
meth, path = env['REQUEST_METHOD'], env['PATH_INFO']
|
23
|
+
@requests += 1
|
24
|
+
$0 = "#{PROGNAME} [#{@appname}/#{port}] (#{@requests}) " \
|
25
|
+
"#{meth} #{path}"
|
26
|
+
|
27
|
+
@app.call(env)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
require 'ruby-prof'
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
# Set the profile=process_time query parameter to download a
|
5
|
+
# calltree profile of the request.
|
6
|
+
#
|
7
|
+
# Pass the :printer option to pick a different result format.
|
8
|
+
class Profiler
|
9
|
+
MODES = %w(
|
10
|
+
process_time
|
11
|
+
wall_time
|
12
|
+
cpu_time
|
13
|
+
allocations
|
14
|
+
memory
|
15
|
+
gc_runs
|
16
|
+
gc_time
|
17
|
+
)
|
18
|
+
|
19
|
+
DEFAULT_PRINTER = RubyProf::CallTreePrinter
|
20
|
+
DEFAULT_CONTENT_TYPE = 'application/octet-stream'
|
21
|
+
|
22
|
+
PRINTER_CONTENT_TYPE = {
|
23
|
+
RubyProf::FlatPrinter => 'text/plain',
|
24
|
+
RubyProf::GraphPrinter => 'text/plain',
|
25
|
+
RubyProf::GraphHtmlPrinter => 'text/html'
|
26
|
+
}
|
27
|
+
|
28
|
+
# Accepts a :printer => [:call_tree|:graph_html|:graph|:flat] option
|
29
|
+
# defaulting to :call_tree.
|
30
|
+
def initialize(app, options = {})
|
31
|
+
@app = app
|
32
|
+
@printer = parse_printer(options[:printer])
|
33
|
+
@times = (options[:times] || 1).to_i
|
34
|
+
end
|
35
|
+
|
36
|
+
def call(env)
|
37
|
+
if mode = profiling?(env)
|
38
|
+
profile(env, mode)
|
39
|
+
else
|
40
|
+
@app.call(env)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
def profiling?(env)
|
46
|
+
unless RubyProf.running?
|
47
|
+
request = Rack::Request.new(env)
|
48
|
+
if mode = request.params.delete('profile')
|
49
|
+
if RubyProf.const_defined?(mode.upcase)
|
50
|
+
mode
|
51
|
+
else
|
52
|
+
env['rack.errors'].write "Invalid RubyProf measure_mode: " +
|
53
|
+
"#{mode}. Use one of #{MODES.to_a.join(', ')}"
|
54
|
+
false
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def profile(env, mode)
|
61
|
+
RubyProf.measure_mode = RubyProf.const_get(mode.upcase)
|
62
|
+
|
63
|
+
GC.enable_stats if GC.respond_to?(:enable_stats)
|
64
|
+
result = RubyProf.profile do
|
65
|
+
@times.times { @app.call(env) }
|
66
|
+
end
|
67
|
+
GC.disable_stats if GC.respond_to?(:disable_stats)
|
68
|
+
|
69
|
+
[200, headers(@printer, env, mode), print(@printer, result)]
|
70
|
+
end
|
71
|
+
|
72
|
+
def print(printer, result)
|
73
|
+
body = StringIO.new
|
74
|
+
printer.new(result).print(body, :min_percent => 0.01)
|
75
|
+
body.rewind
|
76
|
+
body
|
77
|
+
end
|
78
|
+
|
79
|
+
def headers(printer, env, mode)
|
80
|
+
headers = { 'Content-Type' => PRINTER_CONTENT_TYPE[printer] || DEFAULT_CONTENT_TYPE }
|
81
|
+
if printer == RubyProf::CallTreePrinter
|
82
|
+
filename = ::File.basename(env['PATH_INFO'])
|
83
|
+
headers['Content-Disposition'] =
|
84
|
+
%(attachment; filename="#{filename}.#{mode}.tree")
|
85
|
+
end
|
86
|
+
headers
|
87
|
+
end
|
88
|
+
|
89
|
+
def parse_printer(printer)
|
90
|
+
if printer.nil?
|
91
|
+
DEFAULT_PRINTER
|
92
|
+
elsif printer.is_a?(Class)
|
93
|
+
printer
|
94
|
+
else
|
95
|
+
name = "#{camel_case(printer)}Printer"
|
96
|
+
if RubyProf.const_defined?(name)
|
97
|
+
RubyProf.const_get(name)
|
98
|
+
else
|
99
|
+
DEFAULT_PRINTER
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def camel_case(word)
|
105
|
+
word.to_s.gsub(/(?:^|_)(.)/) { $1.upcase }
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'rack'
|
2
|
+
|
3
|
+
# Rack::RelativeRedirect is a simple middleware that converts relative paths in
|
4
|
+
# redirects in absolute urls, so they conform to RFC2616. It allows the user to
|
5
|
+
# specify the absolute path to use (with a sensible default), and handles
|
6
|
+
# relative paths (those that don't start with a slash) as well.
|
7
|
+
class Rack::RelativeRedirect
|
8
|
+
SCHEME_MAP = {'http'=>'80', 'https'=>'443'}
|
9
|
+
# The default proc used if a block is not provided to .new
|
10
|
+
# Just uses the url scheme of the request and the server name.
|
11
|
+
DEFAULT_ABSOLUTE_PROC = proc do |env, res|
|
12
|
+
port = env['SERVER_PORT']
|
13
|
+
scheme = env['rack.url_scheme']
|
14
|
+
"#{scheme}://#{env['SERVER_NAME']}#{":#{port}" unless SCHEME_MAP[scheme] == port}"
|
15
|
+
end
|
16
|
+
|
17
|
+
# Initialize a new RelativeRedirect object with the given arguments. Arguments:
|
18
|
+
# * app : The next middleware in the chain. This is always called.
|
19
|
+
# * &block : If provided, it is called with the environment and the response
|
20
|
+
# from the next middleware. It should return a string representing the scheme
|
21
|
+
# and server name (such as 'http://example.org').
|
22
|
+
def initialize(app, &block)
|
23
|
+
@app = app
|
24
|
+
@absolute_proc = block || DEFAULT_ABSOLUTE_PROC
|
25
|
+
end
|
26
|
+
|
27
|
+
# Call the next middleware with the environment. If the request was a
|
28
|
+
# redirect (response status 301, 302, or 303), and the location header does
|
29
|
+
# not start with an http or https url scheme, call the block provided by new
|
30
|
+
# and use that to make the Location header an absolute url. If the Location
|
31
|
+
# does not start with a slash, make location relative to the path requested.
|
32
|
+
def call(env)
|
33
|
+
res = @app.call(env)
|
34
|
+
if [301,302,303].include?(res[0]) and loc = res[1]['Location'] and !%r{\Ahttps?://}o.match(loc)
|
35
|
+
absolute = @absolute_proc.call(env, res)
|
36
|
+
res[1]['Location'] = if %r{\A/}.match(loc)
|
37
|
+
"#{absolute}#{loc}"
|
38
|
+
else
|
39
|
+
"#{absolute}#{File.dirname(Rack::Utils.unescape(env['PATH_INFO']))}/#{loc}"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
res
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'rack'
|
3
|
+
|
4
|
+
# Rack::ResponseCache is a Rack middleware that caches responses for successful
|
5
|
+
# GET requests with no query string to disk or any ruby object that has an
|
6
|
+
# []= method (so it works with memcached). When caching to disk, it works similar to
|
7
|
+
# Rails' page caching, allowing you to cache dynamic pages to static files that can
|
8
|
+
# be served directly by a front end webserver.
|
9
|
+
class Rack::ResponseCache
|
10
|
+
# The default proc used if a block is not provided to .new
|
11
|
+
# It unescapes the PATH_INFO of the environment, and makes sure that it doesn't
|
12
|
+
# include '..'. If the Content-Type of the response is text/(html|css|xml),
|
13
|
+
# return a path with the appropriate extension (.html, .css, or .xml).
|
14
|
+
# If the path ends with a / and the Content-Type is text/html, change the basename
|
15
|
+
# of the path to index.html.
|
16
|
+
DEFAULT_PATH_PROC = proc do |env, res|
|
17
|
+
path = Rack::Utils.unescape(env['PATH_INFO'])
|
18
|
+
if !path.include?('..') and match = /text\/((?:x|ht)ml|css)/o.match(res[1]['Content-Type'])
|
19
|
+
type = match[1]
|
20
|
+
path = "#{path}.#{type}" unless /\.#{type}\z/.match(path)
|
21
|
+
path = File.join(File.dirname(path), 'index.html') if type == 'html' and File.basename(path) == '.html'
|
22
|
+
path
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Initialize a new ReponseCache object with the given arguments. Arguments:
|
27
|
+
# * app : The next middleware in the chain. This is always called.
|
28
|
+
# * cache : The place to cache responses. If a string is provided, a disk
|
29
|
+
# cache is used, and all cached files will use this directory as the root directory.
|
30
|
+
# If anything other than a string is provided, it should respond to []=, which will
|
31
|
+
# be called with a path string and a body value (the 3rd element of the response).
|
32
|
+
# * &block : If provided, it is called with the environment and the response from the next middleware.
|
33
|
+
# It should return nil or false if the path should not be cached, and should return
|
34
|
+
# the pathname to use as a string if the result should be cached.
|
35
|
+
# If not provided, the DEFAULT_PATH_PROC is used.
|
36
|
+
def initialize(app, cache, &block)
|
37
|
+
@app = app
|
38
|
+
@cache = cache
|
39
|
+
@path_proc = block || DEFAULT_PATH_PROC
|
40
|
+
end
|
41
|
+
|
42
|
+
# Call the next middleware with the environment. If the request was successful (response status 200),
|
43
|
+
# was a GET request, and had an empty query string, call the block set up in initialize to get the path.
|
44
|
+
# If the cache is a string, create any necessary middle directories, and cache the file in the appropriate
|
45
|
+
# subdirectory of cache. Otherwise, cache the body of the reponse as the value with the path as the key.
|
46
|
+
def call(env)
|
47
|
+
res = @app.call(env)
|
48
|
+
if env['REQUEST_METHOD'] == 'GET' and env['QUERY_STRING'] == '' and res[0] == 200 and path = @path_proc.call(env, res)
|
49
|
+
if @cache.is_a?(String)
|
50
|
+
path = File.join(@cache, path) if @cache
|
51
|
+
FileUtils.mkdir_p(File.dirname(path))
|
52
|
+
File.open(path, 'wb'){|f| res[2].each{|c| f.write(c)}}
|
53
|
+
else
|
54
|
+
@cache[path] = res[2]
|
55
|
+
end
|
56
|
+
end
|
57
|
+
res
|
58
|
+
end
|
59
|
+
end
|