Building a macro to setup a static file server. Looking for help

I have a web project with a bunch of assets where all the assets are small enough that I though reading them all at compile time might be an interesting way to include them in a release. (I also guessed it might be really fast, but thats not the issue here).

To that end I had an assets controller that read all the files and looks like this: (This code works great I have left the notes in for all the things it does not do)

defmodule Assets do
    asset_dir = Path.expand("./assets", Path.dirname(__ENV__.file))
    assets = Path.expand("./**/*", asset_dir) |> Path.wildcard

    # other things this should do are
    # - send a response for a HEAD request
    # - return a method not allowed for other HTTP methods
    # - return content error from accept headers
    # - gzip encoding
    # - have an overwritable not_found function
    # - cache control time
    # - Etags
    # - filtered reading of a file
    # - set a maximum size of file to bundle into the code.

    for asset <- assets do
      case File.read(asset) do
        {:ok, content} ->
          relative = Path.relative_to(asset, asset_dir)
          path = Path.split(relative)
          mime = MIME.from_path(asset)

          def handle_request(%{path: unquote(path)}, _) do
            Raxx.Response.ok(unquote(content), [
              {"content-length", "#{:erlang.iolist_size(unquote(content))}"},
              {"content-type", unquote(mime)}
            ])
          end

        {:error, reason} ->
          IO.inspect(reason)
      end
    end

In summary I am looking to generalise this code so I can reuse it across projects. The end API looking something like

defmodule Assets do
  use Raxx.Static, dir: "./static", files: ["./**/*.css", "robots.txt"]
end

I am working through understanding macros but cannot make it work for more than one file at a time. I have an open pull request here. There is one failing test, that is when it tries to serve a second file. Any pointers would be greatly received or some resources to some more advanced macro tutorial, so far I have read Metaprogramming Elixir and looked at the Plug source code but to no avail

Most of what you need may already be present in Plug.Static https://github.com/elixir-lang/plug/blob/master/lib/plug/static.ex

There is a separate Gzip plug here: https://github.com/minhajuddin/plug_contrib/blob/master/lib/plug_contrib/gzip.ex

The plan is to experiment with a different approach to plug.
I can’t really find what I need there, one of the key differences is I think that plug reads the file for every request.

Random idea, but why even read the files ?

The goal when you serve Static stuff is to just copy the bytes from the files onto the socket.

So what you may want is just to load the file in memory, route to them and write them to the socket.

1 Like

The most efficient way to send files to sockets is using the sendfile syscall - it involves no copying in memory, the file is directly copied into the socket - additionally it all happens in the kernel and there’s no context switch between userspace and kernelspace. I don’t think there exists a more efficient way of doing this. This is realised in Erlang by using the :file.sendfile/2 function and in plug with Plug.Conn.send_file/5. This is properly used by the Plug.Static plug.

2 Likes