Add languages
This commit is contained in:
parent
ea389f4b1c
commit
8d3ac4fc46
119
src/database.rs
119
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<Language>,
|
||||
pub installable_langs: Vec<Language>
|
||||
}
|
||||
|
||||
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<Language> = 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<Language> {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Version>
|
||||
}
|
||||
|
||||
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<Self> {
|
||||
// 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<Ordering> {
|
||||
Some(self.name.cmp(&other.name))
|
||||
}
|
||||
}
|
||||
|
|
57
src/main.rs
57
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();
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
|
@ -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<Ordering> {
|
||||
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)
|
||||
}
|
||||
}
|
28
src/views.rs
28
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<content::Html<String>> {
|
||||
match fs::read_to_string("static/index.html") {
|
||||
Ok(file) => Some(content::Html(file)),
|
||||
Err(_) => None
|
||||
pub fn frontend() -> content::Html<String> {
|
||||
match fs::read_to_string(&format!("{}/{}", FRONTEND_DIR, "index.html")) {
|
||||
Ok(file) => content::Html(file),
|
||||
Err(_) => content::Html(String::from("<h1>No web frontend installed.</h1>"))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -72,18 +74,16 @@ pub fn get_entries_like(db: &State<WordDb>, lang: &str, like: &str, limit: usize
|
|||
}
|
||||
|
||||
#[get("/langs?<installed>")]
|
||||
pub fn get_langs(db: &State<WordDb>, installed: bool) -> Json<Vec<String>> {
|
||||
let conn = db.connect();
|
||||
|
||||
let mut langs: Vec<String> = Vec::new();
|
||||
pub fn get_langs(db: &State<WordDb>, installed: bool) -> Json<Vec<Language>> {
|
||||
let mut langs: Vec<Language> = 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())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
||||
<meta charset="utf-8"/>
|
||||
<link rel="icon" href="/static/favicon.png">
|
||||
<link rel="stylesheet" href="static/jquery-ui-1.13.0/jquery-ui.min.css">
|
||||
<link rel="stylesheet" href="static/jquery-ui-1.13.0/jquery-ui.theme.min.css">
|
||||
|
|
|
@ -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('');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue