guzba/mummy

Improve support for static files

Opened this issue · 4 comments

Mummy is not built to be the ideal static file server, however that doesn't mean it shouldn't do a better job making this easy than it does right now.

i use this code and perfoms well as generic static file server.

import std/os, std/paths, std/strutils, std/tables, std/locks
import mummy, mummy/routers
import webby

const pathFileServer:string = "./static"
let aPathFileServer = absolutePath(pathFileServer.Path, root = paths.getCurrentDir()).string

var gFilecache = initTable[string, string]()
var gFilecacheLock: Lock
initLock(gFilecacheLock)

proc rePathfromUri(uri: string): seq[string] {.gcSafe.} =   
  result = @["",""]
  {.cast(gcsafe).}:
    result[0] = aPathFileServer
  let spath = uri.split("/")
  let path = spath[2..^1]
  let file0 = path[^1]
  result[1] = file0.split(".")[^1]  # file extension
  for part in path:
    result[0].add '/'
    result[0].add part

var lheaders {.threadvar.}: HttpHeaders

proc hFileserver(request: Request) =
  let apath = rePathfromUri(request.uri)
  {.cast(gcsafe).}:
    lheaders = default(HttpHeaders)
    if gFileCache.hasKey(apath[0]):
      let cnt: string = gFilecache[apath[0]]
      lheaders["Content-Type"] = "text/" & apath[1]
      request.respond(200, lheaders, cnt)
      return
  if not fileExists(apath[0]):
    request.respond(404, lheaders)
    return
  let pf = getFilePermissions(apath[0])
  if not pf.contains(fpOthersRead):
    request.respond(403, lheaders)
    return
  acquire gFilecacheLock
  let fcontent = readFile(apath[0])
  lheaders["Content-Type"] = "text/" & apath[1]
  request.respond(200, lheaders, fcontent)
  {.cast(gcsafe).}:
    gFilecache[apath[0]] = fcontent
  release gFilecacheLock


var router: Router
router.get("/static/**", hFileserver)

let server = newServer(router)
echo "Serving on http://localhost:8080"
server.serve(Port(8080))

Not sure about wildcards but I've just been staticReading stuff into a Table. I have no idea if that's a terrible idea but for my use-case (tiny website with virtually no traffic) it's worked very well.

import mummy, mummy/routers
import macros

type 
  ResourceTable* = Table[string, string]

proc embed*(directory: string): ResourceTable =
  var pages: ResourceTable
  for fd in walkDir(directory, true):
    if fd.kind == pcFile:
      var p = fd.path.replace("\\", "/")
      pages[p] = staticRead(directory & "/" & p)
  pages


const assets = embed("assets")

macro validatePath*(params: PathParams, key: static[string]): untyped =
  ## Validates a key exists in the Request.pathParams
  ## and creates the key as a let ident that can be 
  ## used by the calling proc/macro.
  #
  result = newStmtList()
  result.add quote do:
    if not(`params`.contains(`key`)):
      echo `key` & " does not exist in pathParams..."
      request.respond(404)
      return

  # Create ident out of key and 
  # inject it into the ast to 
  # make the key available as 
  # a let identifier.
  var keyToIdent = ident("dataKey")
  result.add quote do:
    let `keyToIdent` = `params`[`key`]

macro validateParam*(html: ResourceTable, pathParam: static[string]): untyped =
  ## Validates a Request.pathParam key exists, 
  ## and creates a `data` let ident that
  ## holds the data, from the ResourceTable,
  ## related to the key obtained from the
  ## Request.pathParams param key passed
  ## to this macro.
  ## 
  ## Example:
  ## 
  ##  # Assume the passed route param is named
  ##  # "routeParam" and the passed param is 
  ##  # "someTxtFile.txt" which is a key in our 
  ##  # ResourceTable containing the text 
  ##  # "hello world"
  ##  proc assetHandler(request: Request) =
  ##    someResourceTable.validate("routeParam")
  ##    assert dataKey == "someTxtFile.txt"
  ##    assert data == "hello world"
  ## 
  #
  result = newStmtList()
  result.add quote do:
    request.pathParams.validatePath(`pathParam`)
    if not(`html`.contains(dataKey)):
      echo dataKey & " does not exist in ResourceTable..."
      request.respond(404)
      return

proc assetHandler(request: Request) =
  ## Serve assets like images from the /assets/@assetName
  ## route.
  ## 
  #
  assets.validateParam("assetName")

  # Our param key is validated, the `data` is brought
  # in and `dataKey` is set. The `validateParam` macro
  # would have served a 404 if the /assets/@assetName
  # provided in the request did not resolve to an 
  # existing asset in our `assets` ResourceTable.
  var headers: HttpHeaders
  var fileEnding = dataKey.split(".")[^1]
  case fileEnding:
  of "png":
    headers["Content-Type"] = "image/png"
  else:
    echo "Unknown file ending " & fileEnding
    request.respond(404)
    return

  request.respond(200, headers, data)

var router: Router
router.get("/assets/@assetName", assetHandler)
let server = newServer(router)
echo "Serving on http://127.0.0.1:8080"
server.serve(Port(2231), "127.0.0.1")

Not sure about wildcards but I've just been staticReading stuff into a Table...

When this can work I think it is a great approach actually.

It would not work for sites with lots of static assets or where they want to see changes when refreshing a page during development etc, but if those are not a problem it is great simple and safe approach imo.

Have a look at my router, which handles also static files:
https://github.com/enthus1ast/nimMatchingMummyRouter