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.


77
78
79
80
81
# File 'lib/rack/directory.rb', line 77

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.


74
75
76
# File 'lib/rack/directory.rb', line 74

def root
  @root
end

Instance Method Details

#call(env) ⇒ Object


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

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.


103
104
105
106
107
108
109
110
# File 'lib/rack/directory.rb', line 103

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.


113
114
115
116
117
118
119
120
121
# File 'lib/rack/directory.rb', line 113

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.


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

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


191
192
193
194
195
196
197
# File 'lib/rack/directory.rb', line 191

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.


90
91
92
93
94
95
96
97
98
99
100
# File 'lib/rack/directory.rb', line 90

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.


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
152
153
154
# File 'lib/rack/directory.rb', line 124

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.


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

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.


157
158
159
160
161
# File 'lib/rack/directory.rb', line 157

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