terrazzo-terminal 0.2.7

A simple web-based terminal emulator built on Terrazzo.
use std::path::PathBuf;

use terrazzo::axum::Router;
use terrazzo::axum::body::Body;
use terrazzo::axum::extract::Query;
use terrazzo::axum::response::IntoResponse;
use terrazzo::axum::routing::get;
use terrazzo::axum::routing::post;
use terrazzo::http::StatusCode;
use terrazzo::http::header;
use tokio::io::AsyncWriteExt as _;
use tokio_stream::StreamExt as _;
use trz_gateway_common::dynamic_config::DynamicConfig;
use trz_gateway_common::dynamic_config::has_diff::DiffArc;
use trz_gateway_common::dynamic_config::mode;

use crate::backend::auth::AuthConfig;
use crate::backend::auth::layer::AuthLayer;
use crate::text_editor::file_path::FilePath;

pub(crate) fn fsio_routes(
    auth_config: &DiffArc<DynamicConfig<DiffArc<AuthConfig>, mode::RO>>,
) -> Router {
    Router::new()
        .nest(
            "/text_editor/fsio",
            Router::new()
                .route("/download", get(download_file))
                .route("/upload", post(upload_file)),
        )
        .route_layer(AuthLayer {
            auth_config: auth_config.clone(),
        })
}

async fn download_file(
    Query(path): Query<ApiFilePath>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
    let path = path.into_file_path().full_path();
    if !path.exists() {
        return Err((
            StatusCode::NOT_FOUND,
            format!("Path not found: {}", path.display()),
        ));
    }
    if !path.is_file() {
        return Err((
            StatusCode::BAD_REQUEST,
            format!("Path is not a file: {}", path.display()),
        ));
    }

    let content = tokio::fs::read(path).await.map_err(internal_server_error)?;
    Ok((
        [(header::CONTENT_TYPE, "application/octet-stream")],
        content,
    ))
}

async fn upload_file(
    Query(path): Query<ApiFilePath>,
    content: Body,
) -> Result<impl IntoResponse, (StatusCode, String)> {
    let path = path.into_file_path().full_path();
    if path.is_dir() {
        return Err((
            StatusCode::BAD_REQUEST,
            format!("Path is a directory: {}", path.display()),
        ));
    }
    if let Some(parent) = path.parent()
        && !parent.is_dir()
    {
        return Err((
            StatusCode::NOT_FOUND,
            format!("Parent directory not found: {}", parent.display()),
        ));
    }

    let mut file = tokio::fs::File::create(path)
        .await
        .map_err(internal_server_error)?;
    let mut content = content.into_data_stream();
    while let Some(chunk) = content.next().await {
        let chunk = chunk.map_err(|error| (StatusCode::BAD_REQUEST, error.to_string()))?;
        file.write_all(&chunk)
            .await
            .map_err(internal_server_error)?;
    }

    Ok(StatusCode::NO_CONTENT)
}

fn internal_server_error(error: std::io::Error) -> (StatusCode, String) {
    (StatusCode::INTERNAL_SERVER_ERROR, error.to_string())
}

#[derive(serde::Deserialize)]
struct ApiFilePath {
    base: PathBuf,
    file: PathBuf,
}

impl ApiFilePath {
    fn into_file_path(self) -> FilePath<PathBuf> {
        FilePath {
            base: self.base,
            file: self.file,
        }
    }
}