actix-web-lab 0.18.9

In-progress extractors and middleware for Actix Web
Documentation
use std::{io, time::Duration};

use actix_web::{
    get,
    http::{
        self,
        header::{ContentEncoding, ContentType},
    },
    App, HttpResponse, HttpServer, Responder,
};
use actix_web_lab::body;
use async_zip::{write::ZipFileWriter, ZipEntryBuilder};
use tokio::{
    fs,
    io::{AsyncWrite, AsyncWriteExt as _},
};

fn zip_to_io_err(err: async_zip::error::ZipError) -> io::Error {
    io::Error::new(io::ErrorKind::Other, err)
}

async fn read_dir<W>(zipper: &mut ZipFileWriter<W>) -> io::Result<()>
where
    W: AsyncWrite + Unpin,
{
    let mut path = fs::canonicalize(env!("CARGO_MANIFEST_DIR")).await?;
    path.push("examples");
    path.push("assets");

    tracing::info!("zipping {}", path.display());

    let mut dir = fs::read_dir(path).await?;

    while let Ok(Some(entry)) = dir.next_entry().await {
        if !entry.metadata().await.map(|m| m.is_file()).unwrap_or(false) {
            continue;
        }

        let mut file = match tokio::fs::OpenOptions::new()
            .read(true)
            .open(entry.path())
            .await
        {
            Ok(file) => file,
            Err(_) => continue, // we can't read the file
        };

        let filename = match entry.file_name().into_string() {
            Ok(filename) => filename,
            Err(_) => continue, // the file has a non UTF-8 name
        };

        let mut entry = zipper
            .write_entry_stream(ZipEntryBuilder::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(())
}

#[get("/")]
async fn index() -> impl Responder {
    let (wrt, body) = body::writer();

    // allow response to be started while this is processing
    #[allow(clippy::let_underscore_future)]
    let _ = tokio::spawn(async move {
        let mut zipper = async_zip::write::ZipFileWriter::new(wrt);

        if let Err(err) = read_dir(&mut zipper).await {
            tracing::warn!("Failed to write files from directory to zip: {err}")
        }

        if let Err(err) = zipper.close().await {
            tracing::warn!("Failed to close zipper: {err}")
        }
    });

    HttpResponse::Ok()
        .append_header((
            http::header::CONTENT_DISPOSITION,
            r#"attachment; filename="folder.zip""#,
        ))
        .append_header(ContentEncoding::Identity)
        .append_header((http::header::CONTENT_TYPE, "application/zip"))
        .body(body)
}

#[get("/plain")]
async fn plaintext() -> impl Responder {
    let (mut wrt, body) = body::writer();

    // allow response to be started while this is processing
    #[allow(clippy::let_underscore_future)]
    let _ = tokio::spawn(async move {
        wrt.write_all(b"saying hello in\n").await?;

        wrt.write_all(b"3\n").await?;
        tokio::time::sleep(Duration::from_secs(1)).await;

        wrt.write_all(b"2\n").await?;
        tokio::time::sleep(Duration::from_secs(1)).await;

        wrt.write_all(b"1\n").await?;
        tokio::time::sleep(Duration::from_secs(1)).await;

        wrt.write_all(b"hello world\n").await
    });

    HttpResponse::Ok()
        .append_header(ContentType::plaintext())
        .body(body)
}

#[actix_web::main]
async fn main() -> io::Result<()> {
    env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));

    tracing::info!("staring server at http://localhost:8080");

    HttpServer::new(|| App::new().service(index).service(plaintext))
        .workers(2)
        .bind(("127.0.0.1", 8080))?
        .run()
        .await
}