2 # filehandler.rb -- FileHandler Module
4 # Author: IPR -- Internet Programming with Ruby -- writers
5 # Copyright (c) 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou
6 # Copyright (c) 2003 Internet Programming with Ruby writers. All rights
9 # $IPR: filehandler.rb,v 1.44 2003/06/07 01:34:51 gotoyuzo Exp $
14 require 'webrick/htmlutils'
15 require 'webrick/httputils'
16 require 'webrick/httpstatus'
21 class DefaultFileHandler < AbstractServlet
22 def initialize(server, local_path)
24 @local_path = local_path
28 st = File::stat(@local_path)
30 res['etag'] = sprintf("%x-%x-%x", st.ino, st.size, st.mtime.to_i)
32 if not_modified?(req, res, mtime, res['etag'])
34 raise HTTPStatus::NotModified
36 make_partial_content(req, res, @local_path, st.size)
37 raise HTTPStatus::PartialContent
39 mtype = HTTPUtils::mime_type(@local_path, @config[:MimeTypes])
40 res['content-type'] = mtype
41 res['content-length'] = st.size
42 res['last-modified'] = mtime.httpdate
43 res.body = open(@local_path, "rb")
47 def not_modified?(req, res, mtime, etag)
48 if ir = req['if-range']
50 if Time.httpdate(ir) >= mtime
54 if HTTPUtils::split_header_value(ir).member?(res['etag'])
60 if (ims = req['if-modified-since']) && Time.parse(ims) >= mtime
64 if (inm = req['if-none-match']) &&
65 HTTPUtils::split_header_value(inm).member?(res['etag'])
72 def make_partial_content(req, res, filename, filesize)
73 mtype = HTTPUtils::mime_type(filename, @config[:MimeTypes])
74 unless ranges = HTTPUtils::parse_range_header(req['range'])
75 raise HTTPStatus::BadRequest,
76 "Unrecognized range-spec: \"#{req['range']}\""
78 open(filename, "rb"){|io|
81 boundary = "#{time.sec}_#{time.usec}_#{Process::pid}"
84 first, last = prepare_range(range, filesize)
87 content = io.read(last-first+1)
88 body << "--" << boundary << CRLF
89 body << "Content-Type: #{mtype}" << CRLF
90 body << "Content-Range: #{first}-#{last}/#{filesize}" << CRLF
95 raise HTTPStatus::RequestRangeNotSatisfiable if body.empty?
96 body << "--" << boundary << "--" << CRLF
97 res["content-type"] = "multipart/byteranges; boundary=#{boundary}"
99 elsif range = ranges[0]
100 first, last = prepare_range(range, filesize)
101 raise HTTPStatus::RequestRangeNotSatisfiable if first < 0
102 if last == filesize - 1
107 content = io.read(last-first+1)
109 res['content-type'] = mtype
110 res['content-range'] = "#{first}-#{last}/#{filesize}"
111 res['content-length'] = last - first + 1
114 raise HTTPStatus::BadRequest
119 def prepare_range(range, filesize)
120 first = range.first < 0 ? filesize + range.first : range.first
121 return -1, -1 if first < 0 || first >= filesize
122 last = range.last < 0 ? filesize + range.last : range.last
123 last = filesize - 1 if last >= filesize
128 class FileHandler < AbstractServlet
129 HandlerTable = Hash.new
131 def self.add_handler(suffix, handler)
132 HandlerTable[suffix] = handler
135 def self.remove_handler(suffix)
136 HandlerTable.delete(suffix)
139 def initialize(server, root, options={}, default=Config::FileHandler)
140 @config = server.config
141 @logger = @config[:Logger]
142 @root = File.expand_path(root)
143 if options == true || options == false
144 options = { :FancyIndexing => options }
146 @options = default.dup.update(options)
149 def service(req, res)
150 # if this class is mounted on "/" and /~username is requested.
151 # we're going to override path informations before invoking service.
152 if defined?(Etc) && @options[:UserDir] && req.script_name.empty?
153 if %r|^(/~([^/]+))| =~ req.path_info
154 script_name, user = $1, $2
157 passwd = Etc::getpwnam(user)
158 @root = File::join(passwd.dir, @options[:UserDir])
159 req.script_name = script_name
160 req.path_info = path_info
162 @logger.debug "#{self.class}#do_GET: getpwnam(#{user}) failed"
170 unless exec_handler(req, res)
171 set_dir_list(req, res)
175 def do_POST(req, res)
176 unless exec_handler(req, res)
177 raise HTTPStatus::NotFound, "`#{req.path}' not found."
181 def do_OPTIONS(req, res)
182 unless exec_handler(req, res)
188 # RFC2518: HTTP Extensions for Distributed Authoring -- WEBDAV
190 # PROPFIND PROPPATCH MKCOL DELETE PUT COPY MOVE
193 # RFC3253: Versioning Extensions to WebDAV
194 # (Web Distributed Authoring and Versioning)
196 # VERSION-CONTROL REPORT CHECKOUT CHECK_IN UNCHECKOUT
197 # MKWORKSPACE UPDATE LABEL MERGE ACTIVITY
201 def exec_handler(req, res)
202 raise HTTPStatus::NotFound, "`#{req.path}' not found" unless @root
203 if set_filename(req, res)
204 handler = get_handler(req)
205 call_callback(:HandlerCallback, req, res)
206 h = handler.get_instance(@config, res.filename)
210 call_callback(:HandlerCallback, req, res)
215 suffix1 = (/\.(\w+)$/ =~ req.script_name) && $1.downcase
216 suffix2 = (/\.(\w+)\.[\w\-]+$/ =~ req.script_name) && $1.downcase
217 handler_table = @options[:HandlerTable]
218 return handler_table[suffix1] || handler_table[suffix2] ||
219 HandlerTable[suffix1] || HandlerTable[suffix2] ||
223 def set_filename(req, res)
224 res.filename = @root.dup
225 path_info = req.path_info.scan(%r|/[^/]*|)
227 path_info.unshift("") # dummy for checking @root dir
228 while base = path_info.first
229 check_filename(req, res, base)
231 break unless File.directory?(res.filename + base)
232 shift_path_info(req, res, path_info)
233 call_callback(:DirectoryCallback, req, res)
236 if base = path_info.first
237 check_filename(req, res, base)
239 if file = search_index_file(req, res)
240 shift_path_info(req, res, path_info, file)
241 call_callback(:FileCallback, req, res)
244 shift_path_info(req, res, path_info)
245 elsif file = search_file(req, res, base)
246 shift_path_info(req, res, path_info, file)
247 call_callback(:FileCallback, req, res)
250 raise HTTPStatus::NotFound, "`#{req.path}' not found."
257 def check_filename(req, res, name)
258 @options[:NondisclosureName].each{|pattern|
259 if File.fnmatch("/#{pattern}", name)
260 @logger.warn("the request refers nondisclosure name `#{name}'.")
261 raise HTTPStatus::NotFound, "`#{req.path}' not found."
266 def shift_path_info(req, res, path_info, base=nil)
267 tmp = path_info.shift
269 req.path_info = path_info.join
270 req.script_name << base
274 def search_index_file(req, res)
275 @config[:DirectoryIndex].each{|index|
276 if file = search_file(req, res, "/"+index)
283 def search_file(req, res, basename)
284 langs = @options[:AcceptableLanguages]
285 path = res.filename + basename
289 req.accept_language.each{|lang|
290 path_with_lang = path + ".#{lang}"
291 if langs.member?(lang) && File.file?(path_with_lang)
292 return basename + ".#{lang}"
295 (langs - req.accept_language).each{|lang|
296 path_with_lang = path + ".#{lang}"
297 if File.file?(path_with_lang)
298 return basename + ".#{lang}"
305 def call_callback(callback_name, req, res)
306 if cb = @options[callback_name]
311 def nondisclosure_name?(name)
312 @options[:NondisclosureName].each{|pattern|
313 if File.fnmatch(pattern, name)
320 def set_dir_list(req, res)
321 redirect_to_directory_uri(req, res)
322 unless @options[:FancyIndexing]
323 raise HTTPStatus::Forbidden, "no access permission to `#{req.path}'"
325 local_path = res.filename
326 list = Dir::entries(local_path).collect{|name|
327 next if name == "." || name == ".."
328 next if nondisclosure_name?(name)
329 st = (File::stat(local_path + name) rescue nil)
333 [ name + "/", st.mtime, -1 ]
335 [ name, st.mtime, st.size ]
340 if d0 = req.query["N"]; idx = 0
341 elsif d0 = req.query["M"]; idx = 1
342 elsif d0 = req.query["S"]; idx = 2
343 else d0 = "A" ; idx = 0
345 d1 = (d0 == "A") ? "D" : "A"
348 list.sort!{|a,b| a[idx] <=> b[idx] }
350 list.sort!{|a,b| b[idx] <=> a[idx] }
353 res['content-type'] = "text/html"
355 res.body = <<-_end_of_html_
356 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
358 <HEAD><TITLE>Index of #{HTMLUtils::escape(req.path)}</TITLE></HEAD>
360 <H1>Index of #{HTMLUtils::escape(req.path)}</H1>
363 res.body << "<PRE>\n"
364 res.body << " <A HREF=\"?N=#{d1}\">Name</A> "
365 res.body << "<A HREF=\"?M=#{d1}\">Last modified</A> "
366 res.body << "<A HREF=\"?S=#{d1}\">Size</A>\n"
369 list.unshift [ "..", File::mtime(local_path+".."), -1 ]
370 list.each{ |name, time, size|
372 dname = "Parent Directory"
374 dname = name.sub(/^(.{23})(.*)/){ $1 + ".." }
378 s = " <A HREF=\"#{HTTPUtils::escape(name)}\">#{dname}</A>"
379 s << " " * (30 - dname.size)
380 s << (time ? time.strftime("%Y/%m/%d %H:%M ") : " " * 22)
381 s << (size >= 0 ? size.to_s : "-") << "\n"
384 res.body << "</PRE><HR>"
386 res.body << <<-_end_of_html_
388 #{HTMLUtils::escape(@config[:ServerSoftware])}<BR>
389 at #{req.host}:#{req.port}