← Документация · nv-lang/nova-polaris

Отдача статических файлов

polaris.static отдаёт файлы из источника ReadFsEmbeddedDir (байты, запечённые в бинарник, детерминированно, обычный production-выбор) или DirFs (живое чтение с диска под эффектом Fs, полезно для dev live-reload). Семантика следует http.FileServer/ serveContent из Go в тонких местах ETag/Range, и ServeDir из tower-http — по общей форме.

Исходник: src/static.nv.


Содержание


Отдача встроенных ассетов

fn static_fixture() -> EmbeddedDir =>
    EmbeddedDir.new([
        EmbeddedEntry { path: "index.html", data: "<h1>hi</h1>".bytes() },
        EmbeddedEntry { path: "notes/readme.txt", data: "hello world".bytes() },
    ])

test "static-files: serve embedded assets — mime, ETag, index resolution, 404" {
    mut r = Router.new()
    // static_handler(fs, cfg, param):
    //   fs    — the file source (EmbeddedDir here; DirFs for live-reload dev)
    //   cfg   — Static config; Static.new() = defaults (index.html, no Cache-Control)
    //   param — the {*...} route-parameter NAME the handler reads the path from:
    //           the route says {*path}, so the literal "path" is passed here
    r.get("/assets/{*path}", static_handler(static_fixture(), Static.new(), "path"))!!

    ro txt = route_once(r, get_req("/assets/notes/readme.txt"))
    assert(txt.status_code() == 200)
    assert(hdr(txt, "content-type") == "text/plain; charset=utf-8")
    assert(hdr(txt, "etag") != "")

    ro missing = route_once(r, get_req("/assets/nope.txt"))
    assert(missing.status_code() == 404)

    // empty {*path} can't come through the router (no request maps to it) —
    // call serve_path directly to serve the index file, same as static_handler does.
    mut r2 = Router.new()
    r2.get("/", fn(req ServerRequest) -> ServerResponse =>
        serve_path(static_fixture(), Static.new(), "", req))!!
    ro idx = route_once(r2, get_req("/"))
    assert(hdr(idx, "content-type") == "text/html; charset=utf-8")
}

EmbeddedDir/EmbeddedEntry — типы прелюдии (embed_dir("dir") во время сборки — обычный способ заполнить их из реальной директории — см. std/src/prelude/embed.nv); записи должны быть отсортированы по пути. static_handler(fs, cfg, param) одним вызовом строит готовый Handler для route-catch-all {*param}; serve_path(fs, cfg, path, req) — низкоуровневая функция, которую он оборачивает, для ручного подключения статики (например под /, где нет доступного захвата {*path}, как показывает случай с индексом выше).

Static.new() по умолчанию — index.html для ""/путей с завершающим / и без заголовка Cache-Control; @index(name)/@cache_control(v) переопределяют то или другое.

Что реализовано

Практическое подмножество serveContent из Go, адаптированное под ReadFs, который не несёт mtime:

ВозможностьПоведение
ETag от содержимогосильный тег, "<len-hex>-<crc32-hex>" — идентичные байты получают идентичный тег на любом хосте, в отличие от mtime-based
If-None-Match304, поддержаны список/префикс W//* (слабое сравнение — нормально для content-ETag)
Range (одиночный)206 с Content-Range; If-Range защищает от устаревшего валидатора (полный 200 при несовпадении)
Неудовлетворимый range416 + Content-Range: bytes */<size>
Битый/multi-rangeигнорируется → полный 200 (собственное правило Go)
MIMEпо расширению, небольшая встроенная таблица (html/css/js/json/svg/картинки/шрифты/wasm/pdf/xml/mp4/…, иначе application/octet-stream)
Разрешение индекса""/завершающий /<cfg.index>

Не реализовано: If-Modified-Since/Last-Modified (нет mtime для сравнения — ETag его заменяет), multi-range/multipart-ответы (Go их отдаёт; редкий случай, отложено), автоматический ответ на HEAD (маршрутизация методов — дело Router’а, регистрируйте HEAD явно, если нужно).

Безопасность

Попытка ..-побега никогда не доходит до пользовательского кода: DirFs отклоняет её внутри себя (проверка границ внутри абстракции файловой системы), а EmbeddedDir — это map с точными ключами, где ключ, содержащий ../, просто не существует — оба случая приходят к обычному 404, неотличимому от любого другого отсутствующего пути (никакой утечки информации о причине отказа).

Связанные документы

Полный пример: examples/06-static-site — встроенные файлы, index-фолбэк, Cache-Control, реально запущенные.