Skip to content

Instantly share code, notes, and snippets.

@samleb
Created December 23, 2010 05:09
Show Gist options
  • Select an option

  • Save samleb/752590 to your computer and use it in GitHub Desktop.

Select an option

Save samleb/752590 to your computer and use it in GitHub Desktop.
Experimenting on caching `stat` calls in Rack::File
require 'time'
require 'rack/utils'
require 'rack/mime'
module Rack
# Rack::File serves files below the +root+ directory given, according to the
# path info of the Rack request.
# e.g. when Rack::File.new("/etc") is used, you can access 'passwd' file
# as http://localhost:9292/passwd
#
# Handlers can detect if bodies are a Rack::File, and use mechanisms
# like sendfile on the +path+.
class File
attr_accessor :root
attr_accessor :path
alias :to_path :path
def initialize(root, options = {})
@root = root
unless options[:cache] == false
@cache = {}
@cache_size = options[:cache_size] || 1000
end
end
def call(env)
dup._call(env)
end
F = ::File
def _call(env)
@path_info = Utils.unescape(env["PATH_INFO"])
return fail(403, "Forbidden") if @path_info.include? ".."
@path = F.join(@root, @path_info)
if attrs = size_and_mtime
serving(env, *attrs)
else
fail(404, "File not found: #{@path_info}")
end
end
def serving(env, size, mtime)
response = [
200,
{
"Last-Modified" => mtime,
"Content-Type" => Mime.mime_type(F.extname(@path), 'text/plain')
},
self
]
ranges = Rack::Utils.byte_ranges(env, size)
if ranges.nil? || ranges.length > 1
# No ranges, or multiple ranges (which we don't support):
# TODO: Support multiple byte-ranges
response[0] = 200
@range = 0..size-1
elsif ranges.empty?
# Unsatisfiable. Return error, and file size:
response = fail(416, "Byte range unsatisfiable")
response[1]["Content-Range"] = "bytes */#{size}"
return response
else
# Partial content:
@range = ranges[0]
response[0] = 206
response[1]["Content-Range"] = "bytes #{@range.begin}-#{@range.end}/#{size}"
size = @range.end - @range.begin + 1
end
response[1]["Content-Length"] = size.to_s
response
end
def each
F.open(@path, "rb") do |file|
file.seek(@range.begin)
remaining_len = @range.end-@range.begin+1
while remaining_len > 0
part = file.read([8192, remaining_len].min)
break unless part
remaining_len -= part.length
yield part
end
end
end
private
def size_and_mtime
return get_size_and_mtime unless @cache
unless entry = @cache[@path]
entry = @cache[@path] = get_size_and_mtime || :unavailable
end
prune_cache if @cache.size > @cache_size
entry if entry != :unavailable
end
def get_size_and_mtime
return unless F.file?(@path) && F.readable?(@path)
# NOTE:
# We check via File::size? whether this file provides size info
# via stat (e.g. /proc files often don't), otherwise we have to
# figure it out by reading the whole file into memory.
size = F.size?(@path) || Utils.bytesize(F.read(@path))
mtime = F.mtime(@path).httpdate
[size, mtime]
rescue SystemCallError
end
def prune_cache
target_size = (@cache_size * 0.75).to_i
excess = @cache.size - target_size
excess = prune_cache_entries(excess, true)
prune_cache_entries(excess) if excess > 0
end
def prune_cache_entries(excess, only_unavailable = false)
@cache.each do |path, attrs|
@cache.delete(path) if !only_unavailable || attrs == :unavailable
return 0 if (excess -= 1).zero?
end
excess
end
def fail(status, body)
body += "\n"
[
status,
{
"Content-Type" => "text/plain",
"Content-Length" => body.size.to_s,
"X-Cascade" => "pass"
},
[body]
]
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment