// Responses that are an envelope rather than a body: a redirect, a download,
// a data URL, and a download that weighs the request first.
///|
/// A redirect to `url` (← FastAPI's `RedirectResponse`).
///
/// `307` keeps the method and body, which `302` historically does not. Use `303`
/// after a write, `301` / `308` for a permanent move.
///
/// The URL is escaped as a whole URI reference, so an encoded one is not encoded
/// twice and a `\r\n` cannot open a header of its own.
pub fn redirect(url : String, status~ : Int32 = 307) -> @moonasgi.Response {
@moonasgi.Response::new(
status,
[("location", @moonurl.escape_text(url[:], kind=Whole))],
b"",
)
}
///|
/// A download (← FastAPI's `FileResponse`): a media type from `filename`'s
/// extension unless `media_type` names one, `Content-Length`, and a
/// `Content-Disposition`. No `filename`, no disposition.
///
/// Bytes rather than a path: the same app runs on four backends and only the
/// server can read a file on any of them.
///
/// Always the whole thing. [`Context::serve`] weighs the request first.
pub fn file_response(
content : Bytes,
filename~ : String = "",
media_type~ : String = "",
status~ : Int32 = 200,
inline~ : Bool = false,
) -> @moonasgi.Response {
@moonasgi.Response::new(
status,
envelope(content.length(), filename, media_type, inline),
content,
)
}
///|
/// Serve what a `data:` URL carries (RFC 2397), under the media type it names —
/// what `canvas.toDataURL()` and `FileReader.readAsDataURL()` hand back.
///
/// `None` when it is not a data URL, so a handler tells "no picture" from "not a
/// picture" without touching base64.
pub fn data_response(
url : String,
filename~ : String = "",
status~ : Int32 = 200,
inline~ : Bool = true,
) -> @moonasgi.Response? {
try {
let url = @dataurl.Url::decode(url[:])
Some(
file_response(url.data, filename~, media_type=url.kind, status~, inline~),
)
} catch {
_ => None
}
}
///|
/// The content as a `data:` URL, for embedding it instead of costing a second
/// request.
pub fn data_url(content : Bytes, media_type~ : String = "") -> String {
let kind = if media_type != "" { media_type } else { @dataurl.kind }
@dataurl.Url::new(content, kind~).encode()
}
///|
/// What the body is, how long, and what to save it as.
pub fn envelope(
length : Int32,
filename : String,
media_type : String,
inline : Bool,
) -> Array[(String, String)] {
let kind = if media_type != "" {
media_type
} else {
charset(@moonmime.guess_type(filename[:]))
}
let headers : Array[(String, String)] = [
("content-type", kind),
("content-length", length.to_string()),
]
if filename != "" {
headers.push(
(
"content-disposition",
@moonmime.content_disposition(filename[:], inline~),
),
)
}
headers
}
///|
/// A text type with `charset=utf-8` added, as Starlette's `Response` adds it: a
/// browser handed `text/plain` with no charset applies its own locale's.
fn charset(kind : String) -> String {
if kind.has_prefix("text/") && !kind.to_lower().contains("charset=") {
kind + "; charset=utf-8"
} else {
kind
}
}