use crate::{Request, Result}; use gophermap::{GopherMenu, ItemType}; use std::{ fs, io::prelude::*, io::{BufReader, Read, Write}, net::{TcpListener, TcpStream}, }; use threadpool::ThreadPool; const MAX_WORKERS: usize = 10; const MAX_PEEK_SIZE: usize = 1024; const TCP_BUF_SIZE: usize = 1024; /// Starts a Gopher server at the specified host, port, and root directory. pub fn start(host: &str, port: u16, root: &str) -> Result<()> { let addr = format!("{}:{}", host, port); let listener = TcpListener::bind(&addr)?; let full_root_path = fs::canonicalize(&root)?.to_string_lossy().to_string(); let pool = ThreadPool::new(MAX_WORKERS); println!("-> Listening on {} at {}", addr, full_root_path); for stream in listener.incoming() { let stream = stream?; println!("-> Connection from: {}", stream.peer_addr()?); let req = Request::from(host, port, root)?; pool.execute(move || { if let Err(e) = accept(stream, req) { eprintln!("-! {}", e); } }); } Ok(()) } /// Reads from the client and responds. fn accept(stream: TcpStream, mut req: Request) -> Result<()> { let reader = BufReader::new(&stream); let mut lines = reader.lines(); if let Some(Ok(line)) = lines.next() { println!("-> Client sent: {:?}", line); req.selector = line; write_response(&stream, req)?; } Ok(()) } /// Writes a response to a client based on a Request. fn write_response<'a, W>(w: &'a W, req: Request) -> Result<()> where &'a W: Write, { let md = fs::metadata(&req.file_path())?; if md.is_file() { write_file(w, req) } else if md.is_dir() { write_dir(w, req) } else { Ok(()) } } /// Send a directory listing (menu) to the client based on a Request. fn write_dir<'a, W>(w: &'a W, req: Request) -> Result<()> where &'a W: Write, { let mut dir = fs::read_dir(&req.file_path())?; let mut menu = GopherMenu::with_write(w); let rel_path = req.relative_file_path(); while let Some(Ok(entry)) = dir.next() { let mut path = rel_path.clone(); if !path.ends_with('/') { path.push('/'); } let file_name = entry.file_name(); path.push_str(&file_name.to_string_lossy()); menu.write_entry( file_type(&entry), &file_name.to_string_lossy(), &path, &req.host, req.port, )?; } menu.end()?; Ok(()) } /// Send a file to the client based on a Request. fn write_file<'a, W>(mut w: &'a W, req: Request) -> Result<()> where &'a W: Write, { let path = req.file_path(); let md = fs::metadata(&path)?; let mut f = fs::File::open(&path)?; let mut buf = [0; TCP_BUF_SIZE]; let mut bytes = md.len(); while bytes > 0 { let n = f.read(&mut buf[..])?; bytes -= n as u64; w.write_all(&buf[..n])?; } Ok(()) } /// Determine the gopher type for a DirEntry on disk. fn file_type(dir: &fs::DirEntry) -> ItemType { let metadata = match dir.metadata() { Err(_) => return ItemType::Error, Ok(md) => md, }; if metadata.is_file() { if let Ok(file) = fs::File::open(&dir.path()) { let mut buffer: Vec = vec![]; let _ = file.take(MAX_PEEK_SIZE as u64).read_to_end(&mut buffer); if content_inspector::inspect(&buffer).is_binary() { ItemType::Binary } else { ItemType::File } } else { ItemType::Binary } } else if metadata.is_dir() { ItemType::Directory } else { ItemType::Error } }