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

def initialize(root, app = nil)
  @root = ::File.expand_path(root)
  @root_with_separator = @root.end_with?(::File::SEPARATOR) ? @root : "#{@root}#{::File::SEPARATOR}"
  @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



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

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.



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

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.



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

def check_forbidden(path_info)
  return unless path_info.include? ".."

  expanded_path = ::File.expand_path(::File.join(@root, path_info))
  return if expanded_path == @root || expanded_path.start_with?(@root_with_separator)

  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.



184
185
186
187
188
189
# File 'lib/rack/directory.rb', line 184

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



200
201
202
203
204
205
206
# File 'lib/rack/directory.rb', line 200

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.



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

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.



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
161
162
163
# File 'lib/rack/directory.rb', line 133

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.



174
175
176
177
178
179
180
181
# File 'lib/rack/directory.rb', line 174

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.



166
167
168
169
170
# File 'lib/rack/directory.rb', line 166

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