From 8882712925341cf66132a13ec5d185aca8c7ce8a Mon Sep 17 00:00:00 2001 From: Adrian Heine Date: Sun, 14 May 2017 22:37:35 +0200 Subject: [PATCH] Update --- Cargo.toml | 1 + src/lib.rs | 1 + src/storage.rs | 43 +++++++++ src/symbols/if_already_present.rs | 136 +++++++++++++++++++++++++++ src/symbols/mariadb/database.rs | 6 +- src/symbols/mariadb/database_dump.rs | 55 +++++++++++ src/symbols/mariadb/mod.rs | 2 + src/symbols/mod.rs | 1 + tests/storage.rs | 104 ++++++++++++++++++++ 9 files changed, 346 insertions(+), 3 deletions(-) create mode 100644 src/storage.rs create mode 100644 src/symbols/if_already_present.rs create mode 100644 src/symbols/mariadb/database_dump.rs create mode 100644 tests/storage.rs diff --git a/Cargo.toml b/Cargo.toml index fa16817..ee3f6ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,3 +8,4 @@ users = "0.5.0" [dev-dependencies] tempdir = "0.3" +regex = "0.2" diff --git a/src/lib.rs b/src/lib.rs index 58bbdc8..2244e97 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28,3 +28,4 @@ pub mod symbols; pub mod schema; pub mod repository; pub mod resources; +pub mod storage; diff --git a/src/storage.rs b/src/storage.rs new file mode 100644 index 0000000..9987136 --- /dev/null +++ b/src/storage.rs @@ -0,0 +1,43 @@ +use std::error::Error; +use std::fs::read_dir; +use std::str::FromStr; +use std::time::{SystemTime, UNIX_EPOCH}; + +pub trait Storage { + fn write_filename(&self) -> String; + fn read_filename(&self) -> Result>; + fn recent_date(&self) -> Result>; +} + +pub struct SimpleStorage(String, String); + +impl SimpleStorage { + pub fn new(base: String, filename: String) -> Self { + SimpleStorage(base, filename) + } + + fn get_path(&self, date: Option) -> String { + match date { + Some(d) => format!("{}/_{}/{}", self.0, self.1, d), + None => format!("{}/_{}", self.0, self.1) + } + } +} + +impl Storage for SimpleStorage { + fn write_filename(&self) -> String { + self.get_path(Some(SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs())) + } + + fn read_filename(&self) -> Result> { + Ok(self.get_path(Some(try!(self.recent_date())))) + } + + fn recent_date(&self) -> Result> { + let dir = self.get_path(None); + try!(read_dir(dir)) + .map(|entry| entry.ok().and_then(|e| e.file_name().into_string().ok()).and_then(|filename| u64::from_str(&filename).ok())) + .fold(None, |maybe_newest, maybe_time| maybe_newest.into_iter().chain(maybe_time).max()) + .ok_or("Not found".to_string().into()) + } +} diff --git a/src/symbols/if_already_present.rs b/src/symbols/if_already_present.rs new file mode 100644 index 0000000..755b6fa --- /dev/null +++ b/src/symbols/if_already_present.rs @@ -0,0 +1,136 @@ +use std::error::Error; +use std::fmt; + +use resources::Resource; +use symbols::Symbol; + +pub struct IfAlreadyPresent where A: Symbol, B: Symbol { + a: A, + b: B +} + +impl IfAlreadyPresent where A: Symbol, B: Symbol { + pub fn new(a: A, b: B) -> Self { + IfAlreadyPresent { a: a, b: b } + } +} + +impl Symbol for IfAlreadyPresent where A: Symbol, B: Symbol { + fn target_reached(&self) -> Result> { + self.a.target_reached().and_then(|reached| if reached { self.b.target_reached() } else { Ok(reached) }) + } + + fn execute(&self) -> Result<(), Box> { + // Execute a & b if a is not reached + // Execute b if a was reached and b isn't + if !try!(self.a.target_reached()) { + try!(self.a.execute()); + self.b.execute() + } else if !try!(self.b.target_reached()) { + self.b.execute() + } else { + Ok(()) + } + } + + fn get_prerequisites(&self) -> Vec { + let mut r = vec![]; + r.extend(self.a.get_prerequisites().into_iter()); + r.extend(self.b.get_prerequisites().into_iter()); + r + } + + fn provides(&self) -> Option> { + let mut r = vec![]; + if let Some(provides) = self.a.provides() { + r.extend(provides.into_iter()); + } + if let Some(provides) = self.b.provides() { + r.extend(provides.into_iter()); + } + if r.len() > 0 { Some(r) } else { None } + } +} + +impl fmt::Display for IfAlreadyPresent where A: Symbol, B: Symbol { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error>{ + write!(f, "IfAlreadyPresent {} and then {}", self.a, self.b) + } +} + +#[cfg(test)] +mod test { + use std::error::Error; + use std::fmt; + + use symbols::Symbol; + use symbols::if_already_present::IfAlreadyPresent; + + struct ErrSymbol(String); + impl Symbol for ErrSymbol { + fn target_reached(&self) -> Result> { Err(self.0.clone().into()) } + fn execute(&self) -> Result<(), Box> { Err(self.0.clone().into()) } + } + impl fmt::Display for ErrSymbol { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error>{ write!(f, "") } } + + struct OkSymbol(bool); + impl Symbol for OkSymbol { + fn target_reached(&self) -> Result> { Ok(self.0) } + fn execute(&self) -> Result<(), Box> { Ok(()) } + } + impl fmt::Display for OkSymbol { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error>{ write!(f, "") } } + + #[test] + fn first_target_reached_fails() { + let res = IfAlreadyPresent::new(ErrSymbol("first".into()), ErrSymbol("second".into())).target_reached(); + assert_eq!(res.unwrap_err().description(), "first"); + } + + #[test] + fn first_target_not_reached() { + let res = IfAlreadyPresent::new(OkSymbol(false), ErrSymbol("second".into())).target_reached(); + assert_eq!(res.unwrap(), false); + } + + #[test] + fn second_target_reached_fails() { + let res = IfAlreadyPresent::new(OkSymbol(true), ErrSymbol("second".into())).target_reached(); + assert_eq!(res.unwrap_err().description(), "second"); + } + + #[test] + fn second_target_not_reached() { + let res = IfAlreadyPresent::new(OkSymbol(true), OkSymbol(false)).target_reached(); + assert_eq!(res.unwrap(), false); + } + + #[test] + fn everything_reached() { + let res = IfAlreadyPresent::new(OkSymbol(true), OkSymbol(true)).target_reached(); + assert_eq!(res.unwrap(), true); + } + + #[test] + fn first_execute_fails() { + let res = IfAlreadyPresent::new(ErrSymbol("first".into()), ErrSymbol("second".into())).execute(); + assert_eq!(res.unwrap_err().description(), "first"); + } + + #[test] + fn second_execute_fails_but_isnt_run() { + let res = IfAlreadyPresent::new(OkSymbol(false), ErrSymbol("second".into())).execute(); + assert_eq!(res.unwrap(), ()); + } + + #[test] + fn second_execute_fails() { + let res = IfAlreadyPresent::new(OkSymbol(true), ErrSymbol("second".into())).execute(); + assert_eq!(res.unwrap_err().description(), "second"); + } + + #[test] + fn everything_executes() { + let res = IfAlreadyPresent::new(OkSymbol(true), OkSymbol(true)).execute(); + assert_eq!(res.unwrap(), ()); + } +} diff --git a/src/symbols/mariadb/database.rs b/src/symbols/mariadb/database.rs index cb5dcff..a6b7a3d 100644 --- a/src/symbols/mariadb/database.rs +++ b/src/symbols/mariadb/database.rs @@ -7,15 +7,15 @@ use symbols::Symbol; pub struct MariaDBDatabase<'a> { db_name: Cow<'a, str>, - seed_file: &'a str, + seed_file: Cow<'a, str>, command_runner: &'a CommandRunner } impl<'a> MariaDBDatabase<'a> { - pub fn new(db_name: Cow<'a, str>, command_runner: &'a CommandRunner) -> MariaDBDatabase<'a> { + pub fn new(db_name: Cow<'a, str>, seed_file: Cow<'a, str>, command_runner: &'a CommandRunner) -> MariaDBDatabase<'a> { MariaDBDatabase { db_name: db_name, - seed_file: "/root/seedfile.sql", + seed_file: seed_file, command_runner: command_runner } } diff --git a/src/symbols/mariadb/database_dump.rs b/src/symbols/mariadb/database_dump.rs new file mode 100644 index 0000000..4dc4fb0 --- /dev/null +++ b/src/symbols/mariadb/database_dump.rs @@ -0,0 +1,55 @@ +use std::borrow::Cow; +use std::error::Error; +use std::fmt; +use std::str::FromStr; + +use command_runner::CommandRunner; +use symbols::Symbol; +use storage::Storage; + +pub struct DatabaseDump<'a, S> where S: Storage { + db_name: Cow<'a, str>, + storage: S, + command_runner: &'a CommandRunner +} + +impl<'a, S> DatabaseDump<'a, S> where S: Storage { + pub fn new(db_name: Cow<'a, str>, storage: S, command_runner: &'a CommandRunner) -> Self { + DatabaseDump { + db_name: db_name, + storage: storage, + command_runner: command_runner + } + } + + fn run_sql(&self, sql: &str) -> Result> { + let output = try!(self.command_runner.run_with_args("mariadb", &["--skip-column-names", "-B", "-e", sql])); + if output.status.code() != Some(0) { + return Err(try!(String::from_utf8(output.stderr)).into()); + } + Ok(try!(String::from_utf8(output.stdout))) + } +} + +impl<'a, S> fmt::Display for DatabaseDump<'a, S> where S: Storage { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "MariaDB Database {}", self.db_name) + } +} + +impl<'a, S> Symbol for DatabaseDump<'a, S> where S: Storage { + fn target_reached(&self) -> Result> { + let dump_date = try!(self.storage.recent_date()); + let modified_date = try!(self.run_sql(&format!("select UNIX_TIMESTAMP(MAX(UPDATE_TIME)) from information_schema.tables WHERE table_schema = '{}'", self.db_name))); + Ok(try!(u64::from_str(modified_date.trim_right())) <= dump_date) + } + + fn execute(&self) -> Result<(), Box> { + try!(self.command_runner.run_with_args("sh", &["-c", &format!("mysqldump '{}' > {}", self.db_name, self.storage.write_filename())])); + Ok(()) + } +} + +#[cfg(test)] +mod test { +} diff --git a/src/symbols/mariadb/mod.rs b/src/symbols/mariadb/mod.rs index 7b6623c..30077f2 100644 --- a/src/symbols/mariadb/mod.rs +++ b/src/symbols/mariadb/mod.rs @@ -1,5 +1,7 @@ mod database; mod user; +mod database_dump; pub use self::database::MariaDBDatabase; +pub use self::database_dump::DatabaseDump; pub use self::user::MariaDBUser; diff --git a/src/symbols/mod.rs b/src/symbols/mod.rs index 13776da..7cb0313 100644 --- a/src/symbols/mod.rs +++ b/src/symbols/mod.rs @@ -18,6 +18,7 @@ pub mod dir; pub mod file; pub mod git; pub mod hook; +pub mod if_already_present; pub mod list; pub mod mariadb; pub mod nginx; diff --git a/tests/storage.rs b/tests/storage.rs new file mode 100644 index 0000000..eba8c0b --- /dev/null +++ b/tests/storage.rs @@ -0,0 +1,104 @@ +extern crate regex; +extern crate schematics; +extern crate tempdir; + +use regex::Regex; +use std::fs::{create_dir, File}; +use std::path::Path; +use tempdir::TempDir; +use schematics::storage::{SimpleStorage, Storage}; + +fn get_dir<'a, I: IntoIterator>(content: I) -> TempDir { + let tmp_dir = TempDir::new("unittest").expect("create temp dir"); + let storage_path = tmp_dir.path().join("_filename"); + create_dir(storage_path.clone()).unwrap(); + for path in content { + let file_path = storage_path.join(path); + File::create(file_path).expect("create temp file"); + } + tmp_dir +} + +fn get_storage(path: &Path) -> SimpleStorage { + SimpleStorage::new(path.to_str().unwrap().into(), "filename".into()) +} + +// Normal cases + +#[test] +fn single_file() { + let dir = get_dir(&["12345"]); + let storage = get_storage(dir.path()); + + assert!(Regex::new(&format!("^{}/_filename/\\d+$", dir.path().display())).unwrap().is_match(&storage.write_filename())); + assert_eq!(dir.path().join("_filename").join("12345"), Path::new(&storage.read_filename().unwrap())); + assert_eq!(storage.recent_date().unwrap(), 12345); +} + +#[test] +fn two_files() { + let dir = get_dir(&["12345", "23456"]); + let storage = get_storage(dir.path()); + + assert!(Regex::new(&format!("^{}/_filename/\\d+$", dir.path().display())).unwrap().is_match(&storage.write_filename())); + assert_eq!(dir.path().join("_filename").join("23456"), Path::new(&storage.read_filename().unwrap())); + assert_eq!(storage.recent_date().unwrap(), 23456); +} + +#[test] +fn another_two_files() { + let dir = get_dir(&["23456", "12345"]); + let storage = get_storage(dir.path()); + + assert!(Regex::new(&format!("^{}/_filename/\\d+$", dir.path().display())).unwrap().is_match(&storage.write_filename())); + assert_eq!(dir.path().join("_filename").join("23456"), Path::new(&storage.read_filename().unwrap())); + assert_eq!(storage.recent_date().unwrap(), 23456); +} + +#[test] +fn three_files() { + let dir = get_dir(&["23456", "9", "12345"]); + let storage = get_storage(dir.path()); + + assert!(Regex::new(&format!("^{}/_filename/\\d+$", dir.path().display())).unwrap().is_match(&storage.write_filename())); + assert_eq!(dir.path().join("_filename").join("23456"), Path::new(&storage.read_filename().unwrap())); + assert_eq!(storage.recent_date().unwrap(), 23456); +} + +// Bad cases + +#[test] +fn empty_storage() { + let dir = TempDir::new("unittest").expect("create temp dir"); + let storage = get_storage(dir.path()); + + assert!(Regex::new(&format!("^{}/_filename/\\d+$", dir.path().display())).unwrap().is_match(&storage.write_filename())); + assert!(storage.read_filename().is_err()); + assert_eq!(storage.read_filename().unwrap_err().description(), "entity not found"); + assert!(storage.recent_date().is_err()); + assert_eq!(storage.recent_date().unwrap_err().description(), "entity not found"); +} + +#[test] +fn empty_storage_for_filename() { + let dir = get_dir(&[]); + let storage = get_storage(dir.path()); + + assert!(Regex::new(&format!("^{}/_filename/\\d+$", dir.path().display())).unwrap().is_match(&storage.write_filename())); + assert!(storage.read_filename().is_err()); + assert_eq!(storage.read_filename().unwrap_err().description(), "Not found"); + assert!(storage.recent_date().is_err()); + assert_eq!(storage.recent_date().unwrap_err().description(), "Not found"); +} + +#[test] +fn bad_file() { + let dir = get_dir(&["abba"]); + let storage = get_storage(dir.path()); + + assert!(Regex::new(&format!("^{}/_filename/\\d+$", dir.path().display())).unwrap().is_match(&storage.write_filename())); + assert!(storage.read_filename().is_err()); + assert_eq!(storage.read_filename().unwrap_err().description(), "Not found"); + assert!(storage.recent_date().is_err()); + assert_eq!(storage.recent_date().unwrap_err().description(), "Not found"); +}