From d950c25abf54ef32adade1b84dd2474c4c5a3a22 Mon Sep 17 00:00:00 2001 From: Till Hoeppner Date: Mon, 11 May 2015 22:52:49 +0200 Subject: Update to new Rust --- src/sersve.rs | 410 +++++++++++++++++++++++++++------------------------------- 1 file changed, 190 insertions(+), 220 deletions(-) (limited to 'src') diff --git a/src/sersve.rs b/src/sersve.rs index ec7c127..4b6bb00 100644 --- a/src/sersve.rs +++ b/src/sersve.rs @@ -1,41 +1,37 @@ -#![feature(phase, globs, slicing_syntax, if_let, unboxed_closures)] +#![feature(unboxed_closures, custom_attribute, thread_local, plugin, custom_derive, path_ext, path_relative_from)] +#![plugin(serde_macros, docopt_macros)] -extern crate getopts; -extern crate serialize; extern crate iron; -extern crate persistent; extern crate regex; -extern crate "conduit-mime-types" as conduit_mime; -extern crate mime; +extern crate conduit_mime_types; extern crate mustache; extern crate libc; - -use std::{ str, os }; -use std::str::from_str; -use std::path::{ Path, GenericPath }; -use std::io::{ fs, Reader }; -use std::io::fs::{ File, PathExtensions }; -use std::default::Default; +extern crate num_cpus; +extern crate serde; +#[macro_use] +extern crate lazy_static; +extern crate docopt; +extern crate rustc_serialize; + +use std::{ env, fs, process }; +use std::path::{ Path, PathBuf }; +use std::io::{ Read, Write }; +use std::error::Error; +use std::fs::{ File, PathExt }; use std::sync::Arc; -use std::cell::{ RefCell }; +use std::borrow::Borrow; use regex::Regex; -use conduit_mime::Types; - -use getopts::{ optopt, optflag, getopts, usage, OptGroup }; +use conduit_mime_types::Types; -use serialize::json; -use serialize::json::Json; +use serde::json::{ self, Value }; use iron::prelude::*; -use iron::response::modifiers::*; use iron::status; -use iron::mime::*; -use iron::middleware::ChainBuilder; -use iron::typemap::Assoc; - -use persistent::Read; +use iron::mime::{ Mime, TopLevel, SubLevel }; +use iron::headers::ContentType; +use iron::modifiers::Header; use mustache::{ Template, VecBuilder, MapBuilder }; @@ -43,35 +39,46 @@ use constants::*; pub mod constants; -#[deriving(Send, Clone, Default, Encodable, Decodable)] -struct Options { - host: Option, - port: Option, - root: Option, - filter: Option, - max_size: Option, - template: Option, - fork: Option, - threads: Option -} - -struct OptCarrier; -impl Assoc> for OptCarrier {} - -#[deriving(Send, Clone)] +docopt!(Args derive Debug, " +A minimal static file server, written in Rust with Iron. +Usage: sersve [options] + +Options: + -h, --help Show this message. + -v, --version Show the version of sersve (duh). + -c, --config FILE Provide a configuration file (JSON). + -a, --address HOST The address to bind to. + -p, --port PORT The port to serve. + -r, --root ROOT The uppermost directory to serve. + -f, --filter REGEX A regular expression to filter the filenames. + -s, --size BYTES The maximum size of a file that will be served. + -t, --template TEMPLATE A Mustache template to use for rendering. + --threads THREADS Amount of threads to use for serving. + --fork Fork sersve into a background process.", + flag_help: bool, + flag_version: bool, + flag_config: Option, + flag_address: Option, + flag_port: Option, + flag_root: Option, + flag_filter: Option, + flag_size: Option, + flag_template: Option, + flag_threads: Option, + flag_fork: bool +); + +#[derive(Clone)] struct State { template: Template, + root: Option, mime_types: Arc } -struct StateCarrier; -impl Assoc> for StateCarrier {} - const HOST: &'static str = "0.0.0.0"; const PORT: u16 = 8080; static UNITS: &'static [&'static str] = &["B", "kB", "MB", "GB", "TB"]; -const BRIEF: &'static str = "A minimal directory server, written in Rust with Iron."; const KEY_TITLE: &'static str = "title"; const KEY_CONTENT: &'static str = "content"; @@ -79,9 +86,81 @@ const KEY_URL: &'static str = "url"; const KEY_SIZE: &'static str = "size"; const KEY_NAME: &'static str = "name"; -const DEF_LEN: uint = 10000; +const DEF_LEN: usize = 10000; -thread_local! (static OUT: RefCell> = RefCell::new(Vec::with_capacity(DEF_LEN))) +lazy_static! { + static ref ARGS: Args = { + let mut args: Args = Args::docopt().decode().unwrap_or_else(|e| e.exit()); + + if let Some(ref flag_config) = args.flag_config { + let conf = File::open(&flag_config) + .and_then(|mut f| { + let mut out = String::new(); + f.read_to_string(&mut out).map(|_| out) + }).map_err(|e| error(e.description())) + .unwrap(); + + // cannot if-let, because typesafe errors are helpful + let json = match json::from_str(&conf) { + Ok(Value::Object(o)) => o, + _ => panic!("Invalid configuration file. Doesn't contain valid top-level object.") + }; + args.flag_address = args.flag_address.or(match json.get("address") { + Some(&Value::String(ref s)) => Some((*s).clone()), + None => None, + _ => panic!("Invalid configuration file. `address` field must be a string.") + }); + args.flag_port = args.flag_port.or(match json.get("port") { + Some(&Value::U64(u)) => Some(u as u16), + None => None, + _ => panic!("Invalid configuration file. `port` field must be an unsigned integer.") + }); + args.flag_root = args.flag_root.or(match json.get("root") { + Some(&Value::String(ref s)) => Some(s.clone()), + None => None, + _ => panic!("Invalid configuration file. `root` field must be a string.") + }); + args.flag_filter = args.flag_filter.or(match json.get("filter") { + Some(&Value::String(ref s)) => Some((*s).clone()), + None => None, + _ => panic!("Invalid configuration file. `filter` field must be a string.") + }); + args.flag_size = args.flag_size.or(match json.get("size") { + Some(&Value::U64(u)) => Some(u), + None => None, + _ => panic!("Invalid configuration file. `size` field must be an unsigned integer.") + }); + args.flag_template = args.flag_template.or(match json.get("template") { + Some(&Value::String(ref s)) => Some((*s).clone()), + None => None, + _ => panic!("Invalid configuration file. `template` field must be a string.") + }); + args.flag_fork = args.flag_fork || match json.get("fork") { + Some(&Value::Bool(b)) => b, + None => false, + _ => panic!("Invalid configuration file. `fork` field must be a boolean") + }; + args.flag_threads = args.flag_threads.or(match json.get("threads") { + Some(&Value::U64(u)) => Some(u as usize), + None => None, + _ => panic!("Invalid configuration file. `threads` field must be a string.") + }) + }; + + args + }; + + static ref STATE: State = State { + template: mustache::compile_str(&ARGS.flag_template.as_ref().unwrap_or(&OPT_TEMPLATE.to_owned())), + root: ARGS.flag_root.clone().map(|p| Path::new(&p).to_path_buf()), + mime_types: Arc::new(Types::new().ok().unwrap()) + }; +} + +fn error(e: &str) -> ! { + println!("Error: {}", e); + process::exit(-1); +} fn fork() { unsafe { @@ -91,7 +170,7 @@ fn fork() { return; } else if pid > 0 { // fork succeeded, die - libc::funcs::c95::stdlib::exit(0); + process::exit(0); } else if pid < 0 { // unsuccessful, don't die return; @@ -112,223 +191,114 @@ fn size_with_unit(mut size: u64) -> String { format!("{}.{} {}", size, frac, UNITS[index]) } -fn render<'a>(template: Template, root: Path, dir: Path, files: Vec, filter: Option) -> String { - let data = MapBuilder::<'a>::new() - .insert_str(KEY_TITLE, dir.display().as_cow().into_owned()) - .insert_vec(KEY_CONTENT, |mut vec: VecBuilder<'a>| { +fn render<'a, W: Write>(mut out: W, template: Template, root: PathBuf, dir: PathBuf, files: Vec, filter: Option) { + let data = MapBuilder::new() + .insert_str(KEY_TITLE, format!("{}", dir.display())) + .insert_vec(KEY_CONTENT, |mut vec: VecBuilder| { let item = |map: MapBuilder, url: &Path, size: u64, name: String| { - map.insert(KEY_URL, &format!("{}", url.display())[]).unwrap() - .insert(KEY_SIZE, &size_with_unit(size)[]).unwrap() + map.insert(KEY_URL, &format!("{}", url.display())).unwrap() + .insert(KEY_SIZE, &size_with_unit(size)).unwrap() .insert_str(KEY_NAME, name) }; // add `..` entry if necessary - let mut up = dir.clone(); + let mut up = dir.to_path_buf(); up.pop(); - if root.is_ancestor_of(&up) { - vec = vec.push_map(|map: MapBuilder| item(map, &up.path_relative_from(&root).unwrap(), 0, "..".into_string())); + if up.starts_with(&root) { + vec = vec.push_map(|map: MapBuilder| item(map, &up.relative_from(&root).unwrap(), 0, "..".to_owned())); } for file in files.iter() { - let relative = file.path_relative_from(&root).unwrap(); - let stat = file.stat().unwrap(); - let filename = file.filename_display().as_cow().into_owned(); - if filter.as_ref().map_or(true, |f| f.is_match(filename[])) { - vec = vec.push_map(|map| item(map, &relative, stat.size, filename.clone())); + let relative = file.relative_from(&root).unwrap(); + let stat = file.metadata().unwrap(); + let filename = file.file_name() + .expect("Cannot get filename").to_string_lossy().into_owned(); + if filter.as_ref().map_or(true, |f| f.is_match(&filename)) { + vec = vec.push_map(|map| item(map, &relative, stat.len(), filename.clone())); } } vec }).build(); - // Use thread-local storage to reduce allocation - OUT.with(|ref_out| { - { - let mut o = ref_out.borrow_mut(); - o.clear(); - template.render_data(&mut *o, &data); - } - // The template should be valid utf8, the filenames might not be - String::from_utf8(ref_out.borrow().clone()).unwrap_or_else(|v| { - String::from_utf8_lossy(v[]).into_owned() - }) - }) - } + template.render_data(&mut out, &data); +} + +fn plain(content: &[u8]) -> IronResult { + Ok(Response::with((status::Ok, content))) +} -fn plain(content: B) -> IronResult { - Ok(Response::new() - .set(Status(status::Ok)) - .set(Body(content))) +fn html(content: &[u8]) -> IronResult { + plain(content).map(|r| r.set(Header(ContentType(Mime(TopLevel::Text, SubLevel::Html, Vec::new()))))) } -fn html(content: B) -> IronResult { - plain(content).map(|r| r.set(ContentType(Mime(Text, Html, Vec::new())))) +fn from_path(path: &Path) -> IronResult { + Ok(Response::with((status::Ok, path))) } fn serve(req: &mut Request) -> IronResult { - let (root, filter_str, max_size) = { - let o = req.get::>>().unwrap(); - (o.root.clone().unwrap_or_else(|| os::getcwd().ok().unwrap()), - o.filter.clone(), - o.max_size) - }; + let (filter_str, max_size) = ( + ARGS.flag_filter.clone(), + ARGS.flag_size + ); - let (template, mime_types) = { - let s = req.get::>>().unwrap(); - (s.template.clone(), - s.mime_types.clone()) - }; + let (template, root, mime_types) = ( + STATE.template.clone(), + STATE.root.clone().unwrap_or_else(|| env::current_dir().ok().unwrap()), + STATE.mime_types.clone() + ); let mut path = root.clone(); - for part in req.url.path.iter() { path.push(part[]) } - if !path.exists() { return html("Well, no... We don't have that today."); } + for part in req.url.path.iter() { path.push(part) } + if !path.exists() { return html(b"Well, no... We don't have that today."); } - let filter = filter_str.and_then(|s| Regex::new(s[]).ok()); + let filter = filter_str.and_then(|s| Regex::new(&s).ok()); - if path.is_file() && root.is_ancestor_of(&path) { - let stat = path.stat(); - if stat.as_ref().ok().is_some() && max_size.is_some() && stat.ok().unwrap().size > max_size.unwrap() { - return html("I'm afraid, I'm too lazy to serve the requested file. It's pretty big...") + if path.is_file() && path.starts_with(&root) { + let stat = path.metadata(); + if stat.as_ref().ok().is_some() && max_size.is_some() && stat.ok().unwrap().len() > max_size.unwrap() { + return html(b"I'm afraid, I'm too lazy to serve the requested file. It's pretty big...") } - let content = match File::open(&path).read_to_end() { + /*let content = match File::open(&path).read_to_end() { Ok(s) => s, Err(e) => return html(e.desc) - }; + };*/ - if filter.as_ref().map_or(false, |f| !f.is_match(path.filename_str().unwrap())) { - return html("I don't think you're allowed to do this."); + if filter.as_ref().map_or(false, |f| !f.is_match(path.file_name().unwrap().to_string_lossy().borrow())) { + return html(b"I don't think you're allowed to do this."); } - let mime: Option = path.extension_str() - .map_or(None, |e| mime_types.get_mime_type(e)) - .map_or(None, |m| from_str(m)); + let mime: Option = path.extension().map(|s| s.to_string_lossy().to_owned()) + .map_or(None, |e| mime_types.get_mime_type(e.borrow())) + .and_then(|m| m.parse().ok()); if mime.as_ref().is_some() { - plain(content[]).map(|r| r.set(ContentType((*mime.as_ref().unwrap()).clone()))) + from_path(&path).map(|r| r.set(Header(ContentType((*mime.as_ref().unwrap()).clone())))) } else { - plain(content[]) + from_path(path.as_path()) } } else { - let mut content = match fs::readdir(&path) { - Ok(s) => s, - Err(e) => return html(e.desc) + let mut content: Vec = match fs::read_dir(&path) { + Ok(s) => s.filter_map(Result::ok).map(|s| s.path()).collect(), + Err(e) => return html(e.description().as_bytes()) }; - content.sort_by(|a, b| a.filename_str().unwrap().cmp(b.filename_str().unwrap())); - html(render(template, root, path, content, filter)) + content.sort_by(|a, b| a.to_string_lossy().cmp(&b.to_string_lossy())); + let mut out = Vec::with_capacity(DEF_LEN); + render(&mut out, template, root, path, content, filter); + html(&out) } } -fn print_usage(program: &str, opts: &[OptGroup]) { - println!("Usage: {} [options]\n", program); - println!("{}", usage(BRIEF, opts)); -} - fn main() { - 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"), - optopt("f", "filter", "a regular expression to filter the filenames", "REGEX"), - optopt("s", "size", "the maximum size of a file that will be served", "BYTES"), - optopt("t", "template", "a mustache template to use for rendering", "TEMPLATE"), - optopt("", "threads", "amount of threads to use for serving", "THREADS"), - optflag("", "fork", "fork sersve into a background process"), - 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 mut options: Options = Default::default(); - - matches.opt_str("c").map(|conf_file| { - let conf_file = File::open(&Path::new(conf_file)); - conf_file.as_ref().map_err::<()>(|e| panic!("{}", e.desc)).unwrap(); - - // cannot if-let, because typesafe errors are helpful - 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.") - }; - options.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.") - }; - options.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.") - }; - options.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.") - }; - options.filter = match json.get("filter") { - Some(&Json::String(ref s)) => Some((*s).clone()), - None => None, - _ => panic!("Invalid configuration file. `filter` field must be a string.") - }; - options.max_size = match json.get("size") { - Some(&Json::U64(u)) => Some(u), - None => None, - _ => panic!("Invalid configuration file. `size` field must be an unsigned integer.") - }; - options.template = match json.get("template") { - Some(&Json::String(ref s)) => Some((*s).clone()), - None => None, - _ => panic!("Invalid configuration file. `template` field must be a string.") - }; - options.fork = match json.get("fork") { - Some(&Json::Boolean(b)) => Some(b), - None => None, - _ => panic!("Invalid configuration file. `fork` field must be a boolean") - }; - options.threads = match json.get("threads") { - Some(&Json::U64(u)) => Some(u as uint), - None => None, - _ => panic!("Invalid configuration file. `threads` field must be a string.") - } - }); - let (host, port, threads) = { - options.host = matches.opt_str("a").or(options.host); - options.port = matches.opt_str("p").and_then(|p| str::from_str(p[])).or(options.port); - options.root = matches.opt_str("r").and_then(|p| Path::new_opt(p)).or(options.root); - options.filter = matches.opt_str("f").or(options.filter); - options.max_size = matches.opt_str("s").and_then(|s| str::from_str(s[])).or(options.max_size); - options.template = matches.opt_str("t").or(options.template); - options.threads = matches.opt_str("threads").and_then(|s| str::from_str(s[])).or(options.threads); - (options.host.clone().unwrap_or(HOST.into_string()), - options.port.clone().unwrap_or(PORT), - options.threads.unwrap_or(os::num_cpus())) + (ARGS.flag_address.clone().unwrap_or(HOST.into()), + ARGS.flag_port.clone().unwrap_or(PORT), + ARGS.flag_threads.unwrap_or(num_cpus::get())) }; - if options.fork.unwrap_or(false) || matches.opt_present("fork") { + if ARGS.flag_fork { fork(); } - let template = mustache::compile_str(options.template.clone().unwrap_or(OPT_TEMPLATE.into_string())[]); - let state = State { - template: template, - mime_types: Arc::new(Types::new().ok().unwrap()) - }; - - let mut chain = ChainBuilder::new(serve); - chain.link(Read::>::both(Arc::new(options))); - chain.link(Read::>::both(Arc::new(state))); - match Iron::new(chain).listen_with((host[], port), threads) { + match Iron::new(serve).listen_with((host.as_ref(), port), threads, iron::Protocol::Http) { Ok(_) => (), - Err(e) => println!("I'm sorry, I failed you.\nError: {}", e) + Err(e) => println!("I'm sorry, I failed you.\nError: {:?}", e) } } -- cgit v1.2.3