The Crystal Programming Language Forum

Serve static files over HTTP (kemal)

Hello! I’m new to crystal (coming from go) and I’m trying to statically compile files into my webserver (kemal) using https://github.com/schovi/baked_file_system

I’m using

class StaticFiles
  extend BakedFileSystem

  bake_folder "../public"
end

Now i’m wondering if there is an easy way to plug this into kemal, in go i would use https://github.com/gobuffalo/packr their box struct implements the http.FileSystem interface thus allowing me to plug it directly into my webserver, is there something similar i can do in crystal?

Any help is appreciated :smiley:

Hey, welcome to Crystal!

There is no ready-to-use solution for this, but it should be really easy to adapt HTTP::StaticFileHandler to load files from the baked file system. It’s just a couple of calls to File that need to be mapped to StaticFiles.

At one point it would be great to have this kind of file system access plugable, so you can either use a local file system, or some kind of virtual file system, or a merge between several ones (like overlayFS). But that’s ideas for the future…

I created a handler for it a while back:

# This is a handler to serve files from a file system that
# is coupled with the executable, essentially serving files from
# it, using https://github.com/schovi/baked_file_system
class BakedFileSystemHandler
  include HTTP::Handler

  def initialize(@files : BakedFileSystem)
  end

  def call(context)
    return call_next context if context.request.method != "GET"
    path = context.request.path.to_s.sub(/^\//, "")
    serve context, @files.get(path)
  rescue ::BakedFileSystem::NoSuchFileError
    serve context, @files.get("index.html")
  end

  def serve(context, file)
    mime_type = file.name.ends_with?(".js") ? "application/javascript" : file.mime_type
    context.response.content_type = mime_type + "; charset=utf-8"
    context.response.content_length = file.size
    context.response.write file.to_slice
  end
end

This was for an earlier version of Crystal so it might not work, if not it could be probably updated and it has a fallback to index.html which you might not need.

Thanks so much! I’ll use that @gdotdesign, might be worth PR’ing BakedFileSystem as i think this should be something included.

Though long term, i think it would be nice if HTTP::StaticFileHandler used an abstraction instead of a string to serve files from, in go we have http.Dir that is a wrapper around a string but it adds the Open(name string) (File, error) method, i think something similar would be great to have in crystal, here’s my attempt at translating the same abstraction we have in go.

# https://godoc.org/net/http#FileSystem
abstract class HTTP::FileSystem
  abstract def open(path : String) : IO
  end
end

# https://godoc.org/net/http#Dir
class HTTP::Dir < HTTP::FileSystem
  def open(path : String) : IO
    # ...
  end
end

Then HTTP::StaticFileHandler could work like this

class HTTP::StaticFileHandler
    def initialize(@fs : HTTP::FileSystem, fallthrough = true, directory_listing = true)
    @fallthrough = !!fallthrough
    @directory_listing = !!directory_listing
  end
end

I’m sure my implementation is wrong, but i hope you get the idea.

I’m going to move the HTTP::Dir and HTTP::FileSystem suggestion over to #crystal-contrib