Class: Rack::Directory

Inherits:
Object
  • Object
show all
Defined in:
lib/rack/directory.rb

Overview

Rack::Directory serves entries below the root given, according to the path info of the Rack request. If a directory is found, the file’s contents will be presented in an html based index. If a file is found, the env will be passed to the specified app.

If app is not specified, a Rack::Files of the same root will be used.

Defined Under Namespace

Classes: DirectoryBody

Constant Summary collapse

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>\n"
DIR_PAGE_HEADER =
<<-PAGE
<html><head>
  <title>%s</title>
  <meta http-equiv="content-type" content="text/html; charset=utf-8" />
  <style type='text/css'>
table { width:100%%; }
.name { text-align:left; }
.size, .mtime { text-align:right; }
.type { width:11em; }
.mtime { width:15em; }
  </style>
</head><body>
<h1>%s</h1>
<hr />
<table>
  <tr>
<th class='name'>Name</th>
<th class='size'>Size</th>
<th class='type'>Type</th>
<th class='mtime'>Last Modified</th>
  </tr>
PAGE
<<-PAGE
</table>
<hr />
</body></html>
PAGE
FILESIZE_FORMAT =

Stolen from Ramaze

[
  ['%.1fT', 1 << 40],
  ['%.1fG', 1 << 30],
  ['%.1fM', 1 << 20],
  ['%.1fK', 1 << 10],
]

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(root, app = nil) ⇒ Directory

Set the root directory and application for serving files.



83
84
85
86
87
# File 'lib/rack/directory.rb', line 83

def initialize(root, app = nil)
  @root = ::File.expand_path(root)
  @app = app || Files.new(@root)
  @head = Head.new(method(:get))
end

Instance Attribute Details

#rootObject (readonly)

The root of the directory hierarchy. Only requests for files and directories inside of the root directory are supported.



80
81
82
# File 'lib/rack/directory.rb', line 80

def root
  @root
end

Instance Method Details

#call(env) ⇒ Object



89
90
91
92
# File 'lib/rack/directory.rb', line 89

def call(env)
  # strip body if this is a HEAD call
  @head.call env
end

#check_bad_request(path_info) ⇒ Object

Rack response to use for requests with invalid paths, or nil if path is valid.



109
110
111
112
113
114
115
116
# File 'lib/rack/directory.rb', line 109

def check_bad_request(path_info)
  return if Utils.valid_path?(path_info)

  body = "Bad Request\n"
  [400, { CONTENT_TYPE => "text/plain",
    CONTENT_LENGTH => body.bytesize.to_s,
    "x-cascade" => "pass" }, [body]]
end

#check_forbidden(path_info) ⇒ Object

Rack response to use for requests with paths outside the root, or nil if path is inside the root.



119
120
121
122
123
124
125
126
127
# File 'lib/rack/directory.rb', line 119

def check_forbidden(path_info)
  return unless path_info.include? ".."
  return if ::File.expand_path(::File.join(@root, path_info)).start_with?(@root)

  body = "Forbidden\n"
  [403, { CONTENT_TYPE => "text/plain",
    CONTENT_LENGTH => body.bytesize.to_s,
    "x-cascade" => "pass" }, [body]]
end

#entity_not_found(path_info) ⇒ Object

Rack response to use for unreadable and non-file, non-directory entries.



181
182
183
184
185
186
# File 'lib/rack/directory.rb', line 181

def entity_not_found(path_info)
  body = "Entity not found: #{path_info}\n"
  [404, { CONTENT_TYPE => "text/plain",
    CONTENT_LENGTH => body.bytesize.to_s,
    "x-cascade" => "pass" }, [body]]
end

#filesize_format(int) ⇒ Object

Provide human readable file sizes



197
198
199
200
201
202
203
# File 'lib/rack/directory.rb', line 197

def filesize_format(int)
  FILESIZE_FORMAT.each do |format, size|
    return format % (int.to_f / size) if int >= size
  end

  "#{int}B"
end

#get(env) ⇒ Object

Internals of request handling. Similar to call but does not remove body for HEAD requests.



96
97
98
99
100
101
102
103
104
105
106
# File 'lib/rack/directory.rb', line 96

def get(env)
  script_name = env[SCRIPT_NAME]
  path_info = Utils.unescape_path(env[PATH_INFO])

  if client_error_response = check_bad_request(path_info) || check_forbidden(path_info)
    client_error_response
  else
    path = ::File.join(@root, path_info)
    list_path(env, path, path_info, script_name)
  end
end

#list_directory(path_info, path, script_name) ⇒ Object

Rack response to use for directories under the root.



130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/rack/directory.rb', line 130

def list_directory(path_info, path, script_name)
  url_head = (script_name.split('/') + path_info.split('/')).map do |part|
    Utils.escape_path part
  end

  # Globbing not safe as path could contain glob metacharacters
  body = DirectoryBody.new(@root, path, ->(basename) do
    stat = stat(::File.join(path, basename))
    next unless stat

    url = ::File.join(*url_head + [Utils.escape_path(basename)])
    mtime = stat.mtime.httpdate
    if stat.directory?
      type = 'directory'
      size = '-'
      url << '/'
      if basename == '..'
        basename = 'Parent Directory'
      else
        basename << '/'
      end
    else
      type = Mime.mime_type(::File.extname(basename))
      size = filesize_format(stat.size)
    end

    [ url, basename, size, type, mtime ]
  end)

  [ 200, { CONTENT_TYPE => 'text/html; charset=utf-8' }, body ]
end

#list_path(env, path, path_info, script_name) ⇒ Object

Rack response to use for files and directories under the root. Unreadable and non-file, non-directory entries will get a 404 response.



171
172
173
174
175
176
177
178
# File 'lib/rack/directory.rb', line 171

def list_path(env, path, path_info, script_name)
  if (stat = stat(path)) && stat.readable?
    return @app.call(env) if stat.file?
    return list_directory(path_info, path, script_name) if stat.directory?
  end

  entity_not_found(path_info)
end

#stat(path) ⇒ Object

File::Stat for the given path, but return nil for missing/bad entries.



163
164
165
166
167
# File 'lib/rack/directory.rb', line 163

def stat(path)
  ::File.stat(path)
rescue Errno::ENOENT, Errno::ELOOP
  return nil
end