Compare commits

...

10 commits

Author SHA1 Message Date
Morrígan
1ae83668ee
stupid shit, lets clean up 2024-12-15 11:07:51 +01:00
Milim
107ad5dfa5
add basic chapter content view 2024-12-07 18:01:11 +01:00
Milim
be6b86ee28
add sqlite database stuff 2024-12-07 12:27:48 +01:00
Milim
28f571e02b
add database model types and a few other db things 2024-12-07 10:58:50 +01:00
Milim
0e50b214ed
add simple documentation 2024-12-07 10:38:16 +01:00
Milim
307419d37f
add sqlite database creation migrations 2024-12-07 10:04:34 +01:00
Milim
ee9e682d9e
set default log level to info 2024-12-07 09:25:08 +01:00
Milim
1b98cbac1a
add basic book/chapter 2024-12-07 09:16:01 +01:00
Milim
f5e7e0a36e
add postgress boilerplate 2024-12-06 20:51:45 +01:00
Milim
5548e37234
basic SQLite database setup 2024-12-06 18:08:24 +01:00
12 changed files with 533 additions and 8 deletions

3
.gitignore vendored
View file

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

138
Cargo.lock generated
View file

@ -243,6 +243,21 @@ version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "anstream"
version = "0.6.18"
@ -382,6 +397,12 @@ dependencies = [
"alloc-stdlib",
]
[[package]]
name = "bumpalo"
version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
[[package]]
name = "bytemuck"
version = "1.20.0"
@ -426,6 +447,20 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "colorchoice"
version = "1.0.3"
@ -464,6 +499,12 @@ dependencies = [
"version_check",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "cpufeatures"
version = "0.2.16"
@ -822,6 +863,7 @@ name = "gunnhildr"
version = "0.1.0"
dependencies = [
"actix-web",
"chrono",
"env_logger",
"figment",
"log",
@ -942,6 +984,29 @@ version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "icu_collections"
version = "1.5.0"
@ -1124,6 +1189,16 @@ dependencies = [
"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]]
name = "language-tags"
version = "0.3.2"
@ -2282,6 +2357,60 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "whoami"
version = "1.5.2"
@ -2292,6 +2421,15 @@ dependencies = [
"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]]
name = "windows-sys"
version = "0.48.0"

View file

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

View file

@ -0,0 +1,25 @@
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
);

46
src/api.rs Normal file
View file

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

151
src/db/mod.rs Normal file
View file

@ -0,0 +1,151 @@
#![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!(),
}
}
}

21
src/db/models.rs Normal file
View file

@ -0,0 +1,21 @@
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,
}

1
src/db/postgres.rs Normal file
View file

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

99
src/db/sqlite.rs Normal file
View file

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

24
src/reading/mod.rs Normal file
View file

@ -0,0 +1,24 @@
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))
}