rawhat / mist

gleam HTTP server. because it glistens on a web
Apache License 2.0
260 stars 11 forks source link

Directing "/" to load "index.html" #30

Closed rockerBOO closed 7 months ago

rockerBOO commented 7 months ago

I was looking to have / load up 'index.html but not sure how to achieve it without going into the internals. Also I am sort of new to gleam so I couldn't get this to work properly just yet.

Here is my attempt at trying to get "/" to load "index.html" in the serve_file from the README

import mist.{Connection, ResponseData}
import mist/internal/file
import gleam/http/request.{type Request}
import gleam/http/response.{type Response}

// Having to move over internals to get access to this function

fn convert_file_errors(err: file.FileError) -> FileError {
  case err {
    file.IsDir -> IsDir
    file.NoAccess -> NoAccess
    file.NoEntry -> NoEntry
    file.UnknownFileError -> UnknownFileError
  }
}

fn serve_file(
  _req: Request(Connection),
  path: List(String),
) -> Response(ResponseData) {
  let file_path = string.join(path, "/")
  // Omitting validation for brevity
  mist.send_file(file_path, offset: 0, limit: None)
  |> result.map(fn(file) {
    case file_path {
      // Loading "/"
      "" -> {
        let content_type = "text/html"
        // Load up "index.html"
        let file_resp =
          "index.html"
          |> bit_array.from_string
          |> file.stat
          |> result.map_error(convert_file_errors)
          |> result.map(fn(stat) {
            mist.File(
              descriptor: stat.descriptor,
              offset: 0,
              length: option.unwrap(None, stat.file_size),
            )
          })

        // Ideally here we return 404 if the file isn't found otherwise 200 with the index.html file.
        response.new(200)
        |> response.prepend_header("content-type", content_type)
        |> response.set_body(
          file_resp
          |> result.or(mist.Bytes(bytes_builder.new())), // Return 404 response
        )
      }
      _ -> {
        let content_type = guess_content_type(file_path)
        response.new(200)
        |> response.prepend_header("content-type", content_type)
        |> response.set_body(file)
      }
    }
  })
  |> result.lazy_unwrap(fn() {
    response.new(404)
    |> response.set_body(mist.Bytes(bytes_builder.new()))
  })
}

Additionally I thought of doing it the gleam way of defining the pattern but couldn't get it working (not sure if it works in gleam?).

fn serve_file(
  _req: Request(Connection),
  [""]: List(String),
) -> Response(ResponseData)   { ...

Thank you for this library!

rawhat commented 7 months ago

I think you should be able to do this without any of the internals.

For an example, say you have your index.html and other static assets in priv/ (this is relatively standard convention). You can get this with erlang.priv_directory. You can use this to reference your file path:

// don't do this in your request handler
let assert Ok(priv) = erlang.priv_directory("your_app_here")
case request.path_segments(req) {
  // I think this should cover the cases for this?
  [], ["/index.html"] -> {
    let index = string.join(priv, "/index.html")
    mist.send_file(index, offset: 0, limit: None)
    |> result.map(...)
    |> result.lazy_unwrap(...)
  }
  // other segments
}

The result.map value is the File which you can just pass directly to response.set_body. I guess if you need some data from the file, you might need some other bits from that type. But at least for getting the content type, I don't think it's required.

If the file isn't found (or one of the other errors), the result.lazy_unwrap case will have the errors. You can use that similarly to the example in the README to return a 404.

Having typed this up, I'm not sure if it was particularly helpful. The mist.send_file method should handle all of the stuff you need without having to reach into the internals.

I think maybe the confusion was around detecting the index path, particularly where to handle that? You'd likely want to do that up front in your "routing" case statement (as in that example). So you'd have something like:

case request.path_segments(req) {
  // this needs to be special cased, since it's different from your normal "static asset" path
  [], ["/index.html"] -> serve_index_file(req)
  // this could be used to just pass through `file_path` to `mist.send_file` for CSS, JS, HTML, etc
  ["static", ..file_path] -> serve_static_file(file_path)
  // this is just a dumb example for showing more routing stuff
  ["api", ..api_path] -> api_router(req, api_path)
}

Let me know if you have any other questions, or if I misunderstood what you were trying to achieve here!

rockerBOO commented 7 months ago

Yes, that works great! Thank you! I was trying to do too much in my serve_file and forgot about the router above. This works a lot better and clearer. Your other examples are great ideas as well.

This is what I ended up with but still needs some more work.

import gleam/erlang
import filepath

pub fn main() {
  let assert Ok(priv) = erlang.priv_directory("my_app_name")

  let assert Ok(_) =
    fn(req: Request(Connection)) -> Response(ResponseData) {
      case request.path_segments(req) {
        ["index.html"] -> serve_index_file(req, priv)
        [] -> serve_index_file(req, priv)
        _ -> "..."
      }
    }
    |> mist.new
    |> mist.port(3030)
    |> mist.start_http

  process.sleep_forever()
}       

fn serve_index_file(_req: Request(Connection), priv: String) -> Response(ResponseData) {
  let index = filepath.join(priv, "index.html")
  mist.send_file(index, offset: 0, limit: None)
  |> result.map(fn(file) {
    response.new(200)
    |> response.prepend_header("content-type", "text/html")
    |> response.set_body(file)
  })
  |> result.lazy_unwrap(fn() {
    response.new(404)
    |> response.set_body(mist.Bytes(bytes_builder.new()))
  })
}

Thank you again!