You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
There's no example (except maybe the SSE one) that has streaming responses. Which is probably because it's hard to come up with a good example for streaming responses.
I haven't found good documentation on how to "pipe" an AsyncWrite into a Stream<Item=Bytes(Mut)> (maybe something for actix-web-lab).
The proposed example is one that streams files from a directory (files) as a zip file, i.e. it dynamically creates the zip file.
For this I'm using async_zip which exposes an API that requires the user to pass in an AsyncWrite (ZipFileWriter::new).
To "pipe" the AsyncWrite to a Stream, I'm using a DuplexStream and the BytesCodec.
main.rs
use actix_web::{get, http,App,HttpResponse,HttpServer,Responder};use async_zip::write::{EntryOptions,ZipFileWriter};use futures::stream::TryStreamExt;use std::io;use tokio::io::AsyncWrite;use tokio_util::codec;#[get("/")]asyncfnindex() -> implResponder{let(to_write, to_read) = tokio::io::duplex(2048);
tokio::spawn(asyncmove{letmut zipper = async_zip::write::ZipFileWriter::new(to_write);ifletErr(e) = read_dir(&mut zipper).await{// TODO: do somethingeprintln!("Failed to write files from directory to zip: {e}")}ifletErr(e) = zipper.close().await{// TODO: do somethingeprintln!("Failed to close zipper: {e}")}});let stream = codec::FramedRead::new(to_read, codec::BytesCodec::new()).map_ok(|b| b.freeze());HttpResponse::Ok().append_header((
http::header::CONTENT_DISPOSITION,r#"attachment; filename="folder.zip""#,))// not sure if this is really necessary,// but we're already sending compressed data,// so make sure other middleware won't compress this again.append_header((http::header::CONTENT_ENCODING,"identity")).streaming(stream)}asyncfnread_dir<W>(zipper:&mutZipFileWriter<W>) -> Result<(), io::Error>whereW:AsyncWrite + Unpin,{letmut dir = tokio::fs::read_dir("files").await?;whileletOk(Some(entry)) = dir.next_entry().await{if !entry.metadata().await.map(|m| m.is_file()).unwrap_or(false){continue;}letmut file = match tokio::fs::OpenOptions::new().read(true).open(entry.path()).await{Ok(f) => f,Err(_) => continue,// we can't read the file};let filename = match entry.file_name().into_string(){Ok(s) => s,Err(_) => continue,// the file has a non UTF-8 name};letmut entry = zipper
.write_entry_stream(EntryOptions::new(filename, async_zip::Compression::Deflate)).await.map_err(zip_to_io_err)?;
tokio::io::copy(&mut file,&mut entry).await?;
entry.close().await.map_err(zip_to_io_err)?;}Ok(())}fnzip_to_io_err(e: async_zip::error::ZipError) -> io::Error{
io::Error::new(io::ErrorKind::Other, e)}#[actix_web::main]asyncfnmain() -> io::Result<()>{HttpServer::new(move || App::new().service(index)).bind(("127.0.0.1",8080))?
.run().await}
As I explained on Discord, using a DuplexStream is probably overkill, since it's supposed to be used bi-directional (see example in tokio docs), so I tried to extract the internal Pipe used by the tokio implementation and made a pipe specifically for (buffered) piping of AsyncWrite to a Stream<BytesMut>. I'm not sure if this should be included in actix-web-lab as a utility when dealing with AsyncWrite (or maybe in some other crate?).
async_pipe.rs
use std::{sync::{Arc,Mutex,MutexGuard}, task::{self,Waker,Poll}, pin::Pin};use bytes::{BytesMut,Buf};use futures::Stream;use tokio::io::AsyncWrite;/// The `AsyncWrite` half of an [`async_pipe`]pubstructAsyncPipeWriter(Arc<Mutex<Pipe>>);/// The `Stream` half of an [`async_pipe`]pubstructAsyncPipeReader(Arc<Mutex<Pipe>>);/// Creates buffered pipe that pipes writes from an `AsyncWrite` to a `Stream<Item=BytesMut>`./// /// `max_buf_size` is the maximum amount of bytes that can be written to the pipe's internal buffer/// before further writes return `Poll::Pending`.pubfnasync_pipe(max_buf_size:usize) -> (AsyncPipeWriter,AsyncPipeReader){let pipe = Arc::new(Mutex::new(Pipe::new(max_buf_size)));(AsyncPipeWriter(pipe.clone()),AsyncPipeReader(pipe))}/// A unidirectional IO over a piece of memory.////// Data can be written to the pipe, and reading will return that data./// /// [tokio's](https://github.com/tokio-rs/tokio/blob/de81985762a242c77361a6ab9de198372ca85987/tokio/src/io/util/mem.rs#L54-L76)/// internal representation of a pipe for a `tokio::io::DuplexStream`.#[derive(Debug)]structPipe{/// The buffer storing the bytes written, also read from.////// Using a `BytesMut` because it has efficient `Buf` and `BufMut`/// functionality already.buffer:BytesMut,/// Determines if the write side has been closed.is_closed:bool,/// The maximum amount of bytes that can be written before returning/// `Poll::Pending`.max_buf_size:usize,/// If the `read` side has been polled and is pending, this is the waker/// for that parked task.read_waker:Option<Waker>,/// If the `write` side has filled the `max_buf_size` and returned/// `Poll::Pending`, this is the waker for that parked task.write_waker:Option<Waker>,}implPipe{fnnew(max_buf_size:usize) -> Self{Pipe{buffer:BytesMut::new(),is_closed:false,
max_buf_size,read_waker:None,write_waker:None,}}fnclose_write(&mutself){self.is_closed = true;// needs to notify any readers that no more data will comeifletSome(waker) = self.read_waker.take(){
waker.wake();}}fnclose_read(&mutself){self.is_closed = true;// needs to notify any writers that they have to abortifletSome(waker) = self.write_waker.take(){
waker.wake();}}fnpoll_read_internal(mutself:Pin<&mutSelf>,cx:&mut task::Context<'_>) -> Poll<Option<BytesMut>>{ifself.buffer.has_remaining(){let bytes = std::mem::take(&mutself.buffer);ifletSome(waker) = self.write_waker.take(){
waker.wake();}Poll::Ready(Some(bytes))}elseifself.is_closed{Poll::Ready(None)}else{self.read_waker = Some(cx.waker().clone());Poll::Pending}}fnpoll_write_internal(mutself:Pin<&mutSelf>,cx:&mut task::Context<'_>,buf:&[u8],) -> Poll<std::io::Result<usize>>{ifself.is_closed{returnPoll::Ready(Err(std::io::ErrorKind::BrokenPipe.into()));}let avail = self.max_buf_size - self.buffer.len();if avail == 0{self.write_waker = Some(cx.waker().clone());returnPoll::Pending;}let len = buf.len().min(avail);self.buffer.extend_from_slice(&buf[..len]);ifletSome(waker) = self.read_waker.take(){
waker.wake();}Poll::Ready(Ok(len))}}implAsyncWriteforPipe{fnpoll_write(self:Pin<&mutSelf>,cx:&mut task::Context<'_>,buf:&[u8],) -> Poll<std::io::Result<usize>>{self.poll_write_internal(cx, buf)}fnpoll_flush(self:Pin<&mutSelf>, _:&mut task::Context<'_>) -> Poll<std::io::Result<()>>{Poll::Ready(Ok(()))}fnpoll_shutdown(mutself:Pin<&mutSelf>,
_:&mut task::Context<'_>,) -> Poll<std::io::Result<()>>{self.close_write();Poll::Ready(Ok(()))}}implAsyncWriteforAsyncPipeWriter{fnpoll_write(self:Pin<&mutSelf>,cx:&mut task::Context<'_>,buf:&[u8],) -> Poll<std::io::Result<usize>>{Pin::new(&mut*always_lock(&self.0)).poll_write(cx, buf)}fnpoll_flush(self:Pin<&mutSelf>,cx:&mut task::Context<'_>,) -> Poll<std::io::Result<()>>{Pin::new(&mut*always_lock(&self.0)).poll_flush(cx)}fnpoll_shutdown(self:Pin<&mutSelf>,cx:&mut task::Context<'_>,) -> Poll<std::io::Result<()>>{Pin::new(&mut*always_lock(&self.0)).poll_shutdown(cx)}}implStreamforPipe{typeItem = BytesMut;fnpoll_next(self:Pin<&mutSelf>,cx:&mut task::Context<'_>) -> Poll<Option<Self::Item>>{self.poll_read_internal(cx)}fnsize_hint(&self) -> (usize,Option<usize>){let remaining = self.buffer.remaining();(remaining,Some(remaining))}}implStreamforAsyncPipeReader{typeItem = BytesMut;fnpoll_next(self:Pin<&mutSelf>,cx:&mut task::Context<'_>) -> Poll<Option<Self::Item>>{Pin::new(&mut*always_lock(&self.0)).poll_next(cx)}fnsize_hint(&self) -> (usize,Option<usize>){always_lock(&self.0).size_hint()}}implDropforAsyncPipeWriter{fndrop(&mutself){// notify the other side of the closurealways_lock(&self.0).close_write();}}implDropforAsyncPipeReader{fndrop(&mutself){// notify the other side of the closurealways_lock(&self.0).close_read();}}#[inline]fnalways_lock<T>(mtx:&Mutex<T>) -> MutexGuard<T>{match mtx.lock(){Ok(g) => g,Err(e) => e.into_inner(),}}
index handler with async_pipe
#[get("/")]asyncfnindex() -> implResponder{let(to_write, stream) = async_pipe(2048);
tokio::spawn(asyncmove{letmut zipper = async_zip::write::ZipFileWriter::new(to_write);ifletErr(e) = read_dir(&mut zipper).await{// TODO: do somethingeprintln!("Failed to write files from directory to zip: {e}")}ifletErr(e) = zipper.close().await{// TODO: do somethingeprintln!("Failed to close zipper: {e}")}});HttpResponse::Ok().append_header((
http::header::CONTENT_DISPOSITION,r#"attachment; filename="folder.zip""#,))// not sure if this is really necessary,// but we're already sending compressed data,// so make sure other middleware won't compress this again.append_header((http::header::CONTENT_ENCODING,"identity")).streaming(stream.map(|b| Ok::<_, io::Error>(b.freeze())))}
cargo.toml
[package]
name = "actix-zippy"version = "0.1.0"edition = "2021"
[dependencies]
actix-web = "4.1.0"async_zip = "0.0.8"bytes = "1.2.1"futures = "0.3.23"tokio = { version = "1.20.1", features = ["io-util", "fs"] }
tokio-stream = "0.1.9"tokio-util = { version = "0.7.3", features = ["codec"] }
The text was updated successfully, but these errors were encountered:
I think these might be two issues in one:
AsyncWrite
into aStream<Item=Bytes(Mut)>
(maybe something for actix-web-lab).Note: Also see discussion on actix-web Discord: https://discord.com/channels/771444961383153695/771447545154371646/1009110232473014383
The proposed example is one that streams files from a directory (
files
) as a zip file, i.e. it dynamically creates the zip file.For this I'm using
async_zip
which exposes an API that requires the user to pass in anAsyncWrite
(ZipFileWriter::new
).To "pipe" the
AsyncWrite
to aStream
, I'm using aDuplexStream
and theBytesCodec
.main.rs
As I explained on Discord, using a
DuplexStream
is probably overkill, since it's supposed to be used bi-directional (see example in tokio docs), so I tried to extract the internalPipe
used by the tokio implementation and made a pipe specifically for (buffered) piping ofAsyncWrite
to aStream<BytesMut>
. I'm not sure if this should be included inactix-web-lab
as a utility when dealing withAsyncWrite
(or maybe in some other crate?).async_pipe.rs
index handler with async_pipe
cargo.toml
The text was updated successfully, but these errors were encountered: