From 8d3ac4fc46bbcf7f04892a21db43a772f3158cab Mon Sep 17 00:00:00 2001 From: Augusto Gunsch Date: Sat, 22 Jan 2022 21:58:36 -0300 Subject: [PATCH] Add languages --- src/database.rs | 119 +++++++++++++++++++++++++++++++++------------- src/language.rs | 63 ++++++++++++++++++++++-- src/main.rs | 57 ++++++++++++++++------ src/util.rs | 12 +++++ src/version.rs | 36 ++++++++++++++ src/views.rs | 28 +++++------ static/index.html | 2 +- static/index.js | 12 +++-- 8 files changed, 258 insertions(+), 71 deletions(-) create mode 100644 src/util.rs create mode 100644 src/version.rs diff --git a/src/database.rs b/src/database.rs index ae137d4..f6404db 100644 --- a/src/database.rs +++ b/src/database.rs @@ -1,7 +1,6 @@ use std::fs; -use std::io::ErrorKind; -use std::process::exit; use std::collections::HashSet; +use std::fmt; use reqwest; use rusqlite::{Connection, Transaction, ErrorCode}; @@ -14,29 +13,81 @@ use serde_json; use crate::language::Language; use crate::entry::{WiktionaryEntries, WiktionaryEntry}; use crate::entry::Form; -use crate::{MAJOR, MINOR, PATCH}; +use crate::{DB_DIR, CACHE_DIR, MAJOR, MINOR, PATCH}; +use crate::util; -const DB_DIR: &str = "/usr/share/inflectived/"; -const CACHE_DIR: &str = "/var/cache/"; +pub enum DbError { + AccessDenied, +} + +impl fmt::Display for DbError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + DbError::AccessDenied => write!(f, "Access denied"), + } + } +} /// A database of Wiktionary entries pub struct WordDb { - db_path: String + db_path: String, + pub installed_langs: Vec, + pub installable_langs: Vec } impl WordDb { pub fn new(db_name: &str) -> Self { - let mut db_path = String::from(DB_DIR); - db_path.push_str(db_name); + let db_path = format!("{}/{}", DB_DIR, db_name); - Self { db_path } + let conn = Connection::open(&db_path).unwrap(); + + let mut statement = conn.prepare( + "SELECT code, name, major, minor, patch + FROM langs" + ).unwrap(); + + let mut rows = statement.query([]).unwrap(); + + let mut installed_langs: Vec = Vec::new(); + + while let Some(row) = rows.next().unwrap() { + installed_langs.push(Language::from_row(&row)); + } + + installed_langs.sort(); + + Self { + db_path, + installed_langs, + installable_langs: Language::list_langs(), + } } pub fn connect(&self) -> Connection { Connection::open(&self.db_path).unwrap() } - pub fn clean_tables(&mut self, lang: &Language) { + pub fn list_available(&self) -> String { + let mut list = String::new(); + + for lang in &self.installable_langs { + list.push_str(&format!(" - {} ({})\n", &lang.name, &lang.code)); + } + + list + } + + pub fn get_lang(&self, code: &str) -> Option { + for lang in &self.installable_langs { + if lang.code == code { + return Some(lang.clone()) + } + } + + None + } + + pub fn clean_tables(&mut self, lang: &Language) -> Result<(), DbError> { let mut conn = self.connect(); let transaction = conn.transaction().unwrap(); @@ -51,10 +102,8 @@ impl WordDb { )", []) { match e { SqliteFailure(f, _) => match f.code { - ErrorCode::ReadOnly => { - eprintln!("Could not write to database: Permission denied"); - eprintln!("Please run as root"); - exit(1); + ErrorCode::ReadOnly => { + return Err(DbError::AccessDenied) }, _ => panic!("{}", e) }, @@ -62,7 +111,18 @@ impl WordDb { } } - transaction.execute("DELETE FROM langs WHERE code = ?", [&lang.code]).unwrap(); + if let Err(e) = transaction.execute( + "DELETE FROM langs WHERE code = ?", [&lang.code]) { + match e { + SqliteFailure(f, _) => match f.code { + ErrorCode::ReadOnly => { + return Err(DbError::AccessDenied) + }, + _ => panic!("{}", e) + }, + _ => panic!("{}", e) + } + }; transaction.execute(&format!("DROP TABLE IF EXISTS {0}_words", &lang.code), []).unwrap(); transaction.execute(&format!("DROP TABLE IF EXISTS {0}_types", &lang.code), []).unwrap(); @@ -89,6 +149,8 @@ impl WordDb { ", &lang.code), []).unwrap(); transaction.commit().unwrap(); + + Ok(()) } pub fn insert_entry(&self, transaction: &Transaction, lang: &Language, entry: &WiktionaryEntry) { @@ -194,16 +256,6 @@ impl WordDb { transaction.commit().unwrap(); } - fn try_create_dir(&self, dir: &str) { - match fs::create_dir(dir) { - Err(e) => match e.kind() { - ErrorKind::AlreadyExists => {}, - _ => panic!("{}", e) - }, - _ => {} - } - } - fn insert_types(&mut self, lang: &Language, entries: &WiktionaryEntries) { let mut conn = self.connect(); let transaction = conn.transaction().unwrap(); @@ -235,22 +287,23 @@ impl WordDb { transaction.commit().unwrap(); } - pub async fn upgrade_lang(&mut self, lang: &Language) { - self.try_create_dir(DB_DIR); + pub async fn upgrade_lang(&mut self, lang: &Language) -> Result<(), DbError> { + util::try_create_dir(DB_DIR); println!("Trying to read cached data..."); - let mut cache_file = String::from(CACHE_DIR); - cache_file.push_str("Polish.json"); + let cache_file = format!("{}/{}.json", CACHE_DIR, &lang.name); let cached_data = fs::read_to_string(&cache_file); let mut request = None; if let Err(_) = cached_data { - request = Some(reqwest::get("https://kaikki.org/dictionary/Polish/kaikki.org-dictionary-Polish.json")); + let url = format!("https://kaikki.org/dictionary/{0}/kaikki.org-dictionary-{0}.json", + &lang.name); + request = Some(reqwest::get(url)); } println!("Cleaning tables..."); - self.clean_tables(lang); + self.clean_tables(lang)?; let data; if let Some(request) = request { @@ -259,7 +312,7 @@ impl WordDb { data = request.await.unwrap().text().await.unwrap(); if cfg!(unix) { println!("Caching data..."); - self.try_create_dir(CACHE_DIR); + util::try_create_dir(CACHE_DIR); fs::write(&cache_file, &data).unwrap(); } } @@ -283,5 +336,7 @@ impl WordDb { self.insert_version(lang); println!("Done"); + + Ok(()) } } diff --git a/src/language.rs b/src/language.rs index 9c61853..6286eb4 100644 --- a/src/language.rs +++ b/src/language.rs @@ -1,14 +1,69 @@ -#[derive(Debug)] +use std::cmp::{PartialEq, PartialOrd, Ordering}; + +use rusqlite::Row; +use serde::Serialize; + +use crate::version::Version; + +#[derive(Serialize, Debug, Clone)] pub struct Language { - pub code: String, - pub name: String + pub code: String, // ISO 639-2 + pub name: String, // English name + pub version: Option } impl Language { pub fn new(code: &str, name: &str) -> Self { Self { code: String::from(code), - name: String::from(name) + name: String::from(name), + version: None } } + + pub fn from_row(row: &Row) -> Self { + Self { + code: row.get(0).unwrap(), + name: row.get(1).unwrap(), + version: Some(Version(row.get(2).unwrap(), + row.get(3).unwrap(), + row.get(4).unwrap())) + } + } + + pub fn list_langs() -> Vec { + // Keep this list sorted by name + let langs = vec![ + Self::new("eng", "English"), + Self::new("fre", "French"), + Self::new("ger", "German"), + Self::new("ita", "Italian"), + Self::new("pol", "Polish"), + Self::new("por", "Portuguese"), + Self::new("rus", "Russian"), + Self::new("spa", "Spanish"), + ]; + + langs + } +} + +impl PartialEq for Language { + fn eq(&self, other: &Self) -> bool { + self.name == other.name + } +} + +impl Eq for Language {} + +impl Ord for Language { + fn cmp(&self, other: &Self) -> Ordering { + self.name.cmp(&other.name) + } +} + +impl PartialOrd for Language { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.name.cmp(&other.name)) + } } diff --git a/src/main.rs b/src/main.rs index 29c88fe..c64ca01 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,18 +1,26 @@ -#![feature(slice_group_by)] +#![feature(slice_group_by, path_try_exists)] + +use std::process::exit; +use std::fs; //mod database; use rocket::routes; use rocket::fs::FileServer; use clap::{App, AppSettings, Arg, SubCommand}; + //use database::WordDb; mod database; mod language; mod entry; mod views; +mod version; +mod util; -use database::WordDb; -use language::Language; +use database::{WordDb, DbError}; +const DB_DIR: &str = "/usr/share/inflectived"; +const CACHE_DIR: &str = "/var/cache/inflectived"; +const FRONTEND_DIR: &str = "/opt/inflectived"; const MAJOR: i32 = 0; const MINOR: i32 = 1; @@ -56,23 +64,42 @@ async fn main() { let mut db = WordDb::new("inflectived.db"); - let lang = Language::new("pl", "Polish"); - match matches.subcommand() { - ("upgrade", _) => { db.upgrade_lang(&lang).await; }, + ("upgrade", matches) => { + let lang = db.get_lang(matches.unwrap().value_of("LANG").unwrap()); + + if let None = lang { + eprintln!("The requested language is not available."); + eprintln!("Available languages:"); + eprint!("{}", db.list_available()); + exit(1); + } + + if let Err(e) = db.upgrade_lang(&lang.unwrap()).await { + match e { + DbError::AccessDenied => { + eprintln!("Permission denied. Please run as root."); + exit(1); + } + } + } + }, ("run", _) => { let figment = rocket::Config::figment() .merge(("address", "0.0.0.0")); - rocket::custom(figment) - .manage(db) - .mount("/static", FileServer::from("static/")) - .mount("/", routes![views::get_entries, - views::get_entries_like, - views::get_langs, - views::frontend]) - .launch() - .await.unwrap(); + let mut app = rocket::custom(figment) + .manage(db) + .mount("/", routes![views::get_entries, + views::get_entries_like, + views::get_langs, + views::frontend]); + + if let Ok(true) = fs::try_exists(FRONTEND_DIR) { + app = app.mount("/static", FileServer::from(FRONTEND_DIR)); + } + + app.launch().await.unwrap(); }, _ => {} } diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..8ecacf6 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,12 @@ +use std::fs; +use std::io::ErrorKind; + +pub fn try_create_dir(dir: &str) { + match fs::create_dir(dir) { + Err(e) => match e.kind() { + ErrorKind::AlreadyExists => {}, + _ => panic!("{}", e) + }, + _ => {} + } +} diff --git a/src/version.rs b/src/version.rs new file mode 100644 index 0000000..013a68c --- /dev/null +++ b/src/version.rs @@ -0,0 +1,36 @@ +use std::cmp::{PartialEq, PartialOrd, Ordering}; + +use serde::Serialize; + +#[derive(Serialize, Debug, Clone, PartialEq)] +pub struct Version(pub u32, pub u32, pub u32); + +impl PartialOrd for Version { + fn partial_cmp(&self, other: &Self) -> Option { + if self.0 > other.0 { + return Some(Ordering::Greater); + } + + if self.0 < other.0 { + return Some(Ordering::Less); + } + + if self.1 > other.1 { + return Some(Ordering::Greater); + } + + if self.1 < other.1 { + return Some(Ordering::Less); + } + + if self.2 > other.2 { + return Some(Ordering::Greater); + } + + if self.2 < other.2 { + return Some(Ordering::Less); + } + + Some(Ordering::Equal) + } +} diff --git a/src/views.rs b/src/views.rs index 69024d0..f343552 100644 --- a/src/views.rs +++ b/src/views.rs @@ -8,12 +8,14 @@ use rocket::serde::json::Json; use rusqlite::params; use crate::database::WordDb; +use crate::language::Language; +use crate::FRONTEND_DIR; #[get("/")] -pub fn frontend() -> Option> { - match fs::read_to_string("static/index.html") { - Ok(file) => Some(content::Html(file)), - Err(_) => None +pub fn frontend() -> content::Html { + match fs::read_to_string(&format!("{}/{}", FRONTEND_DIR, "index.html")) { + Ok(file) => content::Html(file), + Err(_) => content::Html(String::from("

No web frontend installed.

")) } } @@ -72,18 +74,16 @@ pub fn get_entries_like(db: &State, lang: &str, like: &str, limit: usize } #[get("/langs?")] -pub fn get_langs(db: &State, installed: bool) -> Json> { - let conn = db.connect(); - - let mut langs: Vec = Vec::new(); +pub fn get_langs(db: &State, installed: bool) -> Json> { + let mut langs: Vec = Vec::new(); if installed { - let mut statement = conn.prepare("SELECT name FROM langs").unwrap(); - - let mut rows = statement.query([]).unwrap(); - - while let Some(row) = rows.next().unwrap() { - langs.push(row.get(0).unwrap()); + for lang in &db.installed_langs { + langs.push(lang.clone()) + } + } else { + for lang in &db.installable_langs { + langs.push(lang.clone()) } } diff --git a/static/index.html b/static/index.html index 3018a78..2c73c74 100644 --- a/static/index.html +++ b/static/index.html @@ -2,7 +2,7 @@ - + diff --git a/static/index.js b/static/index.js index 29d9249..754db57 100644 --- a/static/index.js +++ b/static/index.js @@ -11,12 +11,14 @@ $(document).ready(() => { } }); + const searchBar = $('#search-bar'); + const searchForm = $('#search-form'); + const ajaxContent = $('#ajax-content'); + window.onhashchange = () => { getWord(); }; - const searchBar = $('#search-bar'); - searchBar.autocomplete({ appendTo: '#search-form', source: (request, response) => { @@ -32,7 +34,7 @@ $(document).ready(() => { setTimeout(() => e.currentTarget.select(), 100); }); - $('#search-form').on('submit', e => { + searchForm.on('submit', e => { e.preventDefault(); let word = e.target[0].value @@ -50,7 +52,7 @@ $(document).ready(() => { url: '/langs/pl/words/' + word, success: (data) => { - $('#ajax-content').html(generateHtml(word, data)); + ajaxContent.html(generateHtml(word, data)); }, error: err => console.error(err) @@ -63,7 +65,7 @@ $(document).ready(() => { // A better fix should be made setTimeout(() => searchBar.autocomplete('close'), 1000); } else { - $('#ajax-content').html(''); + ajaxContent.html(''); } }