Skip to content

Loading…

Extract several methods from Rack::File#serving #570

Closed
wants to merge 2 commits into from

3 participants

@graysonwright

Extracted methods:

  • mime_type
  • filesize
  • response_body

Motivation

  1. The method is somewhat complicated
  2. Extracting pieces of the method into their own functions really improves your ability to extend the class to customize it.

Example of Customization

Let's say you have a basic Rack::Directory setup.

run Rack::Directory.new(".")

But you have a lot of markdown files in the directory, and you want to compile and render them on the fly. That requires modifying the mime type, the response body, and the filesize.

By extracting the methods out of Rack::File#serving, you only have to overwrite two methods (file_size is calculated based on the new response_body

The config.ru would look something like this:

require 'redcarpet'
require 'rack/mime'

def markdown_compiler
  Redcarpet::Markdown.new(Redcarpet::Render::HTML.new)
end

class Rack::File
  # Overwrite
  def mime_type
    @mime ||= Rack::Mime.mime_type(F.extname(@path), @default_mime)
    markdown? ? 'text/html' : @mime
  end

  # Overwrite
  def response_body
    if markdown?
      @response_body ||= markdown_compiler.render(F.read(@path))
    else
      nil
    end
  end

  def markdown?
    F.extname(@path) == '.md'
  end
end

run Rack::Directory.new(".")

Other stuff

This doesn't change the behavior of Rack, it just makes it easier to customize and to work on in the future. Because of that, I didn't write any tests. Let me know if I should.

@graysonwright graysonwright Extract several methods from Rack::File#serving
Extracted methods:

  - mime_type
  - filesize
  - response_body
0d2138a
@rkh
Official Rack repositories member
rkh commented

Thanks, generally looks good, but won't this break horribly if the file changes?

@graysonwright

I don't think so -- I'm playing around with it here and it seems to be working fine.

If the file hasn't been modified since env['HTTP_IF_MODIFIED_SINCE'], it still returns a 304 response.
If it has been modified since then, it uses whatever is in the response_body method to generate a new body, then serves that.

Of course, it would be pretty trivial to extract a method (not sure about the name):

def cached?
  last_modified = F.mtime(@path).httpdate
  env['HTTP_IF_MODIFIED_SINCE'] == last_modified
end

so that whoever's extending it can deal with the problem easier.

@rkh
Official Rack repositories member
rkh commented

But isn't the file size cached?

@graysonwright

Are you talking about the instance variable I set up in the filesize method?

Then yeah, I think you're right. I was only paying attention to the Rack::Directory scenario, where a new Rack::File is created for each request (at least that's how I understand it).

But if your config.ru uses run Rack::File.new('some_file.md') then I think it would break.

Can you confirm that's the problem you're talking about?

@rkh
Official Rack repositories member
rkh commented

Yes. I was referring to using Rack::File directly as an endpoint.

@graysonwright

Hopefully that takes care of it.

@raggi raggi commented on an outdated diff
lib/rack/file.rb
@@ -134,5 +131,33 @@ def fail(status, body)
]
end
+ # The MIME type for the contents of the file located at @path
+ #
+ # This method can be overridden to specify custom MIME types for certain files,
@raggi Official Rack repositories member
raggi added a note

I do not like this comment. It seems more sensible to adjust the mime tables.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@raggi
Official Rack repositories member

I think Rack::File should in future be avoiding setting Content-Length if it cannot determine the size of the body by stat. In the case where it cannot, it should be deferring to Rack::ContentLength and/or Rack::Chunked, to avoid slurping until the last responsible moment.

@raggi
Official Rack repositories member

In general, these changes are ok, I'm concerned about introducing comments that enforce inheritance as a public API. I would prefer that these are removed.

@raggi raggi added this to the Rack 1.6 milestone
@graysonwright graysonwright Remove caching of response body and filesize
Causes a bug if a file changes between requests.
a414cc5
@graysonwright

Good point -- I didn't know about the MIME tables when I added those comments. Just removed the offending lines.

@spastorino spastorino added a commit that closed this pull request
@graysonwright graysonwright Extract several methods from Rack::File#serving
Extracted methods:

  - mime_type
  - filesize
  - response_body

Closes #570

Signed-off-by: Santiago Pastorino <santiago@wyeworks.com>
398c59f
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Jun 3, 2013
  1. @graysonwright

    Extract several methods from Rack::File#serving

    graysonwright committed
    Extracted methods:
    
      - mime_type
      - filesize
      - response_body
Commits on Jul 13, 2014
  1. @graysonwright

    Remove caching of response body and filesize

    graysonwright committed
    Causes a bug if a file changes between requests.
This page is out of date. Refresh to see the latest.
Showing with 26 additions and 7 deletions.
  1. +26 −7 lib/rack/file.rb
View
33 lib/rack/file.rb
@@ -68,19 +68,14 @@ def serving(env)
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
+ headers["Content-Type"] = mime_type if mime_type
# Set custom headers
@headers.each { |field, content| headers[field] = content } if @headers
response = [ 200, headers, env["REQUEST_METHOD"] == "HEAD" ? [] : self ]
- # 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))
+ size = filesize
ranges = Rack::Utils.byte_ranges(env, size)
if ranges.nil? || ranges.length > 1
@@ -101,6 +96,8 @@ def serving(env)
size = @range.end - @range.begin + 1
end
+ response[2] = [response_body] unless response_body.nil?
+
response[1]["Content-Length"] = size.to_s
response
end
@@ -134,5 +131,27 @@ def fail(status, body)
]
end
+ # The MIME type for the contents of the file located at @path
+ def mime_type
+ Mime.mime_type(F.extname(@path), @default_mime)
+ end
+
+ def filesize
+ # If response_body is present, use its size.
+ return Rack::Utils.bytesize(response_body) if response_body
+
+ # 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.
+ F.size?(@path) || Utils.bytesize(F.read(@path))
+ end
+
+ # By default, the response body for file requests is nil.
+ # In this case, the response body will be generated later
+ # from the file at @path
+ def response_body
+ nil
+ end
+
end
end
Something went wrong with that request. Please try again.