corn 0.2.0

corn is a socket server free to chat & a web server display files
Documentation
use std::{
    fs::{self},   // , File, Metadata
    io::{Read, Write},
    net::{TcpListener, TcpStream, UdpSocket},
    path::{Path, PathBuf},
    // time::{SystemTime, UNIX_EPOCH},
};

struct Config {
    port: u16,
    root_dir: PathBuf,
}

impl Config {
    fn load() -> Self {
        let config_str = fs::read_to_string("./assets/config.toml").unwrap_or_else(|_| String::from(
            "[server]\nport = 0\nroot_dir = \"./runtime/files\""
        ));
        
        let port = config_str
            .lines()
            .find(|l| l.trim().starts_with("port ="))
            .and_then(|l| l.split('=').nth(1))
            .and_then(|s| s.trim().parse().ok())
            .unwrap_or(0);

        let root_dir = config_str
            .lines()
            .find(|l| l.trim().starts_with("root_dir ="))
            .and_then(|l| l.split('=').nth(1))
            .map(|s| s.trim().trim_matches('"').trim())
            .map(PathBuf::from)
            .unwrap_or_else(|| PathBuf::from("./runtime/files"));

        Self { port, root_dir }
    }
}

/// Start the file web server.
///
/// # Examples
///
/// 
/// ```
/// use corn::start_web_server;
///
/// fn main() {
///     println!("Hello, world!");
///     start_web_server()
/// }
/// ```
pub fn start_web_server() {
    let config = Config::load();
    fs::create_dir_all(&config.root_dir).unwrap();

    let local_ip = get_local_ip();
    let listener = TcpListener::bind((local_ip.clone(), config.port)).unwrap();
    let port = listener.local_addr().unwrap().port();
    println!("[Web Server] http://{}:{}\n[File dir]: {:?}", local_ip, port, config.root_dir);

    for stream in listener.incoming() {
        match stream {
            Ok(stream) => {
                let root_dir = config.root_dir.clone();
                std::thread::spawn(move || handle_connection(stream, root_dir));
            }
            Err(e) => eprintln!("connect failed: {}", e),
        }
    }
}

fn get_local_ip() -> String {
    UdpSocket::bind("0.0.0.0:0")
        .and_then(|s| {
            s.connect("8.8.8.8:80")?;
            Ok(s.local_addr()?.ip().to_string())
        })
        .unwrap_or_else(|_| "0.0.0.0".into())
}

fn handle_connection(mut stream: TcpStream, root_dir: PathBuf) {
    let mut buffer = [0; 1024];
    let bytes_read = stream.read(&mut buffer).unwrap_or(0);
    let request = String::from_utf8_lossy(&buffer[..bytes_read]);

    let path = request
        .lines()
        .next()
        .and_then(|line| line.split_whitespace().nth(1))
        .unwrap_or("/");

    match path {
        "/" => serve_directory_listing(&mut stream, &root_dir, ""),
        p if p.starts_with("/browse/") => serve_directory_listing(&mut stream, &root_dir, &p[8..]),
        p if p.starts_with("/download/") => serve_file(&mut stream, &root_dir, &p[10..]),
        _ => not_found(&mut stream),
    }
}

fn serve_directory_listing(stream: &mut TcpStream, root_dir: &Path, relative_path: &str) {
    let current_path = root_dir.join(relative_path);
    let entries = match fs::read_dir(&current_path) {
        Ok(e) => e,
        Err(_) => return not_found(stream),
    };

    let mut dirs = Vec::new();
    let mut files = Vec::new();

    for entry in entries.filter_map(|e| e.ok()) {
        let _path = entry.path();
        let name = entry.file_name().to_string_lossy().into_owned();
        let metadata = entry.metadata().unwrap();
        
        if metadata.is_dir() {
            dirs.push(format!(
                "<li><a href='/browse/{}{}/' class='dir'>📁 {}/</a></li>",
                relative_path,
                name,
                name
            ));
        } else {
            files.push(format!(
                "<li><a href='/download/{}{}' class='file'>📄 {}</a> ({} bytes)</li>",
                relative_path,
                name,
                name,
                metadata.len()
            ));
        }
    }

    let breadcrumbs = generate_breadcrumbs(relative_path);
    let html = format!(
        r#"<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>File List - {}</title>
    <style>
        body {{ font-family: Arial, sans-serif; margin: 20px; }}
        .breadcrumbs {{ margin-bottom: 20px; }}
        ul {{ list-style: none; padding: 0; }}
        li {{ padding: 8px; border-bottom: 1px solid #eee; }}
        a {{ color: #0066cc; text-decoration: none; }}
        a:hover {{ text-decoration: underline; }}
        .dir {{ color: #009933; }}
        .file {{ color: #0066cc; }}
    </style>
</head>
<body>
    <h1>File List</h1>
    <div class="breadcrumbs">{}</div>
    <ul>
        {}
        {}
    </ul>
</body>
</html>"#,
        relative_path,
        breadcrumbs,
        dirs.join("\n"),
        files.join("\n")
    );

    send_response(stream, "200 OK", &html);
}

fn generate_breadcrumbs(relative_path: &str) -> String {
    let parts = relative_path.split('/').filter(|p| !p.is_empty()).collect::<Vec<_>>();
    let mut crumbs = vec!["<a href='/'>root dir</a>".to_string()];
    let mut current_path = String::new();

    for (i, part) in parts.iter().enumerate() {
        current_path.push_str(part);
        current_path.push('/');
        if i < parts.len() - 1 {
            crumbs.push(format!("<a href='/browse/{}'>{}</a>", current_path, part));
        } else {
            crumbs.push(part.to_string());
        }
    }

    crumbs.join(" / ")
}

fn serve_file(stream: &mut TcpStream, root_dir: &Path, file_path: &str) {
    let path = root_dir.join(file_path);
    if !path.exists() {
        return not_found(stream);
    }

    let filename = path.file_name().unwrap().to_string_lossy();
    let content = match fs::read(&path) {
        Ok(data) => data,
        Err(_) => return send_response(stream, "500 Internal Server Error", "Unable to read file"),
    };

    // inline: preview    attachment: download only
    let response = format!(
        "HTTP/1.1 200 OK\r\n\
         Content-Disposition: inline; filename=\"{}\"\r\n\
         Content-Type: text/html; charset=utf-8\r\n\
         Content-Length: {}\r\n\r\n",
        filename,
        content.len()
    );

    if stream.write_all(response.as_bytes()).is_ok() && stream.write_all(&content).is_ok() {
        println!("file sent: {}", filename);
    }
}

fn not_found(stream: &mut TcpStream) {
    send_response(stream, "404 Not Found", "Page not found");
}

fn send_response(stream: &mut TcpStream, status: &str, content: &str) {
    let response = format!(
        "HTTP/1.1 {}\r\nContent-Length: {}\r\n\r\n{}",
        status,
        content.len(),
        content
    );
    stream.write_all(response.as_bytes()).unwrap();
}