scratch-server 1.0.1

Simple HTTP Server
Documentation
use std::{
    fs,
    path::{Path, PathBuf},
};

use chrono::{DateTime, Utc};
use scratch_server::api_error::ApiError;
use serde::{Deserialize, Serialize};

const SUFFIX: [&str; 9] = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
const UNIT: f64 = 1000.0;

#[derive(Debug, Serialize, Deserialize)]
struct PathParts {
    part_name: String,
    full_path: String,
}

#[derive(Debug, Serialize, Deserialize)]
enum FileType {
    Directory,
    File,
}

#[derive(Debug, Serialize, Deserialize)]
struct Files {
    path: String,
    name: String,
    file_type: FileType,
    last_modified: String,
    size: String,
}

#[derive(Debug, Serialize, Deserialize)]
struct DirectoryInfoResponse {
    paths: Vec<PathParts>,
    files: Vec<Files>,
}

fn human_bytes<T: Into<f64>>(bytes: T) -> String {
    let size = bytes.into();

    if size <= 0.0 {
        return "0 B".to_string();
    }

    let base = size.log10() / UNIT.log10();

    let result = format!("{:.1}", UNIT.powf(base - base.floor()),)
        .trim_end_matches(".0")
        .to_owned();

    [&result, SUFFIX[base.floor() as usize]].join(" ")
}

pub fn list_directory(path: &str) -> Result<serde_json::Value, ApiError> {
    let base_root_path = Path::new("./").canonicalize()?;
    let target_path = PathBuf::from("./").join(
        percent_encoding::percent_decode_str(path)
            .decode_utf8_lossy()
            .to_string(),
    );
    let cannonical_target_path = target_path.canonicalize()?;

    let current_dir = std::env::current_dir()?.canonicalize()?;
    if !cannonical_target_path.starts_with(current_dir) {
        return Err(ApiError::new_with_json(
            400,
            "Only paths relative to the current directory are allowed",
        ));
    }

    let mut current_full_path = String::new();
    let mut directory_response = DirectoryInfoResponse {
        paths: target_path
            .components()
            .map(|c| {
                let part_name = c.as_os_str().to_string_lossy().to_string();
                current_full_path.push_str(&part_name);
                current_full_path.push('/');
                PathParts {
                    part_name,
                    full_path: current_full_path.clone(),
                }
            })
            .collect(),
        files: Vec::new(),
    };

    let directory_contents = fs::read_dir(cannonical_target_path)?;

    for path in directory_contents {
        let path = path?;
        let system_time: DateTime<Utc> = path.metadata()?.modified()?.into();

        let file = Files {
            name: path.file_name().into_string().unwrap(),
            path: path
                .path()
                .strip_prefix(&base_root_path)
                .map_err(|err| ApiError::new_with_json(500, &err.to_string()))?
                .to_string_lossy()
                .into_owned(),
            file_type: if path.path().is_dir() {
                FileType::Directory
            } else {
                FileType::File
            },
            last_modified: system_time.format("%d/%m/%Y %T").to_string(),
            size: human_bytes(path.metadata()?.len() as f64),
        };
        directory_response.files.push(file);
    }

    let v = serde_json::to_value(directory_response)?;

    Ok(v)
}

pub fn parse_index_path(path: &str) -> Result<PathBuf, String> {
    let index_path = PathBuf::from(path);
    if index_path.exists() {
        Ok(index_path)
    } else {
        Err("Index file not found".to_string())
    }
}