HTTP

In this tutorial we will write an HTTP file server. The server takes the HTTP path and serves the corresponding file from the local file system. In spirit it is very similar to python's python -m http.server command.

You will learn how to use the http package to serve HTML pages and the host package to list local files.

Prerequisites

We assume that you have set up your development environment as described in the IDE tutorial.

This tutorial is written for the desktop and not the ESP32. You should still have a look at the Hello world tutorial. However, instead of running the program on the ESP32, you should run it on your desktop, using the -d host option to the jag run command. Note, that jag watch does not support the -d option, so you will have to manually restart the program after each change.

Packages

The HTTP functionality is not part of the core libraries and must be imported as a package. See the packages tutorial for details.

We are using the http package. To install it, run the following command:

jag pkg install github.com/toitlang/pkg-http@v2

For file access we will use the host package. To install it, run the following command:

jag pkg install github.com/toitlang/pkg-host@v1

You can probably just write jag pkg install http and jag pkg install host, but the full IDs together with the versions are more explicit, and will make sure you get the right packages.

Feel free to use a newer version of the package if one is available. You might need to update the code samples below if you do.

Code

Create a new file called file-server.toit, and start by adding the imports and constants:

import host.file
import host.directory
import http
import net

CSS ::= """
"""

For now we leave the CSS empty. We will add some content later.

We can now add the main function:

main:
  network := net.open
  server-socket := network.tcp-listen 0
  port := server-socket.local-address.port
  print "Listening on http://localhost:$port/"

  clients := []
  server := http.Server
  task::
    server.listen server-socket:: | request/http.RequestIncoming response-writer/http.ResponseWriter |
      // Note that we are not sanitizing the path here.  This is a security
      // risk, as it allows a client to access files outside of the current
      // directory.
      path := "./$request.path"
      if file.is-file path:
        serve-file request.path response-writer
      else if file.is-directory path:
        serve-directory request.path response-writer
      else:
        response-writer.write-headers http.STATUS-NOT-FOUND --message="Not Found"
      response-writer.close

The main function starts by establishing a network connection on a random port. It then creates a Server object and starts listening for incoming connections.

Depending on the path of the request, we either serve a file, a directory listing, or a 404 error.

The serve-file function is implemented as follows:

serve-file request-path/string writer/http.ResponseWriter:
  path := "./$request-path"
  // Serve the file as binary data.
  writer.headers.add "content_type" "application/octet-stream"
  writer.headers.add "content_length" "$(file.size path)"
  stream := file.Stream.for-read path
  try:
    writer.out.write-from stream.in
  finally:
    stream.close

It adds the correct headers and then pipes the file contents to the response writer.

The serve-directory function is slightly more involved since it needs to differentiate between files and directories:

serve-directory request-path/string writer/http.ResponseWriter:
  // List the directory contents.
  path := "./$request-path"
  stream := directory.DirectoryStream path
  writer.headers.add "content_type" "text/html"
  writer.out.write """
    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="UTF-8">
      <title>Directory Listing - $request-path</title>
      <style>
        $CSS
      </style>
    </head>
    <body>
      <h1>Directory Listing - $request-path</h1>
    """
  while entry := stream.next:
    entry-path := "$path/$entry"
    prefix := request-path.trim --right "/"
    entry-request-path := "$prefix/$entry"
    if file.is-directory entry-path:
      writer.out.write """
        <div class="directory">
          <a href="$entry-request-path">$entry/</a>
        </div>
      """
    else:
      writer.out.write """
        <div class="file">
          <a href="$entry-request-path" download>$entry</a>
        </div>
      """
  stream.close
  writer.out.write """
    </body>
    </html>
  """

Most of the function consists of HTML code. The key part is the while loop. For each entry in the directory we check if it is a file or a directory. We then generate the appropriate HTML.

CSS

There are many ways to render a directory listing. We have chosen to ask chat.openai.com for help. Our prompt was:

Create a CSS for a web page that will show a file system.
Users can click on files to download them, or click on directories to go into them.
Show an example HTML file that the server needs to generate to use this CSS.

This is what we got back:

body {
    font-family: Arial, sans-serif;
    margin: 0;
    padding: 0;
    background-color: #f5f5f5;
}

.container {
    width: 80%;
    margin: auto;
    padding: 20px;
    background-color: white;
    border-radius: 5px;
    margin-top: 10px;
}

.directory, .file {
    padding: 10px;
    border: 1px solid #ddd;
    border-radius: 5px;
    margin: 5px 0;
    cursor: pointer;
    transition: 0.3s;
}

.directory:hover, .file:hover {
    background-color: #e0e0e0;
}

.directory:before {
    content: "📁 ";
}

.file:before {
    content: "📄 ";
}

a {
    color: inherit;
    text-decoration: none;
}

For reference, the AI model suggested the following HTML page (Note: it was missing the charset meta tag):

<!DOCTYPE html>
<html>
<head>
    <link rel="stylesheet" type="text/css" href="your-stylesheet.css">
</head>
<body>
    <div class="container">
        <div class="directory">
            <a href="/path/to/directory1">Directory 1</a>
        </div>
        <div class="directory">
            <a href="/path/to/directory2">Directory 2</a>
        </div>
        <div class="file">
            <a href="/path/to/file1" download>File 1</a>
        </div>
        <div class="file">
            <a href="/path/to/file2" download>File 2</a>
        </div>
    </div>
</body>
</html>