Search code examples
rubyfilerackthin

How to mount static files for Rack


I want to serv static files as well as dynamic contents through Rack. Previously, I used WEBrick without using Rack, with code like this, and it worked:

    @s = WEBrick::HTTPServer.new(
        Port: 3000,
        BindAddress: "localhost",
        Logger: WEBrick::Log.new(File::NULL),
        AccessLog: [nil, nil]
    )
    %w[INT TERM].each{|signal| trap(signal){@s.shutdown}}
    @s.mount("/", self)
    @s.mount("/resource/", WEBrick::HTTPServlet::FileHandler, "/")
    @s.start

In the above, requests to localhost:3000 will be responded by dynamic contents, and requests to localhost:3000/resource/path_to_file will be responded by a static file located at /path_to_file on my computer.

Now, I want to switch to a system using Rack with Thin server. I wrote the following, but could not get the static files served. How should it be changed to make it work?

    Rack::Handler::Thin.run(->env{
        h = Rack::Utils.parse_nested_query(env["QUERY_STRING"])
        # I tried the following three lines one at a time, but neither worked.
        use(Rack::Static, urls: "/resource/", root: "/") # First try
        Rack::File.new("/resource").call(env) # Second try
        Rack::Directory.new("/resource").call(env) # Third try
        [200, {}, [some_dyamically_generated_content]]
    }, Port: 3000)

I know that ther is a similar question: How to serve static files via Rack?, but I could not make it work. I am not sure how to use Rack::Static, Rack::File, or Rack::Directory. Please teach me.


Solution

  • You need to use Rack::Builder in order to use the use SomeMiddleware syntax in a normal Ruby script (it's normally used in a config.ru file). You also need run for your application. Note that the urls key to Rack::Static takes an array, not a single string:

    require 'rack'
    Rack::Handler::Thin.run(Rack::Builder.new {
        use(Rack::Static, urls: ["/resource/"], root: "/")
        run ->env{[200, {}, [some_dyamically_generated_content]]}
    }, Port: 3000)
    

    Here Rack::Builder is taking your application, ->env{[200, {}, [some_dyamically_generated_content]]}, adding the Rack::Static middleware to it and creating a new combined application which is then passed to Thin to run.

    Rack::Static is a middleware component that you can add to existing rack applications. Rack::File and Rack::Directory are both themselves rack applications, not middleware (Rack::Static uses Rack::File internally, as does Rack::Directory by default). You could achieve the same effect as above using Rack::File and the map command:

    require 'rack'
    Rack::Handler::Thin.run(Rack::Builder.new {
        map "/resource/" do
          run Rack::File.new "/"
        end
        map "/" do
          run ->env{[200, {}, [some_dyamically_generated_content]]}
        end
    }, Port: 3000)
    

    The more common way to do this would be to put the contents of the block passed to Rack::Bundler.new in a config.ru file:

    use(Rack::Static, urls: ["/resource/"], root: "/") 
    run ->env{[200, {}, [some_dyamically_generated_content]]}
    

    You can then run this with thin start, which should find config.ru if run from the same directory, or you can use the -R option to specify the file. The rackup command can also be used, rackup -s thin if you want to specify Thin as the server.