Serving Files

Notes

Exercise: Read an HTML Form File

In the prepared-files directory of the code bundle, you'll find the file form.html with the following contents:

<form action="/bears" method="POST">
  <p>
    Name:<br/>
    <input type="text" name="name">    
  </p>
  <p>
    Type:<br/>
    <input type="text" name="type">    
  </p>
  <p>
    <input type="submit" value="Create Bear">
  </p>
</form>

Copy this form.html file into your servy/pages directory.

We'll use this basic HTML form a bit later to create a new bear. For now, just define a route that handles requests for /bears/new and responds by serving the form.html file. Here's the full request:

request = """
GET /bears/new HTTP/1.1
Host: example.com
User-Agent: ExampleBrowser/1.0
Accept: */*

"""

You'll end up with a small bit of duplication when it comes to getting the absolute path to the file. Don't worry about it—we'll tidy that up in the next section.

Make sure to handle both the success and failure cases. Try both approaches: using a case expression and using multi-clause functions. Which do you prefer? Can you explain why?

Show Answer

Hide Answer

# Using a case expression:

def route(%{method: "GET", path: "/bears/new"} = conv) do
  pages_path = Path.expand("../../pages", __DIR__)
  file = Path.join(pages_path, "form.html")

  case File.read(file) do
    {:ok, content} ->
      %{ conv | status: 200, resp_body: content }
    
    {:error, :enoent} ->
      %{ conv | status: 404, resp_body: "File not found!"}

    {:error, reason } ->
      %{ conv | status: 500, resp_body: "File error: #{reason}"}
  end
end

# Using multi-clause functions:

def route(%{method: "GET", path: "/bears/new"} = conv) do
  Path.expand("../../pages", __DIR__)
    |> Path.join("form.html")
    |> File.read
    |> handle_file(conv)
end

def handle_file({:ok, content}, conv) do
  %{ conv | status: 200, resp_body: content }
end

def handle_file({:error, :enoent}, conv) do
  %{ conv | status: 404, resp_body: "File not found!" }
end

def handle_file({:error, reason}, conv) do
  %{ conv | status: 500, resp_body: "File error: #{reason}" }
end

Exercise: Serve Arbitrary Page Files

Suppose you had other static pages in the pages directory such as contact.html, faq.html, and so on. You already know how to define separate route functions that serve each of those files. Instead, how would you define one generic route function that handles the following requests:

/pages/contact
/pages/faq
/pages/any-other-page

Show Answer

Hide Answer

def route(%{method: "GET", path: "/pages/" <> file} = conv) do
  Path.expand("../../pages", __DIR__)
  |> Path.join(file <> ".html")
  |> File.read
  |> handle_file(conv)
end

Exercise: Explore the Docs

The File and Path modules define familiar functions for working with (wait for it) files and paths. Open an iex session and use the h helper function to pull up the documentation for these modules. Spend a few minutes looking through the functions to get a sense of what's possible.

Code So Far

The code for this video is in the serve-files directory found within the video-code directory of the code bundle.