/
file.rb
151 lines (128 loc) · 3.87 KB
/
file.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
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
SEPS = Regexp.union(*[::File::SEPARATOR, ::File::ALT_SEPARATOR].compact)
ALLOWED_VERBS = %w[GET HEAD]
attr_accessor :root
attr_accessor :path
attr_accessor :cache_control
alias :to_path :path
def initialize(root, headers={})
@root = root
# Allow a cache_control string for backwards compatibility
if headers.instance_of? String
warn \
"Rack::File headers parameter replaces cache_control after Rack 1.5."
@headers = { 'Cache-Control' => headers }
else
@headers = headers
end
end
def call(env)
dup._call(env)
end
F = ::File
def _call(env)
unless ALLOWED_VERBS.include? env["REQUEST_METHOD"]
return fail(405, "Method Not Allowed")
end
@path_info = Utils.unescape(env["PATH_INFO"])
parts = @path_info.split SEPS
parts.inject(0) do |depth, part|
case part
when '', '.'
depth
when '..'
return fail(404, "Not Found") if depth - 1 < 0
depth - 1
else
depth + 1
end
end
@path = F.join(@root, *parts)
available = begin
F.file?(@path) && F.readable?(@path)
rescue SystemCallError
false
end
if available
serving(env)
else
fail(404, "File not found: #{@path_info}")
end
end
def serving(env)
last_modified = F.mtime(@path).httpdate
return [304, {}, []] if env['HTTP_IF_MODIFIED_SINCE'] == last_modified
response = [
200,
{
"Last-Modified" => last_modified,
"Content-Type" => Mime.mime_type(F.extname(@path), 'text/plain')
},
env["REQUEST_METHOD"] == "HEAD" ? [] : self
]
# Set custom headers
@headers.each { |field, content| response[1][field] = content } if @headers
# 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))
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 fail(status, body)
body += "\n"
[
status,
{
"Content-Type" => "text/plain",
"Content-Length" => body.size.to_s,
"X-Cascade" => "pass"
},
[body]
]
end
end
end