mini-static 0.7.1

A secure, async static file server with streaming, traversal protection, and connection limits.
Documentation
use std::pin::Pin;
use std::task::{Context, Poll};

use bytes::{BufMut, Bytes, BytesMut};
use http_body::{Body, Frame};
use http_body_util::Full;
use tokio::fs::File;
use tokio::io::{AsyncRead, ReadBuf};

use crate::error::StaticError;
use crate::reload::SseBody;

/// Chunk size for streaming file reads — 64 KB per frame.
pub(crate) const FILE_CHUNK_SIZE: usize = 65_536;

/// The response body type used by mini-static.
///
/// This is a concrete sum type, not a boxed trait object: `Buffered` covers redirects,
/// errors, HEAD responses, and 304s; `Streamed` covers file `GET` responses; `Sse` covers
/// the live-reload event stream. Keeping it concrete (rather than erasing into `BoxBody`
/// here) lets an embedding crate — one that needs to remap the error type before erasing
/// into its own body type, e.g. `mini-unified` bridging into `mini-serve`'s
/// `ResponseBody` — erase exactly once at that boundary instead of erasing here and then
/// again there.
pub enum ResponseBody {
    /// A body already fully in memory.
    Buffered(Full<Bytes>),
    /// A body streamed from disk one chunk at a time.
    Streamed(FileBody),
    /// A live-reload SSE stream (see [`crate::Server::with_live_reload`]).
    Sse(SseBody),
}

impl Body for ResponseBody {
    type Data = Bytes;
    type Error = StaticError;

    fn poll_frame(
        self: Pin<&mut Self>,
        cx: &mut Context<'_>,
    ) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
        match self.get_mut() {
            ResponseBody::Buffered(body) => match Pin::new(body).poll_frame(cx) {
                Poll::Ready(Some(Ok(frame))) => Poll::Ready(Some(Ok(frame))),
                // `Full<Bytes>`'s error type is `Infallible` — this arm can never run.
                Poll::Ready(Some(Err(never))) => match never {},
                Poll::Ready(None) => Poll::Ready(None),
                Poll::Pending => Poll::Pending,
            },
            ResponseBody::Streamed(body) => Pin::new(body).poll_frame(cx),
            ResponseBody::Sse(body) => Pin::new(body).poll_frame(cx),
        }
    }
}

/// An `http_body::Body` that streams a `tokio::fs::File` to the client one chunk at a
/// time, instead of buffering the whole file before the response body is polled.
///
/// Each `poll_frame` call reads directly into `buf`'s spare (uninitialized) capacity via
/// `ReadBuf::uninit` and marks only the bytes the read syscall actually wrote as
/// initialized via `advance_mut` — there's no `resize`-driven zero-fill and no extra
/// copy: `split_to(n).freeze()` hands the just-filled bytes to the caller and leaves
/// `buf`'s already-reserved spare capacity in place for the next read.
pub struct FileBody {
    file: File,
    buf: BytesMut,
}

impl FileBody {
    pub(crate) fn new(file: File) -> Self {
        FileBody {
            file,
            buf: BytesMut::new(),
        }
    }
}

impl Body for FileBody {
    type Data = Bytes;
    type Error = StaticError;

    fn poll_frame(
        self: Pin<&mut Self>,
        cx: &mut Context<'_>,
    ) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
        let this = self.get_mut();

        if this.buf.capacity() - this.buf.len() < FILE_CHUNK_SIZE {
            this.buf.reserve(FILE_CHUNK_SIZE);
        }

        let mut read_buf = ReadBuf::uninit(this.buf.spare_capacity_mut());
        let file = Pin::new(&mut this.file);

        match file.poll_read(cx, &mut read_buf) {
            Poll::Ready(Ok(())) => {
                let n = read_buf.filled().len();
                if n == 0 {
                    return Poll::Ready(None);
                }
                // Safety: `poll_read` reported exactly `n` bytes filled into the spare
                // capacity we handed it via `ReadBuf::uninit`; advancing by that same
                // `n` only marks bytes the reader actually initialized.
                unsafe { this.buf.advance_mut(n) };
                let chunk = this.buf.split_to(n).freeze();
                Poll::Ready(Some(Ok(Frame::data(chunk))))
            }
            Poll::Ready(Err(e)) => Poll::Ready(Some(Err(StaticError::Io(e)))),
            Poll::Pending => Poll::Pending,
        }
    }
}