Class: Rack::TailFile

Inherits:
Object
  • Object
show all
Defined in:
lib/rack/tail_file.rb,
lib/rack/tail_file/version.rb

Overview

Rack::File serves files below the root directory given, according to the path info of the Rack request. e.g. when Rack::File.new(“/etc”) is used, you can access ‘passwd’ file as localhost:9292/passwd

Handlers can detect if bodies are a Rack::File, and use mechanisms like sendfile on the path.

Constant Summary collapse

SEPS =
Regexp.union(*[::File::SEPARATOR, ::File::ALT_SEPARATOR].compact)
ALLOWED_VERBS =
%w[GET HEAD]
F =
::File
VERSION =
"0.0.2"

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(root, headers = {}, default_mime = 'text/plain') ⇒ TailFile

Returns a new instance of TailFile.



28
29
30
31
32
# File 'lib/rack/tail_file.rb', line 28

def initialize(root, headers={}, default_mime = 'text/plain')
  @root = root
  @headers = headers
  @default_mime = default_mime
end

Instance Attribute Details

#cache_controlObject

Returns the value of attribute cache_control.



24
25
26
# File 'lib/rack/tail_file.rb', line 24

def cache_control
  @cache_control
end

#pathObject Also known as: to_path

Returns the value of attribute path.



23
24
25
# File 'lib/rack/tail_file.rb', line 23

def path
  @path
end

#root(env) ⇒ Object

Returns the value of attribute root.



22
23
24
# File 'lib/rack/tail_file.rb', line 22

def root
  @root
end

Instance Method Details

#_call(env) ⇒ Object



40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/rack/tail_file.rb', line 40

def _call(env)
  return fail(405, "Method Not Allowed") unless method_allowed?(env)
  return fail(403, "Forbidden") unless path_is_within_root?(env)

  @path = file_path(env)

  if available?
    serving(env)
  else
    fail(404, "File not found: #{path_info_for(env)}")
  end
end

#available?Boolean

Returns:

  • (Boolean)


57
58
59
60
61
62
63
# File 'lib/rack/tail_file.rb', line 57

def available?
  begin
    F.file?(@path) && F.readable?(@path)
  rescue SystemCallError
    false
  end
end

#call(env) ⇒ Object



34
35
36
# File 'lib/rack/tail_file.rb', line 34

def call(env)
  dup._call(env)
end

#eachObject



147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/rack/tail_file.rb', line 147

def each
  F.open(@path, "rb") do |file|
    file.seek(@range.begin)
    remaining_len = @range.end-@range.begin+1
    while remaining_len > 0
      part = file.read([8192, remaining_len].min)
      break unless part
      remaining_len -= part.length

      yield part
    end
  end
end

#file_path(env) ⇒ Object



85
86
87
# File 'lib/rack/tail_file.rb', line 85

def file_path(env)
  target_file env
end

#method_allowed?(env) ⇒ Boolean

Returns:

  • (Boolean)


53
54
55
# File 'lib/rack/tail_file.rb', line 53

def method_allowed? env
  ALLOWED_VERBS.include? env["REQUEST_METHOD"]
end

#path_info_for(env) ⇒ Object



65
66
67
# File 'lib/rack/tail_file.rb', line 65

def path_info_for env
  Utils.unescape(env["PATH_INFO"])
end

#path_is_within_root?(env) ⇒ Boolean

Returns:

  • (Boolean)


69
70
71
72
73
# File 'lib/rack/tail_file.rb', line 69

def path_is_within_root? env
  root = root env
  target = target_file env
  !target.relative_path_from(root).to_s.split(SEPS).any?{|p| p == ".."}
end

#requested_lines_size(env) ⇒ Object



105
106
107
# File 'lib/rack/tail_file.rb', line 105

def requested_lines_size(env)
  (env.fetch("QUERY_STRING")[/\d+/] || 50).to_i
end

#requested_size(env, response) ⇒ Object



123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# File 'lib/rack/tail_file.rb', line 123

def requested_size(env, response)
  # NOTE:
  #   We check via File::size? whether this file provides size info
  #   via stat (e.g. /proc files often don't), otherwise we have to
  #   figure it out by reading the whole file into memory.
  size = F.size?(@path) || Utils.bytesize(F.read(@path))

  #TODO handle invalid lines
  tail_size = tail_size_for requested_lines_size(env)

  if tail_size == size
    response[0] = 200
    @range = 0..size-1
  else
    start_byte = size - tail_size - 1
    @range = start_byte..size-1
    response[0] = 206
    response[1]["Content-Range"] = "bytes #{@range.begin}-#{@range.end}/#{size}"
    size = @range.end - @range.begin + 1
  end

  size
end

#serving(env) ⇒ Object



89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/rack/tail_file.rb', line 89

def serving(env)
  last_modified = F.mtime(@path).httpdate
  return [304, {}, []] if env['HTTP_IF_MODIFIED_SINCE'] == last_modified

  headers = { "Last-Modified" => last_modified }
  mime = Mime.mime_type(F.extname(@path), @default_mime)
  headers["Content-Type"] = mime if mime

  # Set custom headers
  @headers.each { |field, content| headers[field] = content } if @headers

  response = [ 200, headers, env["REQUEST_METHOD"] == "HEAD" ? [] : self ]
  response[1]["Content-Length"] = requested_size(env, response).to_s
  response
end

#tail_size_for(line_count) ⇒ Object



109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/rack/tail_file.rb', line 109

def tail_size_for line_count
  elif = Elif.new(@path)
  tail_size = 0
  line_count.times do
    begin
      tail_size += Rack::Utils.bytesize(elif.readline)
    rescue EOFError
      return tail_size
    end
  end
  tail_size - 1 # Don't include the first \n
end

#target_file(env) ⇒ Object



75
76
77
78
79
# File 'lib/rack/tail_file.rb', line 75

def target_file env
  path_info = Pathname.new("").join(*path_info_for(env).split(SEPS))
  root = root env
  root.join(path_info)
end