From 15fa7769ecb645eaa6629ff068e8a3e91e3b9942 Mon Sep 17 00:00:00 2001 From: Till Hoeppner Date: Wed, 26 Nov 2014 22:37:56 +0100 Subject: Added configuration options --- default.json | 5 ++ src/sersve.rs | 235 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 228 insertions(+), 12 deletions(-) create mode 100644 default.json diff --git a/default.json b/default.json new file mode 100644 index 0000000..4f26f46 --- /dev/null +++ b/default.json @@ -0,0 +1,5 @@ +{ + "address": "0.0.0.0", + "port": 80, + "root": "/home/till" +} diff --git a/src/sersve.rs b/src/sersve.rs index ad7f001..eadb0f0 100644 --- a/src/sersve.rs +++ b/src/sersve.rs @@ -1,32 +1,243 @@ -#![feature(globs, slicing_syntax)] +#![feature(globs, slicing_syntax, if_let, unboxed_closures)] +extern crate getopts; +extern crate serialize; +extern crate glob; extern crate iron; +extern crate persistent; +extern crate error; -use std::os; +use std::{ str, os }; use std::path::{ Path, GenericPath }; +use std::io::{ fs, Reader }; use std::io::fs::{ File, PathExtensions }; +use std::default::Default; +use std::sync::Mutex; + +use error::Error; + +use getopts::{ optopt, optflag, getopts, usage, OptGroup }; + +use serialize::json; +use serialize::json::Json; use iron::prelude::*; use iron::response::modifiers::*; use iron::status; +use iron::mime::*; +use iron::mime::SubLevel::Ext as SubExt; +use iron::middleware::ChainBuilder; +use iron::typemap::Assoc; + +use persistent::Read; + +pub mod constants; + +#[deriving(Send, Clone, Default, Encodable, Decodable)] +struct Options { + host: Option, + port: Option, + root: Option, + custom_css: Option, + filter: Option, + max_size: Option, + not_found: Option +} + +struct OptCarrier; +impl Assoc> for OptCarrier {} + +static UNITS: &'static [&'static str] = &["B", "kB", "MB", "GB", "TB"]; +const BRIEF: &'static str = "A minimal directory server, written in Rust with Iron."; + +fn size_with_unit(mut size: u64) -> String { + let mut frac = 0; + let mut index = 0; + + while size > 1000 && index + 1 < UNITS.len() { + frac = size % 1000; + size /= 1000; + index += 1; + } + + format!("{}.{} {}", size, frac, UNITS[index]) +} + +fn render(root: Path, dir: Path, files: Vec) -> String { + fn render_item(url: Path, size: u64, name: &str) -> String { + format!("{name}{size}\n", + url = url.display(), size = size_with_unit(size), name = name) + } + + let mut content = String::new(); + let mut up = dir.clone(); + up.pop(); + if root.is_ancestor_of(&up) { + content.push_str(render_item(up.path_relative_from(&root).unwrap(), 0, "..")[]); + } + + for file in files.iter() { + let relative = file.path_relative_from(&root).unwrap(); + let stat = file.stat().unwrap(); + let filename = file.filename_display().as_maybe_owned().into_string(); + content.push_str(render_item(relative, stat.size, filename.clone()[])[]); + } + + format!(" + + {title} + + +
+

{title}

+ + + + + + + + + {content} + +
NameSize
+
+ +", + title = dir.display().as_maybe_owned(), + css = constants::CSS, + content = content) +} -fn text(desc: &str) -> IronResult { - Ok(Response::new().set(Status(status::Ok)).set(Body(desc))) +fn plain(content: B) -> IronResult { + Ok(Response::new() + .set(Status(status::Ok)) + .set(Body(content))) +} + +fn html(content: B) -> IronResult { + plain(content).map(|r| r.set(ContentType(Mime(Text, Html, Vec::new())))) +} + +fn binary(content: B) -> IronResult { + plain(content).map( + |r| r.set(ContentType(Mime(Application, SubExt("octet-stream".into_string()), Vec::new())))) +} + +fn guess_text(data: &[u8]) -> bool { + let mut total = 0u; + let mut text = 0u; + for (c, _) in data.iter().zip(range(0u, 1000)) { + let c = *c as char; + if c.is_alphanumeric() || c.is_whitespace() { + text += 1; + } + total += 1; + } + text as f64 / total as f64 > 0.75 } fn serve(req: &mut Request) -> IronResult { - let mut path = os::getcwd().ok().unwrap(); + let root = { + let o = req.get::>>().unwrap(); + let mutex = o.lock(); + mutex.root.clone().unwrap() //_or_else(|| os::getcwd().ok().unwrap()) + }; + + let mut path = root.clone(); for part in req.url.path.iter() { path.push(part[]) } - if !path.exists() { return text("404"); } //Ok(Response::new().set(Status(status::NotFound))); } + if !path.exists() { return html("Well, no... We don't have that today."); } - let content = match File::open(&path).read_to_string() { - Ok(s) => s, - Err(e) => return text(e.desc) - }; + if path.is_file() && root.is_ancestor_of(&path) { + let content = match File::open(&path).read_to_end() { + Ok(s) => s, + Err(e) => return html(e.desc) + }; + if guess_text(content[]) { plain(content[]) } else { binary(content[]) } + } else { + let mut content = match fs::readdir(&path) { + Ok(s) => s, + Err(e) => return html(e.desc) + }; + content.sort_by(|a, b| a.filename_str().unwrap().cmp(b.filename_str().unwrap())); + html(render(root, path, content)[]) + } +} - text(content[]) +fn print_usage(program: &str, opts: &[OptGroup]) { + println!("Usage: {} [options]\n", program); + println!("{}", usage(BRIEF, opts)); } fn main() { - Iron::new(serve).listen("127.0.0.1:80").unwrap(); + let args: Vec = os::args(); + let program = args[0].clone(); + let opts = &[ + optopt("c", "config", "set config file name", "NAME"), + optopt("a", "address", "the address to bind to", "HOST"), + optopt("p", "port", "the port to serve", "PORT"), + optopt("r", "root", "the uppermost directory to serve", "ROOT"), + optflag("h", "help", "print this help menu") + ]; + let matches = match getopts(args.tail(), opts) { + Ok(m) => { m } + Err(f) => { + println!("{}", f.to_string()); + return; + } + }; + + if matches.opt_present("h") { + print_usage(program[], opts); + return; + } + + let options: Mutex = Mutex::new(Default::default()); + + matches.opt_str("c").map(|conf_file| { + let conf_file = File::open(&Path::new(conf_file)); + //conf_file.map_err(|e| panic!("{}", e.desc)); + let json = match json::from_reader(&mut conf_file.ok().unwrap()) { + Ok(Json::Object(o)) => o, + _ => panic!("Invalid configuration file. Doesn't contain top-level object.") + }; + let mut o = options.lock(); + o.host = match json.get("address") { + Some(&Json::String(ref s)) => Some((*s).clone()), + None => None, + _ => panic!("Invalid configuration file. `address` field must be a string.") + }; + o.port = match json.get("port") { + Some(&Json::U64(u)) => Some(u as u16), + None => None, + _ => panic!("Invalid configuration file. `port` field must be an unsigned integer.") + }; + o.root = match json.get("root") { + Some(&Json::String(ref s)) => Some(Path::new((*s).clone())), + None => None, + _ => panic!("Invalid configuration file. `root` field must be a string.") + }; + }); + + let (host, port) = { + let mut o = options.lock(); + o.host = o.host.clone().or(matches.opt_str("a")); + o.port = o.port.or(matches.opt_str("p").and_then(|p| str::from_str(p[]))); + o.root = o.root.clone().or(matches.opt_str("r").and_then(|p| Path::new_opt(p))); + (o.host.clone().unwrap_or("0.0.0.0".into_string()), + o.port.clone().unwrap_or(8080)) + }; + + let mut chain = ChainBuilder::new(serve); + chain.link(Read::>::both(options)); + match Iron::new(chain).listen((host[], port)) { + Ok(_) => (), + Err(e) => println!("I'm sorry, I failed you. {}", if e.description().is_some() { + e.description().unwrap()[] + } else { + "And I don't even know why..." + }) + } } -- cgit v1.2.3