Rust · Files and Streams

Files and Streams

use ucall::FileRange;use ucall::PathRange;use ucall::ReplyStream;use ucall::*; 1.0.0-rc.1 · 60202332

Handlers run on the polling thread, so a reply or a pull that finishes later runs on a worker: Request::detach hands the request over as a DetachedRequest, which reads and replies like a Request and alone has the three verbs below.

VerbReturnsFinal when
reply_parts(status, headers, parts)None for bytes alone, else a Deferredevery part is sent
reply_stream(status, headers)a ReplyStream with append(parts) and end()end() returns None or a Deferred
body_chunk()the bytes decoded since the last pullan empty chunk once the body ended

§ Serving a File

use std::sync::mpsc;
use ucall::*;

let (handed, received) = mpsc::channel::<DetachedRequest>();
let interface = Interface::new("0.0.0.0:8080").reply_directories(["/srv/videos"]);
let mut server = Server::new(&[interface])?;
server.get("/videos/{name}", move |request: Request| handed.send(request.detach()).unwrap())?;

std::thread::scope(|scope| {
    scope.spawn(|| server.take_calls(0));
    for detached in received {
        let name = detached.path_parameter("name").unwrap_or_default().to_vec();
        let path = PathRange { directory: 0, path: name.as_slice().into(), offset: 0, length: 0 };
        let parts = [ReplyPart::Path(path)];
        let replied = detached.reply_parts(HTTPStatus::OK, &[], &parts);
        if let Ok(Some(deferred)) = replied {
            let _ = deferred.wait(); // or: deferred.await
        }
    }
});

reply_parts concatenates ReplyPart::Bytes, ReplyPart::Path and ReplyPart::File parts into one body whose Content-Length is measured from the files. ReplyPart::from turns a borrowed std::io::IoSlice into a bytes part, so vectored buffers from other I/O code map over directly. A Path resolves beneath the interface’s reply_directories, refusing absolute paths, .. and links leading out, and the event loop opens, reads and closes it unbuffered. A File borrows the caller’s descriptor, opened with O_DIRECT, for the Deferred’s lifetime, while bytes and paths are copied before the call returns, so they may be dropped right after it. A file that is missing, unreadable or too short replaces the reply with 404, 416 or 500 before its head leaves, and the Deferred reports the cause. Deferred::status polls without blocking, Deferred::wait blocks until the reply is final, .await polls the status between other tasks, since the library has no wake-up, and dropping a Deferred waits for it, which is why only a detached request makes one.

§ Streaming a Reply

fn send_rows(detached: DetachedRequest, rows: &[String]) -> Result<()> {
    let mut stream = detached.reply_stream(HTTPStatus::OK, &["content-type: text/csv".into()])?;
    for row in rows {
        stream.append(&[ReplyPart::Bytes(row.as_bytes().into())])?;
    }
    stream.end().map(|_| ())
}

The stream is chunked unless a Content-Length header declares the length; append refuses with Status::WouldBlock, queueing nothing, while the peer lags behind what is queued; end answers None or a Deferred like reply_parts, and dropping an unended stream ends it.

§ Streaming an Upload

fn store(mut detached: DetachedRequest) -> Result<()> {
    let mut received_bytes = 0usize;
    loop {
        let chunk = detached.body_chunk()?;
        if chunk.is_empty() {
            break;
        }
        received_bytes += chunk.len();
    }
    let total = received_bytes.to_string();
    detached.reply_parts(HTTPStatus::OK, &[], &[ReplyPart::Bytes(total.as_bytes().into())]).map(|_| ())
}

An endpoint registered through Server::route_with with BodyDelivery::Streamed runs its handler once the headers arrive; the handler hands request.detach() to a worker as above, and body_chunk blocks for the bytes decoded since the last pull, empty once the body ended. On the polling thread, where nothing arrives while the handler runs, a pull with nothing decoded refuses with Status::WouldBlock instead of waiting.

API

Types

DescriptorA file descriptor borrowed for 'f, laid out as the C int the library reads.
lib.rs:509 ↗
pub type Descriptor<'f> = BorrowedFd<'f>;
StructFileRangeucall_file_range_t: the caller's O_DIRECT descriptor, borrowed until the reply is final.
server.rs:1758 ↗
pub struct FileRange<'f>

Fields

fileDescriptor<'f>The open file, borrowed and never closed by UCall.
offsetu64The range's first byte.
lengthu64The range's byte count; zero sends through the end of the file.

Implements

Clone, Copy, Debug
StructPathRangeucall_path_range_t: a file beneath one of the interface's reply directories.
server.rs:1770 ↗
pub struct PathRange<'b>

Fields

directoryusizeThe position of the directory among the interface's reply directories.
pathBytesView<'b>The file beneath it, relative, copied before the call returns.
offsetu64The range's first byte.
lengthu64The range's byte count; zero sends through the end of the file.

Implements

Clone, Copy, Debug
StructReplyStreamA reply whose head left and whose body is appended part by part, from any one thread at a time, until ReplyStream::end; dropping it ends it, waiting for its file parts to send.2
server.rs:1939 ↗
pub struct ReplyStream<'f>

The descriptors its file parts borrow stay open for 'f, through the Deferred its end may hand back.

Implements

Send, Drop
appendQueues parts on the body together, or none of them.
server.rs:1953 ↗
pub fn append(&mut self, parts: &[ReplyPart<'_, 'f>]) -> Result<()>

Fails with Status::WouldBlock, queueing nothing, while the peer lags behind the parts already queued; retry once it catches up.

endEnds the body, consuming the request whatever the outcome; None once final, and a Deferred while file or path parts still send.
server.rs:1971 ↗
pub fn end(self) -> Result<Option<Deferred<'f>>>

A body short of its declared Content-Length fails with Status::Malformed and closes the connection after the bytes already sent.