///|
/// Serving a file properly: the four answers RFC 9110 §13 and §14 call for, and
/// the `data:` URL a browser hands back from a canvas or a `FileReader`.
///
/// `Context::serve` is `file_response` with the request taken into account. It
/// answers a repeat request with `304`, a failed precondition with `412`, a
/// `Range` with `206` — one part or several — and an impossible one with `416`.
/// Nothing here opens a file: the bytes arrive from wherever the server got
/// them, which is what keeps the same handler running on four backends.
///
/// moon run src/examples/24-files
#coverage.skip
async fn main {
let app = @moonapi.App::new()
let report = b"a quarterly report, ten times longer than it needs to be"
let tag = @conditional.hash(report[:], digest=@sha2.Hasher::new())
app.get("/report.txt", ctx => {
ctx.serve(report, filename="report.txt", etag=tag)
})
// A client posts back what `canvas.toDataURL()` gave it; the server serves
// what is inside, under the media type the URL itself names.
app.post("/thumbnail", ctx => {
match ctx.body_json() {
Some(String(url)) =>
match @moonapi.data_response(url, filename="thumb") {
Some(resp) => resp
None => @moonapi.text(400, "not a data URL")
}
_ => @moonapi.text(400, "expected a JSON string")
}
})
// 1. Nobody has it yet.
let first = ask(app, "GET", "/report.txt", [])
println(
"plain GET \{first.status} \{first.body.length()} octets",
)
let etag = first.header("etag").unwrap_or("")
println(" ETag \{etag}")
println(
" Accept-Ranges \{first.header("accept-ranges").unwrap_or("")}",
)
// 2. The same client comes back holding it.
let again = ask(app, "GET", "/report.txt", [("if-none-match", etag)])
println(
"GET with If-None-Match \{again.status} \{again.body.length()} octets",
)
// 3. A download that stopped halfway, resumed.
let rest = ask(app, "GET", "/report.txt", [
("range", "bytes=40-"),
("if-range", etag),
])
println(
"resumed with Range \{rest.status} \{rest.header("content-range").unwrap_or("")}",
)
println(" what came back \{text(rest.body)}")
// 4. Resumed against a copy that has since changed: the whole thing again,
// which is what §13.1.5 asks for rather than an error.
let moved = ask(app, "GET", "/report.txt", [
("range", "bytes=40-"),
("if-range", "\"something-else\""),
])
println(
"resumed, but it changed \{moved.status} \{moved.body.length()} octets",
)
// 5. Two ranges at once, and one that is not there.
let two = ask(app, "GET", "/report.txt", [("range", "bytes=0-1,54-55")])
println(
"two ranges \{two.status} \{two.header("content-type").unwrap_or("")}",
)
let past = ask(app, "GET", "/report.txt", [("range", "bytes=900-999")])
println(
"a range past the end \{past.status} \{past.header("content-range").unwrap_or("")}",
)
println("")
// 6. The data URL round trip.
let posted = "data:image/gif;base64,AAEC"
let served = ask_body(app, "POST", "/thumbnail", "\"\{posted}\"")
println(
"posted data URL \{served.status} \{served.header("content-type").unwrap_or("")}",
)
println(" octets \{served.body.length()}")
println(
" and back again \{@moonapi.data_url(served.body, media_type="image/gif")}",
)
}
///|
async fn ask(
app : @moonapi.App,
verb : String,
path : String,
headers : Array[(String, String)],
) -> @moonasgi.Response {
app.handle({ http_method: verb, path, query_string: b"", headers, body: b"", })
}
///|
async fn ask_body(
app : @moonapi.App,
verb : String,
path : String,
body : String,
) -> @moonasgi.Response {
app.handle({
http_method: verb,
path,
query_string: b"",
headers: [("content-type", "application/json")],
body: @utf8.encode(body[:]),
})
}
///|
fn text(body : Bytes) -> String {
let out = StringBuilder()
for i = 0; i < body.length(); i = i + 1 {
out.write_char(body[i].to_int().unsafe_to_char())
}
out.to_string()
}