duplo 0.1.2

Web application for easy deployment of a file sharing portal in private networks
use std::{sync::Arc, time::UNIX_EPOCH};

use askama::Template;
use askama_axum::IntoResponse;
use axum::extract::OriginalUri;
use axum::http::header::CACHE_CONTROL;
use axum::response::{Response, Redirect};
use axum::{
    self,
    extract::State,
    http::{HeaderValue, StatusCode},
    Extension,
};
use humansize::BINARY;

use crate::SharedDirectory;
use crate::disksize::Quotas;

pub struct FileInfo {
    pub time: u64,
    pub name: String,
    pub size: String,
}

#[derive(Template)]
#[template(path = "view.html")]
pub struct ViewTemplate {
    pub title: String,
    pub files: Vec<FileInfo>,
    pub err: String,
}

#[axum::debug_handler]
pub(crate) async fn serve_view(
    OriginalUri(uri): OriginalUri,
    Extension(shared_dir): Extension<Arc<SharedDirectory>>,
    State(quotas): State<Arc<Quotas>>,
) -> Result<Response, StatusCode> {
    if ! uri.path().ends_with('/') {
        return Ok(Redirect::permanent(&format!("{}/", uri.path())).into_response())
    }
    let files = std::fs::read_dir(&*shared_dir.dir).map_err(|e| {
        tracing::error!("readdir: {e}");
        StatusCode::INTERNAL_SERVER_ERROR
    })?;
    let mut err = String::new();
    if quotas.files.is_exceed() {
        err += "Too many files\n"
    }
    if quotas.bytes.is_close_to_exeeed() {
        if quotas.bytes.is_exceed() {
            err += "Disk storage quota full\n"
        } else {
            err += "Disk storage quota is close to being full\n"
        }
    }
    let files = files
        .flat_map(|f| match f {
            Err(e) => {
                tracing::error!("readdir 2: {e}");
                err += &format!("{e}");
                None
            }
            Ok(f) => {
                if let Ok(mut name) = f.file_name().into_string() {
                    if name.starts_with('.') {
                        return None;
                    }
                    let mut time = 0;
                    let mut size = String::new();
                    if let Ok(metadata) = f.metadata() {
                        size = humansize::format_size(metadata.len(), BINARY);
                        if let Ok(modified) = metadata.modified() {
                            if let Ok(dur) = modified.duration_since(UNIX_EPOCH) {
                                time = dur.as_secs();
                            }
                        }
                        if metadata.is_dir() {
                            name += "/";
                        }
                    }
                    Some(FileInfo { name, size, time })
                } else {
                    err += "Malformed filename skipped from the list";
                    None
                }
            }
        })
        .collect();
    let mut response = ViewTemplate {
        title: shared_dir.title.clone(),
        files,
        err,
    }
    .into_response();
    let h = response.headers_mut();
    h.insert(CACHE_CONTROL, HeaderValue::from_static("no-cache"));
    h.insert(axum::http::header::CONTENT_SECURITY_POLICY, HeaderValue::from_static("default-src 'none'; img-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; connect-src 'self'; font-src 'self'; frame-ancestors 'none'"));
    Ok(response)
}