Route: download

Verb-mode ?view=download. Lives in src/routes/download.rs and is invoked from Route: view (dispatch) when the query string says view=download. Dispatches per ctx.type; not all resource types are downloadable.

Permission gate

if let Err(resp) = guard::require(req, &resource_path, Role::Read).await {
    return resp;
}

Download requires Role::Read — the same gate as viewing the page. If you can see the resource, you can download it; if you cannot, guard::require returns the standard 403 forbidden response and the handler short-circuits. There is no separate “download” role.

Type dispatch table

After the permission check the handler reads ctx.json, pulls ctx.get("type"), and matches:

singlemd

let item = SingleMd::read(resource_path.clone());
let body = item.read_body(&lang);
let file_name = format!("{}.md", item.file_slug(&lang));
content::binary_response(body.into_bytes(), &file_name, "text/markdown; charset=utf-8", true)

Serves the raw markdown source as an attachment. Filename is <file_slug>.md, MIME is text/markdown; charset=utf-8. The active language comes from crate::lang_for(req), so a /zh/...?view=download request returns the Chinese .md slot.

mdbook

Bundles every chapter’s .md source into a single .zip. Steps:

  1. book.chapter_count() -> n. Empty book (n == 0) returns 404 No chapters to download.
  2. For each chapter, read the markdown body and build a filename of the form {:0width$}-<slug>.md where width = n.to_string().len().
  3. Hand the (name, bytes) pairs to crate::zip::archive_files_to_bytes.
  4. Wrap in content::binary_response with MIME application/zip and attachment=true.

The zero-padding is load-bearing: it ensures unzip preserves the book’s reading order via filename sort. A 12-chapter book produces 01-intro.md, 02-..., …, 12-conclusion.md; without padding, 10-... would sort before 2-....

The archive name is <folder_slug>.zip, where folder_slug is the last non-empty path segment of resource_path. If the path has none (e.g. /), the name defaults to "mdbook":

let folder_slug = resource_path
    .trim_matches('/')
    .rsplit('/')
    .next()
    .filter(|s| !s.is_empty())
    .unwrap_or("mdbook");

file

let item = FileContent::read(resource_path.clone());
let bytes = item.read_bytes(&lang);
let fname = item.file_name(&lang);
let mime = content::guess_mime(&fname);
content::binary_response(bytes, &fname, &mime, true)

Serves the raw bytes verbatim under the original filename stored in ctx.json’s file slot. MIME is inferred from the extension via content::guess_mime.

other

Anything else returns 400 Download not supported for type "...". Container types (board, list, board_group, board_list) are not downloadable — they aggregate children but have no canonical serialized form. link resources are not downloadable either; the URL is the artifact.

content::binary_response shape

The four-argument binary_response(bytes, filename, mime, attachment) is the universal byte-shipping helper. With attachment=true it sets Content-Disposition: attachment; filename="..."; with attachment=false it would inline-render. All three downloadable types pass true because the verb is explicitly “download.” (The inline-streaming path for FileContent with download_only=true lives in the default render branch, not here.)

Active-language selection

singlemd and mdbook honor crate::lang_for(req) to pick which .md slot to serve when the underlying file field is a LangDict. Reaching ?view=download from /zh/handbook/ downloads the Chinese chapters; reaching it from /handbook/ downloads the default-language ones. See Language system for the cookie/prefix resolution order.

file does the same with FileContent::read_bytes and FileContent::file_name. If a particular language slot is missing, the LangDict falls back per its own rules (default language entry) — the download handler doesn’t have to know.

Where this gets surfaced

The per-type editors render a “Download” button when their template gets can_download = true. That flag is set by content::render after probing Role::Read on the resource. The button is a plain <a href="?view=download"> — no JS, no fetch — so it works with hreflang prefix URLs the same way the page itself does.

The per-type page templates (singlemd.html, mdbook.html, file.html) include the button; container templates (board.html, list.html) do not, matching the dispatch table above.

Cross-links

For the per-type field schemas that drive what gets packaged: Type: singlemd, Type: mdbook, Type: file. For the verb-routing entry point: Route: view (dispatch).