Search code examples
rustrust-axum

How to create file in server and download locally when pushing a button?


I have zero experience with web development. This sounds like a reasonably simple thing to implement, but I guess I don't know the correct keywords to use when searching online because I haven't gotten very far.

I want to build a website such that whenever I go to 127.0.0.1:8080/some_number it will show 2 buttons that say Download csv and Download txt.

When I press a button (let's assume Download csv) it should do the following:

  1. Check if the file <some_number>.csv already exists in the server.
  2. If it does, then just download it.
  3. If it doesn't exist, then run an external command that creates the file in the server (let's say touch <some_number>.csv) and then download the file.

So far I have managed to check if the files exist or not:

use axum::extract::Path;
use axum::{routing::get, Router};

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/", get(|| async { "Hello, World!" }))
        .route("/:run_number", get(check_files));

    let listener = tokio::net::TcpListener::bind("127.0.0.1:8080")
        .await
        .unwrap();
    axum::serve(listener, app).await.unwrap();
}

async fn check_files(Path(run_number): Path<u32>) -> String {
    let mut response = String::new();

    let csv_file = format!("{run_number}.csv");
    let csv_file = std::path::Path::new(&csv_file);
    if csv_file.exists() {
        response.push_str(&format!("File `{}` does exist\n", csv_file.display()));
    } else {
        response.push_str(&format!("File `{}` does not exist\n", csv_file.display()));
    }

    let txt_file = format!("{run_number}.txt");
    let txt_file = std::path::Path::new(&txt_file);
    if txt_file.exists() {
        response.push_str(&format!("File `{}` does exist", txt_file.display()));
    } else {
        response.push_str(&format!("File `{}` does not exist", txt_file.display()));
    }

    response
}

Can someone point me into some resources on how to move forward from here?

Thanks


Solution

  • this should do the trick.

    the / endpoint returns the html needed to request a file. If the button is pressed it requests the file from the /{number}/{csv/txt} endpoint

    /index.html

    <!DOCTYPE html>
    <html>
        <body>
            <a href="/1/csv" download> Download csv</a>
            <a href="/1/txt" download> Download txt</a>
        </body>
    </html>
    

    /src/main.rs

    use axum::{
        extract::Path,
        http::{header, HeaderMap},
        response::Html,
        routing::get,
        Router,
    };
    use tokio::{fs, process};
    
    #[tokio::main]
    async fn main() {
        let app = Router::new()
            .route("/:number/:file_type", get(file_handler))
            .route("/", get(index));
        let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
        axum::serve(listener, app).await.unwrap();
    }
    
    async fn file_handler(
        Path((number, filetype)): Path<(i32, String)>,
    ) -> Result<(HeaderMap, Vec<u8>), String> {
        if filetype != "csv" && filetype != "txt" {
            return Err("filetype not supported".into());
        };
    
        let file_name = format!("{number}.{filetype}");
    
        let Some(file_contents) = read_and_create_file(&file_name).await else {
            return Err("Could not read/create file".into());
        };
        let mut headers = HeaderMap::new();
        headers.insert(
            header::CONTENT_TYPE,
            format!("text/{filetype}; charset=utf-8").parse().unwrap(),
        );
        headers.insert(
            header::CONTENT_DISPOSITION,
            format!("attachment; filename=\"{file_name}\"")
                .parse()
                .unwrap(),
        );
        Ok((headers, file_contents))
    }
    
    async fn read_and_create_file(file_name: &str) -> Option<Vec<u8>> {
        let file = std::path::Path::new(file_name);
        println!("{file_name}");
        if !file.exists() {
            let _status_code = process::Command::new("touch")
                .arg(file_name)
                .spawn()
                .ok()?
                .wait()
                .await;
        }
        fs::read(file).await.ok()
    }
    
    async fn index() -> Html<Vec<u8>> {
        let html = fs::read("index.html").await.unwrap();
        Html(html)
    }