Compare commits

..

No commits in common. "1ae83668ee97a39788819174171ac5d15e81a552" and "8162f81a57ff4cb40c910fe2ca2dba8b6b6d0815" have entirely different histories.

12 changed files with 8 additions and 533 deletions

3
.gitignore vendored
View file

@ -1,4 +1 @@
/target /target
gunnhildr.db
gunnhildr.db-shm
gunnhildr.db-wal

138
Cargo.lock generated
View file

@ -243,21 +243,6 @@ version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "anstream" name = "anstream"
version = "0.6.18" version = "0.6.18"
@ -397,12 +382,6 @@ dependencies = [
"alloc-stdlib", "alloc-stdlib",
] ]
[[package]]
name = "bumpalo"
version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
[[package]] [[package]]
name = "bytemuck" name = "bytemuck"
version = "1.20.0" version = "1.20.0"
@ -447,20 +426,6 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"wasm-bindgen",
"windows-targets 0.52.6",
]
[[package]] [[package]]
name = "colorchoice" name = "colorchoice"
version = "1.0.3" version = "1.0.3"
@ -499,12 +464,6 @@ dependencies = [
"version_check", "version_check",
] ]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]] [[package]]
name = "cpufeatures" name = "cpufeatures"
version = "0.2.16" version = "0.2.16"
@ -863,7 +822,6 @@ name = "gunnhildr"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"actix-web", "actix-web",
"chrono",
"env_logger", "env_logger",
"figment", "figment",
"log", "log",
@ -984,29 +942,6 @@ version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "iana-time-zone"
version = "0.1.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]] [[package]]
name = "icu_collections" name = "icu_collections"
version = "1.5.0" version = "1.5.0"
@ -1189,16 +1124,6 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "js-sys"
version = "0.3.76"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7"
dependencies = [
"once_cell",
"wasm-bindgen",
]
[[package]] [[package]]
name = "language-tags" name = "language-tags"
version = "0.3.2" version = "0.3.2"
@ -2357,60 +2282,6 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
[[package]]
name = "wasm-bindgen"
version = "0.2.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396"
dependencies = [
"cfg-if",
"once_cell",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79"
dependencies = [
"bumpalo",
"log",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6"
[[package]] [[package]]
name = "whoami" name = "whoami"
version = "1.5.2" version = "1.5.2"
@ -2421,15 +2292,6 @@ dependencies = [
"wasite", "wasite",
] ]
[[package]]
name = "windows-core"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
dependencies = [
"windows-targets 0.52.6",
]
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.48.0" version = "0.48.0"

View file

@ -5,10 +5,9 @@ edition = "2021"
[dependencies] [dependencies]
actix-web = {version="4.9.0"} actix-web = {version="4.9.0"}
chrono = "0.4.38"
env_logger = "0.11.5" env_logger = "0.11.5"
figment = {version="0.10.19", features=["env"]} figment = {version="0.10.19", features=["env"]}
log = "0.4.22" log = "0.4.22"
serde = {version="1.0.215", features=["derive"]} serde = {version="1.0.215", features=["derive"]}
sqlx = {version="0.8.2", features=["runtime-tokio","postgres", "sqlite"]} sqlx = {version="0.8.2", features=["runtime-tokio"]}
tokio = "1.42.0" tokio = "1.42.0"

View file

@ -1,25 +0,0 @@
CREATE TABLE IF NOT EXISTS users(
user_id INTEGER PRIMARY KEY,
name TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS books(
book_id INTEGER PRIMARY KEY,
book_title TEXT NOT NULL,
book_description TEXT,
book_creation_date INT NOT NULL,
author_id INTEGER NOT NULL,
FOREIGN KEY (author_id) REFERENCES users(user_id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS chapters(
chapter_id INTEGER PRIMARY KEY,
chapter_title TEXT NOT NULL,
chapter_text TEXT NOT NULL,
chapter_creation_date INT NOT NULL,
book_id INTEGER NOT NULL,
author_id INTEGER NOT NULL,
FOREIGN KEY (book_id) REFERENCES books(book_id) ON DELETE CASCADE,
FOREIGN KEY (author_id) REFERENCES users(user_id) ON DELETE CASCADE
);

View file

@ -1,46 +0,0 @@
use actix_web::{
get,
web::{self, Json, Redirect},
Scope,
};
use serde::Deserialize;
use crate::db::DbInterface;
pub fn api_scope() -> Scope {
web::scope("/api")
.service(create_book)
.service(create_chapter)
.service(create_user)
}
#[derive(Deserialize)]
struct BookForm {
title: String,
description: String,
}
#[get("/create/book")]
async fn create_book(Json(form): Json<BookForm>, db: web::Data<DbInterface>) -> Redirect {
let id = db
.create_book(&form.title, &form.description, todo!())
.await
.unwrap();
Redirect::to(format!("r/b/{}", id)).permanent()
}
#[get("/create/chapter")]
async fn create_chapter() -> String {
todo!()
}
#[derive(Deserialize)]
struct UserForm {
name: String,
}
#[get("/create/user")]
async fn create_user(web::Form(form): web::Form<UserForm>, db: web::Data<DbInterface>) -> String {
todo!()
}

View file

@ -5,11 +5,9 @@ use figment::{
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::net::{IpAddr, Ipv4Addr}; use std::net::{IpAddr, Ipv4Addr};
#[derive(Deserialize, Serialize, Clone, Copy)] #[derive(Deserialize, Serialize)]
pub struct Config { pub struct Config {
/// Ip address that the gunnhildr should bind to
pub binding_ip: IpAddr, pub binding_ip: IpAddr,
/// Port that gunnhildr should listen on
pub port: u16, pub port: u16,
} }
@ -22,7 +20,6 @@ impl Default for Config {
} }
} }
/// Parse and merge all config sources
pub fn parse_config() -> Config { pub fn parse_config() -> Config {
Figment::from(Serialized::defaults(Config::default())) Figment::from(Serialized::defaults(Config::default()))
.merge(Env::prefixed("HILDR")) .merge(Env::prefixed("HILDR"))

View file

@ -1,151 +0,0 @@
#![allow(unused)]
use log::{info, warn};
use sqlx::{migrate::MigrateDatabase, PgPool, Postgres, Sqlite, SqlitePool};
pub mod models;
mod postgres;
mod sqlite;
/// Utility for interacting with the database
#[derive(Clone)]
pub enum DbInterface {
/// Used for Sqlite database
Sqlite(sqlx::Pool<Sqlite>),
/// Used for Postgres database
Postgres(sqlx::Pool<Postgres>),
}
/// Error type for handling DB related errors
#[derive(Debug)]
pub enum DbError {
/// No such entry found
NotFound,
/// Database related error
SqlxError(sqlx::Error),
}
impl From<sqlx::Error> for DbError {
fn from(value: sqlx::Error) -> Self {
match value {
sqlx::Error::RowNotFound => DbError::NotFound,
_ => DbError::SqlxError(value),
}
}
}
type DbResult<T> = Result<T, DbError>;
impl DbInterface {
/// Database backed by SQLite
pub async fn sqlite() -> Self {
// Check if db exists, if not create it.
if !sqlx::Sqlite::database_exists("sqlite:gunnhildr.db")
.await
.expect("failed to connect to db")
{
warn!("No SQLite database found, if this is the first time you are starting Gunnhildr, you can safely ignore this.");
sqlx::Sqlite::create_database("sqlite:gunnhildr.db")
.await
.expect("failed to create SQLite Database");
info!("Created new SQLite Database");
}
let pool = SqlitePool::connect("sqlite:gunnhildr.db").await.unwrap();
// run migrations
sqlx::migrate!("migrations/sqlite")
.run(&pool)
.await
.expect("Failed to apply migration!");
info!("Applied migrations.");
Self::Sqlite(pool)
}
/// Database backed by Postgres
pub async fn postgres(url: &str) -> Self {
// check if database exists and create one if not
if !sqlx::Postgres::database_exists(url)
.await
.expect("failed to connect to db")
{
warn!("No Postgres database found, if this is the first time you are starting Gunnhildr, you can safely ignore this.");
sqlx::Postgres::create_database(url)
.await
.expect("failed to create Postgres Database!");
info!("Created new Postgres Database");
}
let pool = PgPool::connect("url").await.unwrap();
// run migrations
sqlx::migrate!("migrations/postgres")
.run(&pool)
.await
.expect("Failed to apply migration!");
Self::Postgres(pool)
}
/// Tries to fetch a book from the database
pub async fn get_book(&self, id: u32) -> DbResult<models::Book> {
match self {
DbInterface::Sqlite(pool) => sqlite::sqlite_book(pool, id).await,
DbInterface::Postgres(pool) => todo!(),
}
}
/// Tries to create a book and returns the book's id if successful
pub async fn create_book(
&self,
title: &String,
description: &String,
author_id: u32,
) -> DbResult<u32> {
match self {
DbInterface::Sqlite(pool) => {
sqlite::sqlite_book_create(pool, title, description, author_id).await
}
DbInterface::Postgres(pool) => todo!(),
}
}
/// Tries to fetch a chapter from the database
pub async fn get_chapter(&self, id: u32) -> DbResult<models::Chapter> {
match self {
DbInterface::Sqlite(pool) => sqlite::sqlite_chapter(pool, id).await,
DbInterface::Postgres(pool) => todo!(),
}
}
/// Tries to create a chapter and returns the chapter's id if successful
pub async fn create_chapter(
&self,
title: &String,
text: &String,
book_id: u32,
author_id: u32,
) -> DbResult<u32> {
match self {
DbInterface::Sqlite(pool) => {
sqlite::sqlite_chapter_create(pool, title, text, book_id, author_id).await
}
DbInterface::Postgres(pool) => todo!(),
}
}
/// Tries to fetch a user from the database
pub async fn get_user(&self, id: u32) -> DbResult<models::User> {
match self {
DbInterface::Sqlite(pool) => sqlite::sqlite_user(pool, id).await,
DbInterface::Postgres(pool) => todo!(),
}
}
/// Tries to create a user and returns the user's id if successful
pub async fn create_user(&self, name: &String) -> DbResult<u32> {
match self {
DbInterface::Sqlite(pool) => sqlite::sqlite_user_create(pool, name).await,
DbInterface::Postgres(pool) => todo!(),
}
}
}

View file

@ -1,21 +0,0 @@
pub struct Book {
pub id: u32,
pub title: String,
pub description: String,
pub creation_date: String,
pub author_id: u32,
}
pub struct Chapter {
pub id: u32,
pub title: String,
pub text: String,
pub creation_date: String,
pub book_id: u32,
pub author_id: u32,
}
pub struct User {
pub id: u32,
pub name: String,
}

View file

@ -1 +0,0 @@
//! Module containing database code for Postgres

View file

@ -1,99 +0,0 @@
//! Module containing database code for SQLite
use super::models::{Book, Chapter, User};
use super::{DbError, DbResult};
use serde::de;
use sqlx::{Pool, Row, Sqlite};
pub async fn sqlite_book(pool: &Pool<Sqlite>, id: u32) -> DbResult<Book> {
let row = sqlx::query("SELECT * FROM users WHERE user_id = ?")
.bind(id)
.fetch_one(pool)
.await?;
let book = Book {
id,
title: row.get("book_title"),
description: row.get("book_description"),
creation_date: row.get("book_creation_date"),
author_id: row.get("author_id"),
};
Ok(book)
}
pub async fn sqlite_book_create(
pool: &Pool<Sqlite>,
title: &String,
description: &String,
author_id: u32,
) -> DbResult<u32> {
let id = sqlx::query("INSERT INTO books (title, description, author_id, book_creation_date) VALUES ( ?1, ?2, ?3, ?4 )")
.bind(title)
.bind(description)
.bind(author_id)
.bind(chrono::Local::now().timestamp())
.execute(pool)
.await?
.last_insert_rowid() as u32;
Ok(id)
}
pub async fn sqlite_chapter(pool: &Pool<Sqlite>, id: u32) -> DbResult<Chapter> {
let row = sqlx::query("SELECT * FROM chapters WHERE chapter_id = ?")
.bind(id)
.fetch_one(pool)
.await?;
let chapter = Chapter {
id,
title: row.get("chapter_title"),
text: row.get("chapter_text"),
creation_date: row.get("chapter_creation_date"),
book_id: row.get("book_id"),
author_id: row.get("author_id"),
};
Ok(chapter)
}
pub async fn sqlite_chapter_create(
pool: &Pool<Sqlite>,
title: &String,
text: &String,
book_id: u32,
author_id: u32,
) -> DbResult<u32> {
let id = sqlx::query("INSERT INTO chapters (chapter_title, chapter_text, book_id, author_id, chapter_creation_date) VALUES ( ?1, ?2, ?3, ?4, ?5 )")
.bind(title)
.bind(text)
.bind(book_id)
.bind(author_id)
.bind(chrono::Local::now().timestamp())
.execute(pool)
.await?
.last_insert_rowid() as u32;
Ok(id)
}
pub async fn sqlite_user(pool: &Pool<Sqlite>, id: u32) -> DbResult<User> {
let row = sqlx::query("SELECT * FROM users WHERE user_id = ?")
.bind(id)
.fetch_one(pool)
.await?;
let user = User {
id,
name: row.get("name"),
};
Ok(user)
}
pub async fn sqlite_user_create(pool: &Pool<Sqlite>, name: &String) -> DbResult<u32> {
let id = sqlx::query("INSERT INTO users (name) VALUES ( ? )")
.bind(name)
.execute(pool)
.await?
.last_insert_rowid() as u32;
Ok(id)
}

View file

@ -1,29 +1,16 @@
use actix_web::{get, web, App, HttpServer}; use actix_web::{get, App, HttpServer};
use log::info; use log::info;
mod api;
mod config; mod config;
mod db;
mod reading;
#[actix_web::main] #[actix_web::main]
async fn main() -> Result<(), std::io::Error> { async fn main() -> Result<(), std::io::Error> {
// init env logger env_logger::init();
env_logger::builder()
.filter_level(log::LevelFilter::Info)
.init();
let config = config::parse_config(); let config = config::parse_config();
let db = db::DbInterface::sqlite().await;
info!("Server starting..."); info!("Server starting...");
HttpServer::new(move || { HttpServer::new(|| App::new().service(hello))
App::new()
.app_data(web::Data::new(config))
.app_data(web::Data::new(db.clone()))
.service(reading::reading_scope())
.service(hello)
})
.bind((config.binding_ip, config.port))? .bind((config.binding_ip, config.port))?
.run() .run()
.await .await

View file

@ -1,24 +0,0 @@
use actix_web::{get, web, Scope};
use crate::db::DbInterface;
/// scope to handle all reading related pages
pub fn reading_scope() -> Scope {
web::scope("/r").service(book_view).service(chapter_view)
}
/// route to view info for a specific book
#[get("/b/{book}")]
async fn book_view(book: web::Path<u32>, db: web::Data<DbInterface>) -> Option<String> {
Some(format!(
"This is the info for {}",
db.get_book(*book).await.ok()?.title
))
}
/// view for reading a chapter
#[get("/c/{chapter})")]
async fn chapter_view(id: web::Path<u32>, db: web::Data<DbInterface>) -> Option<String> {
let chapter = db.get_chapter(*id).await.ok()?;
Some(format!("Text: {}", chapter.text))
}