cifra/attack/
database.rs

1/// Cifra database definition.
2use diesel::prelude::*;
3use diesel::sqlite::SqliteConnection;
4use dotenv::dotenv;
5use std::env;
6use std::ffi::{OsStr, OsString};
7use std::fs;
8use std::path::{Path, PathBuf};
9// use std::env::VarError;
10
11use crate::{ErrorKind, Result, ResultExt, Error};
12use crate::schema::languages;
13// use crate::schema::languages::dsl::*;
14use crate::schema::words;
15// use crate::schema::words::dsl::*;
16// use std::fmt::Error;
17
18embed_migrations!("migrations/");
19
20pub type DatabaseSession = SqliteConnection;
21
22pub const DATABASE_ENV_VAR: &'static str = "DATABASE_URL";
23// const DATABASE_STANDARD_PATH: &'static str = ".cifra/cifra_database.sqlite";
24// Database is going to be stored in this path that is relative to user home folder.
25const DATABASE_STANDARD_RELATIVE_PATH: &'static str = ".cifra/cifra_database.sqlite";
26
27/// This type provides a dynamically built path to database folder.
28///
29/// Just create an instance of this type and use it whenever a PathBuf, String or OsString with
30/// database path is expected. This type will automatically convert to those three types.
31struct DATABASE_STANDARD_PATH;
32
33impl DATABASE_STANDARD_PATH{
34    fn get_database_standard_path()-> PathBuf{
35        let mut path: PathBuf = dirs::home_dir().expect("No home user found to place database file.");
36        path.push(PathBuf::from(DATABASE_STANDARD_RELATIVE_PATH));
37        path
38    }
39}
40
41impl From<DATABASE_STANDARD_PATH> for String {
42    fn from(_: DATABASE_STANDARD_PATH) -> Self {
43        let path = DATABASE_STANDARD_PATH::get_database_standard_path();
44        String::from(path.to_str().unwrap())
45    }
46}
47
48impl From<DATABASE_STANDARD_PATH> for PathBuf {
49    fn from(_: DATABASE_STANDARD_PATH) -> Self {
50        DATABASE_STANDARD_PATH::get_database_standard_path()
51    }
52}
53
54impl From<DATABASE_STANDARD_PATH> for OsString {
55    fn from(_: DATABASE_STANDARD_PATH) -> Self {
56        let path = DATABASE_STANDARD_PATH::get_database_standard_path();
57        path.into_os_string()
58    }
59}
60
61/// Check if DATABASE_URL environment variable actually exists and create it if not.
62///
63/// At tests a .env file is used to shadow default DATABASE_URL var. But at production
64/// that environment variable must be set to let cifra find its database. If this
65/// function does not find DATABASE_URL then it creates that var and populates it
66/// with default value stored at *DATABASE_STANDARD_PATH*, but notifies that situations
67/// returning a VarError.
68///
69/// # Returns:
70/// * Environment value if DEFAULT_URL exists and a VarError if not.
71fn check_database_url_env_var_exists()-> Result<String>{
72    return env::var(DATABASE_ENV_VAR)
73        .chain_err (|| {
74            ErrorKind::DatabaseError(String::from("Error finding out if database env var existed previously."))
75        })
76}
77
78/// Create and populate database with its default tables.
79pub fn create_database()-> Result<Database> {
80    let database = Database::new()?;
81    embedded_migrations::run(&database.session)
82        .chain_err(|| ErrorKind::DatabaseError(String::from("Error running database migrations.")))?;
83    Ok(database)
84}
85
86/// Take a path and create all folders that don't actually exists yet.
87fn create_folder_path(path: &Path) -> Result<()>{
88    if let Ok(()) = fs::create_dir_all(path) {
89        Ok(())
90    } else {
91        let path_string = path.as_os_str();
92        bail!(String::from(path_string.to_str().unwrap()))
93    }
94}
95
96pub struct Database {
97    pub session: DatabaseSession,
98    database_path: String
99}
100
101impl Database {
102
103    /// Create a new Database type.
104    ///
105    /// At creation it checks if DATABASE_URL environment variable actually exists and
106    /// create it if not.
107    ///
108    /// At tests a .env file is used to shadow default DATABASE_URL var. But at production
109    /// that environment variable must be set to let cifra find its database. If this
110    /// function does not find DATABASE_URL then it creates that var and populates it
111    /// with default value stored at *DATABASE_STANDARD_PATH*
112    ///
113    /// If DATABASE_URL was not set that is a signal about database does not exist yet so
114    /// its created too.
115    pub fn new() -> Result<Self> {
116        if let Ok(database_path) = check_database_url_env_var_exists() {
117            // Database already exists.
118            Ok(Database {
119                session: Self::open_session()?,
120                database_path
121            })
122        } else {
123            // Database does not exists yet. So we must create it.
124            let database_path: OsString = DATABASE_STANDARD_PATH.into();
125            env::set_var(DATABASE_ENV_VAR, database_path.as_os_str());
126            let database_path = PathBuf::from(DATABASE_STANDARD_PATH);
127            let database_folder = database_path.parent()
128                .chain_err(|| ErrorKind::FolderError(String::from(database_path.as_os_str().to_str().unwrap())))?;
129            create_folder_path(database_folder);
130            create_database()
131        }
132    }
133
134    /// Connect to current dictionaries database.
135    ///
136    /// Returns:
137    /// A connection to underlying database.
138    fn open_session() -> Result<DatabaseSession> {
139        dotenv().ok();
140        let database_url = env::var("DATABASE_URL")
141            .chain_err(|| ErrorKind::DatabaseError(String::from("DATABASE_URL must be set")))?;
142        SqliteConnection::establish(&database_url)
143            .chain_err(|| ErrorKind::DatabaseError(format!("Error connecting to DATABASE_URL: {}", database_url)))
144    }
145}
146
147/// Model for Languages database table.
148#[derive(Queryable, Identifiable, Associations, Debug, PartialEq)]
149#[table_name="languages"]
150// #[has_many(words)]
151pub struct Language {
152    pub id: i32,
153    pub language: String,
154}
155
156#[derive(Insertable)]
157#[table_name="languages"]
158pub struct NewLanguage<'a> {
159    pub language: &'a str,
160}
161
162
163/// Model for Words database table.
164#[derive(Queryable, Identifiable, Associations, Debug, PartialEq)]
165#[table_name="words"]
166#[belongs_to(Language)]
167pub struct Word {
168    pub id: i32,
169    pub word: String,
170    pub word_pattern: String,
171    pub language_id: i32
172}
173
174#[derive(Insertable)]
175#[table_name="words"]
176pub struct NewWord<'a> {
177    pub word: &'a str,
178    pub word_pattern: String,
179    pub language_id: i32
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185    use std::ffi::OsStr;
186    use std::path::Path;
187    use test_common::fs::tmp::TestEnvironment;
188    use test_common::system::env::TemporalEnvironmentVariable;
189
190    #[test]
191    fn test_create_database() {
192        let test_folder = TestEnvironment::new();
193        let absolute_path_to_database = test_folder.path().join("cifra_database.sqlite");
194        let absolute_pathname_to_database = match absolute_path_to_database.to_str() {
195            Some(path)=> path,
196            None=> panic!("Path uses non valid characters.")
197        };
198        // Database does not exists yet.
199        let database_path = Path::new(absolute_pathname_to_database);
200        assert!(!database_path.exists());
201        let test_env = TemporalEnvironmentVariable::new("DATABASE_URL", absolute_pathname_to_database);
202        create_database();
203        // Database now exists.
204        assert!(database_path.exists());
205    }
206
207    #[test]
208    fn test_create_database_path() {
209        let test_folder = TestEnvironment::new();
210        let absolute_path_to_database = test_folder.path().join(".cifra/parent1/parent2/cifra_database.sqlite");
211        let database_folder = absolute_path_to_database.parent().unwrap();
212        assert!(!database_folder.exists());
213        create_folder_path(database_folder);
214        assert!(database_folder.exists());
215    }
216
217    #[test]
218    fn test_database_standard_path() {
219        let mut expected_database_folder = dirs::home_dir().unwrap();
220        expected_database_folder.push(DATABASE_STANDARD_RELATIVE_PATH);
221        assert_eq!(expected_database_folder.into_os_string(), OsString::from(DATABASE_STANDARD_PATH));
222    }
223
224
225}