///|
pub(all) struct StaticAssetMeta {
asset_type : String?
etag : String?
mtime : Int64?
path : String?
size : Int64?
encoding : String?
}
///|
pub fn StaticAssetMeta::new(
asset_type? : String,
etag? : String,
mtime? : Int64,
path? : String,
size? : Int64,
encoding? : String,
) -> Self {
{ asset_type, etag, mtime, path, size, encoding }
}
///|
pub(open) trait ServeStaticProvider {
// This function should resolve asset meta
fn get_meta(Self, id : StringView) -> StaticAssetMeta?
// This function should resolve asset content
fn get_contents(Self, id : StringView) -> &Responder
// Custom MIME type resolver function
fn get_type(Self, ext : String) -> String?
// Encodings map
fn get_encodings(Self) -> Map[String, String]
// Index names
fn get_index_names(Self) -> Array[String]
// Fallthrough
fn get_fallthrough(Self) -> Bool
}
///|
test "normalize_path" {
inspect(@posix.Path::normalize("/foo/bar"), content="/foo/bar")
inspect(@posix.Path::normalize("/foo/../bar"), content="/bar")
inspect(@posix.Path::normalize("/foo/./bar"), content="/foo/bar")
inspect(@posix.Path::normalize("/foo/../../bar"), content="/bar")
inspect(@posix.Path::normalize("/foo/./../bar"), content="/bar")
inspect(@posix.Path::normalize("/foo/.././bar"), content="/bar")
inspect(@posix.Path::normalize("/foo/./.././bar"), content="/bar")
inspect(@posix.Path::normalize("/foo/.././../bar"), content="/bar")
inspect(@posix.Path::normalize("/foo/./.././../bar"), content="/bar")
inspect(@posix.Path::normalize("/../../bar"), content="/bar")
inspect(@posix.Path::normalize("/.."), content="/")
inspect(@posix.Path::normalize("/foo/bar/.."), content="/foo")
inspect(@posix.Path::normalize("/foo/bar/../."), content="/foo")
inspect(@posix.Path::normalize("/"), content="/")
inspect(@posix.Path::normalize("foo/bar"), content="foo/bar")
inspect(@posix.Path::normalize("/foo//bar"), content="/foo/bar")
}
///|
pub fn Mocket::static_assets(
self : Mocket,
path : String,
provider : &ServeStaticProvider,
) -> Unit {
self.use_middleware(async fn(event, next) noraise {
if !(match_path(path, event.req.url) is None) {
return next()
}
// Method check
if event.req.http_method != "GET" && event.req.http_method != "HEAD" {
if provider.get_fallthrough() {
return next()
}
event.res.headers.set("Allow", "GET, HEAD")
return HttpResponse::new(MethodNotAllowed)
}
let raw_id = event.req.url[path.length():]
let original_id = @posix.Path::normalize(raw_id.to_owned()).to_string()
// Parse Accept-Encoding
// Headers are Map[StringView, StringView]
let accept_encoding = event.req.headers.get("Accept-Encoding").unwrap_or("")
let encodings = provider.get_encodings()
let matched_encodings = []
if accept_encoding != "" {
// split requires `chars` label
for pair in accept_encoding.split(",") {
let encoding = pair.trim(chars=" ").to_owned()
match encodings.get(encoding) {
Some(mapped) => matched_encodings.push(mapped)
None => ()
}
}
}
if matched_encodings.length() > 1 {
event.res.headers.set("Vary", "Accept-Encoding")
}
// Search paths
let mut id = original_id
let mut meta : StaticAssetMeta? = None
let index_names = provider.get_index_names()
if index_names.length() == 0 {
ignore(index_names.push("/index.html"))
}
// Search logic: suffix -> encoding
let mut found = false
let suffixes = [""]
suffixes.append(index_names)
let try_encodings = matched_encodings.copy()
try_encodings.push("") // Add empty encoding (identity)
for suffix in suffixes {
if found {
break
}
for encoding in try_encodings {
let try_id = id + suffix + encoding
match provider.get_meta(try_id) {
Some(m) => {
meta = Some(m)
id = try_id
found = true
break
}
None => ()
}
}
}
match meta {
None => {
if provider.get_fallthrough() {
return next()
}
return HttpResponse::new(NotFound)
}
Some(meta) => {
// Handle caching
match meta.mtime {
Some(_mtime) =>
// TODO: Date parsing/comparison is tricky without a library.
// For now, we just set Last-Modified.
// event.res.headers.set("Last-Modified", ... )
()
None => ()
}
match meta.etag {
Some(etag) => {
if !event.res.headers.contains("ETag") {
event.res.headers.set("ETag", etag)
}
if event.req.headers.get("If-None-Match") == Some(etag) {
return HttpResponse::new(NotModified)
}
}
None => ()
}
// Content-Type
if !event.res.headers.contains("Content-Type") {
match meta.asset_type {
Some(t) => event.res.headers.set("Content-Type", t)
None => {
// Simple extension extraction
let parts = id.split(".").collect()
if parts.length() > 1 {
match provider.get_type(parts[parts.length() - 1].to_owned()) {
Some(t) => event.res.headers.set("Content-Type", t)
None => ()
}
}
}
}
}
// Content-Encoding
match meta.encoding {
Some(enc) =>
if !event.res.headers.contains("Content-Encoding") {
event.res.headers.set("Content-Encoding", enc)
}
None => ()
}
// Content-Length
match meta.size {
Some(size) =>
if size > 0L && !event.res.headers.contains("Content-Length") {
event.res.headers.set("Content-Length", size.to_string())
}
None => ()
}
if event.req.http_method == "HEAD" {
return HttpResponse::new(OK)
}
let contents = provider.get_contents(id)
event.res.status_code = OK
contents
}
}
})
}