use clap; use directories::ProjectDirs; use std::fmt::Debug; use std::fs::File; use std::io::{Read, Write}; use std::path::{Path, PathBuf}; use tile_source::TileSource; use toml::Value; static DEFAULT_CONFIG: &'static str = ""; static DEFAULT_TILE_SOURCES: &'static str = include_str!("../default_tile_sources.toml"); lazy_static! { static ref PROJ_DIRS: ProjectDirs = ProjectDirs::from("", "", "DeltaMap"); } #[derive(Debug)] pub struct Config { tile_cache_dir: PathBuf, sources: Vec<(String, TileSource)>, pbf_path: Option, search_pattern: Option, fps: f64, use_network: bool, async: bool, } impl Config { //TODO use builder pattern to create config pub fn from_arg_matches<'a>(matches: &clap::ArgMatches<'a>) -> Result { let mut config = if let Some(config_path) = matches.value_of_os("config") { Config::from_toml_file(config_path)? } else { Config::find_or_create()? }; if let Some(tile_sources_path) = matches.value_of_os("tile-sources") { config.add_tile_sources_from_file(tile_sources_path)?; } else { config.add_tile_sources_from_default_or_create()?; }; if let Some(os_path) = matches.value_of_os("pbf") { let path = PathBuf::from(os_path); if path.is_file() { config.pbf_path = Some(path); } else { return Err(format!("PBF file does not exist: {:?}", os_path)); } } config.merge_arg_matches(matches); Ok(config) } fn merge_arg_matches<'a>(&mut self, matches: &clap::ArgMatches<'a>) { self.search_pattern = matches.value_of("search").map(|s| s.to_string()); if let Some(Ok(fps)) = matches.value_of("fps").map(|s| s.parse()) { self.fps = fps; } if matches.is_present("offline") { self.use_network = false; } if matches.is_present("sync") { self.async = false; } } fn create_config_file + Debug>(dir_path: P, file_path: P, contents: &[u8]) -> Result<(), String> { if !dir_path.as_ref().is_dir() { if let Err(err) = ::std::fs::create_dir_all(&dir_path) { return Err(format!("failed to create config directory ({:?}): {}", dir_path, err )); } } let mut file = File::create(&file_path) .map_err(|err| format!("failed to create config file {:?}: {}", &file_path, err))?; file.write_all(contents) .map_err(|err| format!( "failed to write contents to config file {:?}: {}", &file_path, err )) } fn find_or_create() -> Result { let config_dir = PROJ_DIRS.config_dir(); let config_file = { let mut path = PathBuf::from(config_dir); path.push("config.toml"); path }; if config_file.is_file() { info!("load config from path {:?}", config_file); Config::from_toml_file(config_file) } else { // try to write a default config file match Config::create_config_file( config_dir, &config_file, DEFAULT_CONFIG.as_bytes() ) { Err(err) => warn!("{}", err), Ok(()) => info!("create default config file {:?}", config_file), } Config::from_toml_str(DEFAULT_CONFIG) } } fn add_tile_sources_from_default_or_create(&mut self) -> Result<(), String> { let config_dir = PROJ_DIRS.config_dir(); let sources_file = { let mut path = PathBuf::from(config_dir); path.push("tile_sources.toml"); path }; if sources_file.is_file() { info!("load tile sources from path {:?}", sources_file); self.add_tile_sources_from_file(sources_file) } else { // try to write a default config file match Config::create_config_file( config_dir, &sources_file, DEFAULT_TILE_SOURCES.as_bytes() ) { Err(err) => warn!("{}", err), Ok(()) => info!("create default tile sources file {:?}", sources_file), } self.add_tile_sources_from_str(DEFAULT_TILE_SOURCES) } } /// Returns a tile cache directory path at a standard location. The returned path may not /// exist. fn default_tile_cache_dir() -> PathBuf { let mut path = PathBuf::from(PROJ_DIRS.cache_dir()); path.push("tiles"); path } fn from_toml_str(toml_str: &str) -> Result { match toml_str.parse::() { Ok(Value::Table(ref table)) => { let tile_cache_dir = { match table.get("tile_cache_dir") { Some(dir) => { PathBuf::from( dir.as_str() .ok_or_else(|| "tile_cache_dir has to be a string".to_string())? ) }, None => Config::default_tile_cache_dir(), } }; let fps = { match table.get("fps") { Some(&Value::Float(fps)) => fps, Some(&Value::Integer(fps)) => fps as f64, Some(_) => return Err("fps has to be an integer or a float.".to_string()), None => 60.0, } }; let use_network = { match table.get("use_network") { Some(&Value::Boolean(x)) => x, Some(_) => return Err("use_network has to be a boolean.".to_string()), None => true, } }; let async = { match table.get("async") { Some(&Value::Boolean(x)) => x, Some(_) => return Err("async has to be a boolean.".to_string()), None => true, } }; Ok( Config { tile_cache_dir, sources: vec![], pbf_path: None, search_pattern: None, fps, use_network, async, } ) }, Ok(_) => Err("TOML file has invalid structure. Expected a Table as the top-level element.".to_string()), Err(e) => Err(format!("{}", e)), } } fn from_toml_file>(path: P) -> Result { let mut file = File::open(path).map_err(|e| format!("{}", e))?; let mut content = String::new(); file.read_to_string(&mut content).map_err(|e| format!("{}", e))?; Config::from_toml_str(&content) } fn add_tile_sources_from_str(&mut self, toml_str: &str) -> Result<(), String> { match toml_str.parse::() { Ok(Value::Table(ref table)) => { let sources_array = table.get("tile_sources") .ok_or_else(|| "missing \"tile_sources\" table".to_string())? .as_array() .ok_or_else(|| "\"tile_sources\" has to be an array.".to_string())?; for (id, source) in sources_array.iter().enumerate() { let name = source.get("name") .ok_or_else(|| "tile_source is missing \"name\" entry.".to_string())? .as_str() .ok_or_else(|| "\"name\" has to be a string".to_string())?; let min_zoom = source.get("min_zoom") .unwrap_or_else(|| &Value::Integer(0)) .as_integer() .ok_or_else(|| "min_zoom has to be an integer".to_string()) .and_then(|m| { if m < 0 || m > 30 { Err(format!("min_zoom = {} is out of bounds, has to be in interval [0, 30]", m)) } else { Ok(m) } })?; let max_zoom = source.get("max_zoom") .ok_or_else(|| format!("source {:?} is missing \"max_zoom\" entry", name))? .as_integer() .ok_or_else(|| "max_zoom has to be an integer".to_string()) .and_then(|m| { if m < 0 || m > 30 { Err(format!("max_zoom = {} is out of bounds, has to be in interval [0, 30]", m)) } else { Ok(m) } })?; if min_zoom > max_zoom { warn!("min_zoom ({}) and max_zoom ({}) allow no valid tiles", min_zoom, max_zoom); } else if min_zoom == max_zoom { warn!("min_zoom ({}) and max_zoom ({}) allow only one zoom level", min_zoom, max_zoom); } let url_template = source.get("url_template") .ok_or_else(|| format!("source {:?} is missing \"url_template\" entry", name))? .as_str() .ok_or_else(|| "url_template has to be a string".to_string())?; let extension = source.get("extension") .ok_or_else(|| format!("source {:?} is missing \"extension\" entry", name))? .as_str() .ok_or_else(|| "extension has to be a string".to_string())?; //TODO reduce allowed strings to a reasonable subset of valid UTF-8 strings // that can also be used as a directory name or introduce a dir_name key with // more restrictions. if name.contains('/') || name.contains('\\') { return Err(format!("source name ({:?}) must not contain slashes (\"/\" or \"\\\")", name)); } let mut path = PathBuf::from(&self.tile_cache_dir); path.push(name); self.sources.push(( name.to_string(), TileSource::new( id as u32, url_template.to_string(), path, extension.to_string(), min_zoom as u32, max_zoom as u32, )?, )); } Ok(()) }, Ok(_) => Err("TOML file has invalid structure. Expected a Table as the top-level element.".to_string()), Err(e) => Err(format!("{}", e)), } } fn add_tile_sources_from_file>(&mut self, path: P) -> Result<(), String> { let mut file = File::open(path).map_err(|e| format!("{}", e))?; let mut content = String::new(); file.read_to_string(&mut content).map_err(|e| format!("{}", e))?; self.add_tile_sources_from_str(&content) } pub fn tile_sources(&self) -> &[(String, TileSource)] { &self.sources } pub fn pbf_path(&self) -> Option<&Path> { self.pbf_path.as_ref().map(|p| p.as_path()) } pub fn search_pattern(&self) -> Option<&str> { self.search_pattern.as_ref().map(|s| s.as_str()) } pub fn fps(&self) -> f64 { self.fps } pub fn use_network(&self) -> bool { self.use_network } pub fn async(&self) -> bool { self.async } } #[cfg(test)] mod tests { use config::*; #[test] fn default_config() { let mut config = Config::from_toml_str(DEFAULT_CONFIG).unwrap(); config.add_tile_sources_from_str(DEFAULT_TILE_SOURCES).unwrap(); } }