Merged updates from trunk into stable branch
[feedcatcher.git] / vendor / rails / actionpack / lib / action_controller / vendor / rack-1.0 / rack / directory.rb
1 require 'time'
2 require 'rack/utils'
3 require 'rack/mime'
4
5 module Rack
6 # Rack::Directory serves entries below the +root+ given, according to the
7 # path info of the Rack request. If a directory is found, the file's contents
8 # will be presented in an html based index. If a file is found, the env will
9 # be passed to the specified +app+.
10 #
11 # If +app+ is not specified, a Rack::File of the same +root+ will be used.
12
13 class Directory
14 DIR_FILE = "<tr><td class='name'><a href='%s'>%s</a></td><td class='size'>%s</td><td class='type'>%s</td><td class='mtime'>%s</td></tr>"
15 DIR_PAGE = <<-PAGE
16 <html><head>
17 <title>%s</title>
18 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
19 <style type='text/css'>
20 table { width:100%%; }
21 .name { text-align:left; }
22 .size, .mtime { text-align:right; }
23 .type { width:11em; }
24 .mtime { width:15em; }
25 </style>
26 </head><body>
27 <h1>%s</h1>
28 <hr />
29 <table>
30 <tr>
31 <th class='name'>Name</th>
32 <th class='size'>Size</th>
33 <th class='type'>Type</th>
34 <th class='mtime'>Last Modified</th>
35 </tr>
36 %s
37 </table>
38 <hr />
39 </body></html>
40 PAGE
41
42 attr_reader :files
43 attr_accessor :root, :path
44
45 def initialize(root, app=nil)
46 @root = F.expand_path(root)
47 @app = app || Rack::File.new(@root)
48 end
49
50 def call(env)
51 dup._call(env)
52 end
53
54 F = ::File
55
56 def _call(env)
57 @env = env
58 @script_name = env['SCRIPT_NAME']
59 @path_info = Utils.unescape(env['PATH_INFO'])
60
61 if forbidden = check_forbidden
62 forbidden
63 else
64 @path = F.join(@root, @path_info)
65 list_path
66 end
67 end
68
69 def check_forbidden
70 return unless @path_info.include? ".."
71
72 body = "Forbidden\n"
73 size = Rack::Utils.bytesize(body)
74 return [403, {"Content-Type" => "text/plain","Content-Length" => size.to_s}, [body]]
75 end
76
77 def list_directory
78 @files = [['../','Parent Directory','','','']]
79 glob = F.join(@path, '*')
80
81 Dir[glob].sort.each do |node|
82 stat = stat(node)
83 next unless stat
84 basename = F.basename(node)
85 ext = F.extname(node)
86
87 url = F.join(@script_name, @path_info, basename)
88 size = stat.size
89 type = stat.directory? ? 'directory' : Mime.mime_type(ext)
90 size = stat.directory? ? '-' : filesize_format(size)
91 mtime = stat.mtime.httpdate
92 url << '/' if stat.directory?
93 basename << '/' if stat.directory?
94
95 @files << [ url, basename, size, type, mtime ]
96 end
97
98 return [ 200, {'Content-Type'=>'text/html; charset=utf-8'}, self ]
99 end
100
101 def stat(node, max = 10)
102 F.stat(node)
103 rescue Errno::ENOENT, Errno::ELOOP
104 return nil
105 end
106
107 # TODO: add correct response if not readable, not sure if 404 is the best
108 # option
109 def list_path
110 @stat = F.stat(@path)
111
112 if @stat.readable?
113 return @app.call(@env) if @stat.file?
114 return list_directory if @stat.directory?
115 else
116 raise Errno::ENOENT, 'No such file or directory'
117 end
118
119 rescue Errno::ENOENT, Errno::ELOOP
120 return entity_not_found
121 end
122
123 def entity_not_found
124 body = "Entity not found: #{@path_info}\n"
125 size = Rack::Utils.bytesize(body)
126 return [404, {"Content-Type" => "text/plain", "Content-Length" => size.to_s}, [body]]
127 end
128
129 def each
130 show_path = @path.sub(/^#{@root}/,'')
131 files = @files.map{|f| DIR_FILE % f }*"\n"
132 page = DIR_PAGE % [ show_path, show_path , files ]
133 page.each_line{|l| yield l }
134 end
135
136 # Stolen from Ramaze
137
138 FILESIZE_FORMAT = [
139 ['%.1fT', 1 << 40],
140 ['%.1fG', 1 << 30],
141 ['%.1fM', 1 << 20],
142 ['%.1fK', 1 << 10],
143 ]
144
145 def filesize_format(int)
146 FILESIZE_FORMAT.each do |format, size|
147 return format % (int.to_f / size) if int >= size
148 end
149
150 int.to_s + 'B'
151 end
152 end
153 end