use std::{
fs::{self}, io::{Read, Write},
net::{TcpListener, TcpStream, UdpSocket},
path::{Path, PathBuf},
};
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 }
}
}
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(¤t_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"),
};
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();
}