[add] paste cache

[remove] booklist
[chore] bump version (v0.8.1 -> v0.8.2)
This commit is contained in:
hkau 2024-02-25 15:33:52 -05:00
parent 8417e72e7f
commit ddaf4a0eb9
14 changed files with 287 additions and 205 deletions

View file

@ -3,7 +3,7 @@ name = "bundlrs"
authors = ["hkau"]
license = "MIT"
version = "0.8.1"
version = "0.8.2"
edition = "2021"
rust-version = "1.75"
@ -21,6 +21,7 @@ actix-files = "0.6.5"
actix-web = "4.4.1"
dotenv = "0.15.0"
either = "1.9.0"
env_logger = "0.11.2"
hex_fmt = "0.3.0"
idna = "0.5.0"
pulldown-cmark = "0.9.3"

View file

@ -1,3 +1,5 @@
use std::sync::Mutex;
use actix_web::{get, post, web, HttpRequest, HttpResponse, Responder};
use crate::{db::bundlesdb::AppData, utility};
@ -13,7 +15,10 @@ struct LoginInfo {
}
#[post("/api/auth/register")]
pub async fn register(body: web::Json<RegisterInfo>, data: web::Data<AppData>) -> impl Responder {
pub async fn register(
body: web::Json<RegisterInfo>,
data: web::Data<Mutex<AppData>>,
) -> impl Responder {
// if server disabled registration, return
let disabled = crate::config::get_var("REGISTRATION_DISABLED");
@ -24,7 +29,12 @@ pub async fn register(body: web::Json<RegisterInfo>, data: web::Data<AppData>) -
// ...
let username = &body.username.trim();
let res = data.db.create_user(username.to_string()).await;
let res = data
.lock()
.unwrap()
.db
.create_user(username.to_string())
.await;
let c = res.clone();
let set_cookie = if res.success && res.payload.is_some() {
@ -41,11 +51,13 @@ pub async fn register(body: web::Json<RegisterInfo>, data: web::Data<AppData>) -
}
#[post("/api/auth/login")]
pub async fn login(body: web::Json<LoginInfo>, data: web::Data<AppData>) -> impl Responder {
pub async fn login(body: web::Json<LoginInfo>, data: web::Data<Mutex<AppData>>) -> impl Responder {
let id = body.uid.trim();
let id_hashed = utility::hash(id.to_string());
let res = data
.lock()
.unwrap()
.db
.get_user_by_hashed(id_hashed) // if the user is returned, that means the ID is valid
.await;
@ -65,7 +77,7 @@ pub async fn login(body: web::Json<LoginInfo>, data: web::Data<AppData>) -> impl
}
#[get("/api/auth/logout")]
pub async fn logout(req: HttpRequest, data: web::Data<AppData>) -> impl Responder {
pub async fn logout(req: HttpRequest, data: web::Data<Mutex<AppData>>) -> impl Responder {
let cookie = req.cookie("__Secure-Token");
if cookie.is_none() {
@ -73,6 +85,8 @@ pub async fn logout(req: HttpRequest, data: web::Data<AppData>) -> impl Responde
}
let res = data
.lock()
.unwrap()
.db
.get_user_by_hashed(cookie.unwrap().value().to_string()) // if the user is returned, that means the ID is valid
.await;

View file

@ -1,3 +1,5 @@
use std::sync::Mutex;
use actix_web::{get, post, web, HttpRequest, HttpResponse, Responder};
use crate::db::bundlesdb::{self, AtomicPasteFSFile};
@ -62,10 +64,10 @@ pub async fn render_ssm_request(body: web::Json<RenderInfo>) -> impl Responder {
#[get("/api/ssm/{url:.*}")]
pub async fn render_paste_ssm_request(
req: HttpRequest,
data: web::Data<bundlesdb::AppData>,
data: web::Data<Mutex<bundlesdb::AppData>>,
) -> impl Responder {
let custom_url: String = req.match_info().get("url").unwrap().to_string();
let res = data.db.get_paste_by_url(custom_url).await;
let res = data.lock().unwrap().db.get_paste_by_url(custom_url).await;
if !res.success {
return HttpResponse::NotFound()
@ -98,7 +100,7 @@ pub async fn render_paste_ssm_request(
pub async fn create_request(
req: HttpRequest,
body: web::Json<CreateInfo>,
data: web::Data<bundlesdb::AppData>,
data: web::Data<Mutex<bundlesdb::AppData>>,
) -> impl Responder {
let custom_url: String = body.custom_url.trim().to_string();
let edit_password: &String = &body.edit_password;
@ -115,7 +117,9 @@ pub async fn create_request(
let token_cookie = req.cookie("__Secure-Token");
let token_user = if token_cookie.is_some() {
Option::Some(
data.db
data.lock()
.unwrap()
.db
.get_user_by_hashed(token_cookie.as_ref().unwrap().value().to_string()) // if the user is returned, that means the ID is valid
.await,
)
@ -140,6 +144,8 @@ pub async fn create_request(
// create paste
let res = data
.lock()
.unwrap()
.db
.create_paste(
&mut bundlesdb::Paste {
@ -173,7 +179,7 @@ pub async fn create_request(
pub async fn edit_request(
req: HttpRequest,
body: web::Json<EditInfo>,
data: web::Data<bundlesdb::AppData>,
data: web::Data<Mutex<bundlesdb::AppData>>,
) -> impl Responder {
let custom_url: String = body.custom_url.trim().to_string();
let content: String = body.content.trim().to_string();
@ -185,7 +191,9 @@ pub async fn edit_request(
let token_cookie = req.cookie("__Secure-Token");
let token_user = if token_cookie.is_some() {
Option::Some(
data.db
data.lock()
.unwrap()
.db
.get_user_by_hashed(token_cookie.as_ref().unwrap().value().to_string()) // if the user is returned, that means the ID is valid
.await,
)
@ -202,6 +210,8 @@ pub async fn edit_request(
// ...
let res = data
.lock()
.unwrap()
.db
.edit_paste_by_url(
custom_url,
@ -228,7 +238,7 @@ pub async fn edit_request(
pub async fn edit_atomic_request(
req: HttpRequest,
body: web::Json<EditAtomicInfo>,
data: web::Data<bundlesdb::AppData>,
data: web::Data<Mutex<bundlesdb::AppData>>,
) -> impl Responder {
// this is essentially the same as edit_request but it handles the atomic JSON file system
// ...it does NOT accept an edit password! users must be authenticated
@ -240,7 +250,9 @@ pub async fn edit_atomic_request(
let token_cookie = req.cookie("__Secure-Token");
let token_user = if token_cookie.is_some() {
Option::Some(
data.db
data.lock()
.unwrap()
.db
.get_user_by_hashed(token_cookie.as_ref().unwrap().value().to_string()) // if the user is returned, that means the ID is valid
.await,
)
@ -256,8 +268,12 @@ pub async fn edit_atomic_request(
}
// get paste
let paste: bundlesdb::DefaultReturn<Option<bundlesdb::Paste<String>>> =
data.db.get_paste_by_url(custom_url.clone()).await;
let paste: bundlesdb::DefaultReturn<Option<bundlesdb::Paste<String>>> = data
.lock()
.unwrap()
.db
.get_paste_by_url(custom_url.clone())
.await;
if paste.success == false {
return HttpResponse::Ok()
@ -298,6 +314,8 @@ pub async fn edit_atomic_request(
// ...
let res = data
.lock()
.unwrap()
.db
.edit_paste_by_url(
custom_url,
@ -324,7 +342,7 @@ pub async fn edit_atomic_request(
pub async fn delete_request(
req: HttpRequest,
body: web::Json<DeleteInfo>,
data: web::Data<bundlesdb::AppData>,
data: web::Data<Mutex<bundlesdb::AppData>>,
) -> impl Responder {
let custom_url: String = body.custom_url.trim().to_string();
let edit_password: String = body.edit_password.to_owned();
@ -333,7 +351,9 @@ pub async fn delete_request(
let token_cookie = req.cookie("__Secure-Token");
let token_user = if token_cookie.is_some() {
Option::Some(
data.db
data.lock()
.unwrap()
.db
.get_user_by_hashed(token_cookie.as_ref().unwrap().value().to_string()) // if the user is returned, that means the ID is valid
.await,
)
@ -350,6 +370,8 @@ pub async fn delete_request(
// delete
let res = data
.lock()
.unwrap()
.db
.delete_paste_by_url(
custom_url,
@ -373,7 +395,7 @@ pub async fn delete_request(
pub async fn metadata_request(
req: HttpRequest,
body: web::Json<MetadataInfo>,
data: web::Data<bundlesdb::AppData>,
data: web::Data<Mutex<bundlesdb::AppData>>,
) -> impl Responder {
let custom_url: String = body.custom_url.trim().to_string();
let edit_password: String = body.edit_password.to_owned();
@ -385,7 +407,9 @@ pub async fn metadata_request(
let token_cookie = req.cookie("__Secure-Token");
let token_user = if token_cookie.is_some() {
Option::Some(
data.db
data.lock()
.unwrap()
.db
.get_user_by_hashed(token_cookie.as_ref().unwrap().value().to_string()) // if the user is returned, that means the ID is valid
.await,
)
@ -402,6 +426,8 @@ pub async fn metadata_request(
// ...
let res = data
.lock()
.unwrap()
.db
.edit_paste_metadata_by_url(
custom_url,
@ -425,10 +451,10 @@ pub async fn metadata_request(
/// Check if a paste exists
pub async fn exists_request(
req: HttpRequest,
data: web::Data<bundlesdb::AppData>,
data: web::Data<Mutex<bundlesdb::AppData>>,
) -> impl Responder {
let custom_url: String = req.match_info().get("url").unwrap().to_string();
let res = data.db.get_paste_by_url(custom_url).await;
let res = data.lock().unwrap().db.get_paste_by_url(custom_url).await;
// return
return HttpResponse::Ok()
@ -440,11 +466,11 @@ pub async fn exists_request(
/// Get paste by `custom_url`
pub async fn get_from_url_request(
req: HttpRequest,
data: web::Data<bundlesdb::AppData>,
data: web::Data<Mutex<bundlesdb::AppData>>,
) -> impl Responder {
let custom_url: String = req.match_info().get("url").unwrap().to_string();
let res: bundlesdb::DefaultReturn<Option<bundlesdb::Paste<String>>> =
data.db.get_paste_by_url(custom_url).await;
data.lock().unwrap().db.get_paste_by_url(custom_url).await;
// if res.metadata contains '"private_source":"on"', return NotFound
if res.payload.is_some()
@ -487,11 +513,11 @@ pub async fn get_from_url_request(
/// Get paste by ID
pub async fn get_from_id_request(
req: HttpRequest,
data: web::Data<bundlesdb::AppData>,
data: web::Data<Mutex<bundlesdb::AppData>>,
) -> impl Responder {
let id: String = req.match_info().get("id").unwrap().to_string();
let res: bundlesdb::DefaultReturn<Option<bundlesdb::Paste<String>>> =
data.db.get_paste_by_id(id).await;
data.lock().unwrap().db.get_paste_by_id(id).await;
// if res.metadata contains '"private_source":"on"', return NotFound
if res.payload.is_some()
@ -534,11 +560,11 @@ pub async fn get_from_id_request(
/// Get all pastes by owner
pub async fn get_from_owner_request(
req: HttpRequest,
data: web::Data<bundlesdb::AppData>,
data: web::Data<Mutex<bundlesdb::AppData>>,
) -> impl Responder {
let username: String = req.match_info().get("username").unwrap().to_string();
let res: bundlesdb::DefaultReturn<Option<Vec<bundlesdb::PasteIdentifier>>> =
data.db.get_pastes_by_owner(username).await;
data.lock().unwrap().db.get_pastes_by_owner(username).await;
// return
return HttpResponse::Ok()

View file

@ -1,68 +0,0 @@
//! The "booklist" is a file stored in the cwd which contains a list of blocked URLs.
//!
//! These URLs can be claimed, but will always return the same dummy record when fetched from the database.
//!
//! # Example:
//!
//! ```
//! ;
//! url1;url2;url3
//! ```
//!
//! # Example:
//!
//! ```
//! ,
//! url1, url2, url3
//! ```
//!
//! The first line of a booklist defines the separator (\n works)
use std::fs;
#[allow(dead_code)]
#[derive(Debug)]
struct BookList {
separator: String,
full: String,
}
#[allow(dead_code)]
fn fetch_booklist() -> BookList {
let content = fs::read_to_string(format!("booklist.txt"));
if content.is_err() {
return BookList {
separator: String::new(),
full: String::new(),
};
}
// get sep
let content = content.unwrap();
let sep = &content.lines().next();
// return
return BookList {
separator: if sep.is_some() {
sep.unwrap().to_string()
} else {
String::from("\n")
},
full: content,
};
}
pub fn check_booklist(looking_for: &String) -> bool {
// load booklist
let list = fetch_booklist();
// check for word
let split: Vec<String> = list
.full
.split(&list.separator)
.map(|s| s.to_string())
.collect::<Vec<String>>();
return split.contains(looking_for) | split.contains(&format!("\n{}", looking_for));
}

View file

@ -1,7 +1,10 @@
//! # BundlesDB
//! Database handler for all database types
use super::sql::{self, Database, DatabaseOpts};
use super::{
cache::CacheStore,
sql::{self, Database, DatabaseOpts},
};
use sqlx::{Column, Row};
use crate::utility;
@ -139,12 +142,14 @@ pub struct BundlesDB {
#[cfg(feature = "sqlite")]
pub struct BundlesDB {
pub db: Database<sqlx::SqlitePool>,
pub paste_cache: CacheStore<Paste<String>>,
}
impl BundlesDB {
pub async fn new(options: DatabaseOpts) -> BundlesDB {
return BundlesDB {
db: sql::create_db(options).await,
paste_cache: CacheStore::new(),
};
}
@ -206,7 +211,7 @@ impl BundlesDB {
}
#[cfg(feature = "sqlite")]
fn textify_row(&self, row: sqlx::sqlite::SqliteRow) -> DatabaseReturn {
fn textify_row(&mut self, row: sqlx::sqlite::SqliteRow) -> DatabaseReturn {
// get all columns
let columns = row.columns();
@ -223,7 +228,7 @@ impl BundlesDB {
}
#[cfg(feature = "postgres")]
fn textify_row(&self, row: sqlx::postgres::PgRow) -> DatabaseReturn {
fn textify_row(&mut self, row: sqlx::postgres::PgRow) -> DatabaseReturn {
// get all columns
let columns = row.columns();
@ -240,7 +245,7 @@ impl BundlesDB {
}
#[cfg(feature = "mysql")]
fn textify_row(&self, row: sqlx::mysql::MySqlRow) -> DatabaseReturn {
fn textify_row(&mut self, row: sqlx::mysql::MySqlRow) -> DatabaseReturn {
// get all columns
let columns = row.columns();
@ -277,7 +282,7 @@ impl BundlesDB {
///
/// # Arguments:
/// * `hashed` - `String` of the user's hashed ID
pub async fn get_user_by_hashed(&self, hashed: String) -> DefaultReturn<Option<UserState>> {
pub async fn get_user_by_hashed(&mut self, hashed: String) -> DefaultReturn<Option<UserState>> {
let query: &str = if (self.db._type == "sqlite") | (self.db._type == "mysql") {
"SELECT * FROM \"Users\" WHERE \"id_hashed\" = ?"
} else {
@ -319,7 +324,10 @@ impl BundlesDB {
///
/// # Arguments:
/// * `username` - `String` of the user's username
pub async fn get_user_by_username(&self, username: String) -> DefaultReturn<Option<UserState>> {
pub async fn get_user_by_username(
&mut self,
username: String,
) -> DefaultReturn<Option<UserState>> {
let query: &str = if (self.db._type == "sqlite") | (self.db._type == "mysql") {
"SELECT * FROM \"Users\" WHERE \"username\" = ?"
} else {
@ -362,7 +370,7 @@ impl BundlesDB {
///
/// # Arguments:
/// * `username` - `String` of the user's `username`
pub async fn create_user(&self, username: String) -> DefaultReturn<Option<String>> {
pub async fn create_user(&mut self, username: String) -> DefaultReturn<Option<String>> {
// make sure user doesn't already exists
let existing = &self.get_user_by_username(username.clone()).await;
if existing.success {
@ -438,7 +446,7 @@ impl BundlesDB {
///
/// # Arguments:
/// * `id` - `String` of the log's `id`
pub async fn get_log_by_id(&self, id: String) -> DefaultReturn<Option<Log>> {
pub async fn get_log_by_id(&mut self, id: String) -> DefaultReturn<Option<Log>> {
let query: &str = if (self.db._type == "sqlite") | (self.db._type == "mysql") {
"SELECT * FROM \"Logs\" WHERE \"id\" = ?"
} else {
@ -480,7 +488,7 @@ impl BundlesDB {
/// * `logtype` - `String` of the log's `logtype`
/// * `content` - `String` of the log's `content`
pub async fn create_log(
&self,
&mut self,
logtype: String,
content: String,
) -> DefaultReturn<Option<String>> {
@ -522,7 +530,7 @@ impl BundlesDB {
/// # Arguments:
/// * `id` - `String` of the log's `id`
/// * `content` - `String` of the log's new content
pub async fn edit_log(&self, id: String, content: String) -> DefaultReturn<Option<String>> {
pub async fn edit_log(&mut self, id: String, content: String) -> DefaultReturn<Option<String>> {
// make sure log exists
let existing = &self.get_log_by_id(id.clone()).await;
if !existing.success {
@ -567,7 +575,7 @@ impl BundlesDB {
///
/// # Arguments:
/// * `id` - `String` of the log's `id`
pub async fn delete_log(&self, id: String) -> DefaultReturn<Option<String>> {
pub async fn delete_log(&mut self, id: String) -> DefaultReturn<Option<String>> {
// make sure log exists
let existing = &self.get_log_by_id(id.clone()).await;
if !existing.success {
@ -606,46 +614,65 @@ impl BundlesDB {
// pastes
/// Count the `view_paste` logs for a specific [`Paste`]
async fn count_paste_views(&mut self, custom_url: String) -> usize {
let c = &self.db.client;
// count views
let query: &str = if (self.db._type == "sqlite") | (self.db._type == "mysql") {
"SELECT * FROM \"Logs\" WHERE \"logtype\" = 'view_paste' AND \"content\" LIKE ?"
} else {
"SELECT * FROM \"Logs\" WHERE \"logtype\" = 'view_paste' AND \"content\" LIKE $1"
};
let views_res = sqlx::query(query)
.bind::<&String>(&format!("{}::%", &custom_url))
.fetch_all(c)
.await;
if views_res.is_err() {
return 0;
}
return views_res.unwrap().len();
}
/// Build a [`Paste`] query with information about it
async fn build_result_from_query(
&self,
&mut self,
query: &str,
selector: &str,
allow_cache: bool,
) -> DefaultReturn<Option<Paste<String>>> {
// check if we're fetching a booklist url
let is_banned = crate::booklist::check_booklist(&selector.to_lowercase());
// ...
let exists_in_cache = &self.paste_cache.load(selector).is_some();
if is_banned == true {
if (exists_in_cache == &true) && allow_cache {
// get views
// if allow_cache is true, `selector` should ALWAYS be the custom_url since the cache stores by that, not ID
let views = &self.count_paste_views(selector.to_owned()).await;
let paste = &self.paste_cache.load(selector).unwrap();
// return
return DefaultReturn {
success: true,
message: String::from("Paste exists (booklist)"),
message: String::from("Paste exists"),
payload: Option::Some(Paste {
custom_url: selector.to_string(),
id: String::new(),
group_name: String::new(),
edit_password: String::new(),
pub_date: 0,
edit_date: 0,
content: String::new(),
content_html: String::from(
"This custom URL has been blocked by the server booklist.txt file. This is an automatically generated body content.",
),
metadata: serde_json::to_string::<PasteMetadata>(&PasteMetadata {
owner: String::from(""),
private_source: String::from("on"),
title: Option::Some(String::new()),
description: Option::Some(String::new()),
favicon: Option::None,
embed_color: Option::None,
view_password: Option::None,
})
.unwrap(),
views: 0,
custom_url: paste.custom_url.to_string(),
id: paste.id.to_string(),
group_name: paste.group_name.to_string(),
edit_password: paste.edit_password.to_string(),
pub_date: paste.pub_date,
edit_date: paste.edit_date,
content: paste.content.to_string(),
content_html: paste.content_html.to_string(),
metadata: paste.metadata.to_string(),
views: views.to_owned(),
}),
};
}
// ...
// fetch from db
let c = &self.db.client;
let res = sqlx::query(query)
.bind::<&String>(&selector.to_lowercase())
@ -664,42 +691,35 @@ impl BundlesDB {
let row = res.unwrap();
let row = self.textify_row(row).data;
// count views
let query: &str = if (self.db._type == "sqlite") | (self.db._type == "mysql") {
"SELECT * FROM \"Logs\" WHERE \"logtype\" = 'view_paste' AND \"content\" LIKE ?"
} else {
"SELECT * FROM \"Logs\" WHERE \"logtype\" = 'view_paste' AND \"content\" LIKE $1"
};
let views_res = sqlx::query(query)
.bind::<&String>(&format!("{}::%", &row.get("custom_url").unwrap()))
.fetch_all(c)
// get views
let views = &self
.count_paste_views(row.get("custom_url").unwrap().to_owned())
.await;
if views_res.is_err() {
return DefaultReturn {
success: false,
message: String::from(views_res.err().unwrap().to_string()),
payload: Option::None,
};
// add to cache
let paste = Paste {
custom_url: row.get("custom_url").unwrap().to_string(),
id: row.get("id").unwrap().to_string(),
group_name: row.get("group_name").unwrap().to_string(),
edit_password: row.get("edit_password").unwrap().to_string(),
pub_date: row.get("pub_date").unwrap().parse::<u128>().unwrap(),
edit_date: row.get("edit_date").unwrap().parse::<u128>().unwrap(),
content: row.get("content").unwrap().to_string(),
content_html: row.get("content_html").unwrap().to_string(),
metadata: row.get("metadata").unwrap().to_string(),
views: views.to_owned(),
};
if allow_cache {
self.paste_cache
.store(row.get("custom_url").unwrap().to_string(), paste.clone());
}
// return
return DefaultReturn {
success: true,
message: String::from("Paste exists"),
payload: Option::Some(Paste {
custom_url: row.get("custom_url").unwrap().to_string(),
id: row.get("id").unwrap().to_string(),
group_name: row.get("group_name").unwrap().to_string(),
edit_password: row.get("edit_password").unwrap().to_string(),
pub_date: row.get("pub_date").unwrap().parse::<u128>().unwrap(),
edit_date: row.get("edit_date").unwrap().parse::<u128>().unwrap(),
content: row.get("content").unwrap().to_string(),
content_html: row.get("content_html").unwrap().to_string(),
metadata: row.get("metadata").unwrap().to_string(),
views: views_res.unwrap().len(),
}),
payload: Option::Some(paste),
};
}
@ -708,28 +728,28 @@ impl BundlesDB {
///
/// # Arguments:
/// * `url` - `String` of the paste's `custom_url`
pub async fn get_paste_by_url(&self, url: String) -> DefaultReturn<Option<Paste<String>>> {
pub async fn get_paste_by_url(&mut self, url: String) -> DefaultReturn<Option<Paste<String>>> {
let query: &str = if (self.db._type == "sqlite") | (self.db._type == "mysql") {
"SELECT * FROM \"Pastes\" WHERE \"custom_url\" = ?"
} else {
"SELECT * FROM \"Pastes\" WHERE \"custom_url\" = $1"
};
return self.build_result_from_query(query, &url).await;
return self.build_result_from_query(query, &url, true).await;
}
/// Get a [`Paste`] given its `id`
///
/// # Arguments:
/// * `id` - `String` of the paste's `id`
pub async fn get_paste_by_id(&self, id: String) -> DefaultReturn<Option<Paste<String>>> {
pub async fn get_paste_by_id(&mut self, id: String) -> DefaultReturn<Option<Paste<String>>> {
let query: &str = if (self.db._type == "sqlite") | (self.db._type == "mysql") {
"SELECT * FROM \"Pastes\" WHERE \"id\" = ?"
} else {
"SELECT * FROM \"Pastes\" WHERE \"id\" = $1"
};
return self.build_result_from_query(query, &id).await;
return self.build_result_from_query(query, &id, false).await;
}
/// Get all [pastes](Paste) owned by a specific user
@ -737,7 +757,7 @@ impl BundlesDB {
/// # Arguments:
/// * `owner` - `String` of the owner's `username`
pub async fn get_pastes_by_owner(
&self,
&mut self,
owner: String,
) -> DefaultReturn<Option<Vec<PasteIdentifier>>> {
let query: &str = if (self.db._type == "sqlite") | (self.db._type == "mysql") {
@ -784,7 +804,7 @@ impl BundlesDB {
/// # Arguments:
/// * `owner` - `String` of the owner's `username`
pub async fn get_atomic_pastes_by_owner(
&self,
&mut self,
owner: String,
) -> DefaultReturn<Option<Vec<PasteIdentifier>>> {
let query: &str = if (self.db._type == "sqlite") | (self.db._type == "mysql") {
@ -834,7 +854,7 @@ impl BundlesDB {
/// * `props` - [`Paste<String>`](Paste)
/// * `as_user` - The ID of the user creating the paste
pub async fn create_paste(
&self,
&mut self,
props: &mut Paste<String>,
as_user: Option<String>, // id of paste owner
) -> DefaultReturn<Option<Paste<String>>> {
@ -1010,7 +1030,7 @@ impl BundlesDB {
/// Edit an existing [`Paste`] given its `custom_url`
pub async fn edit_paste_by_url(
&self,
&mut self,
url: String,
content: String,
edit_password: String,
@ -1101,6 +1121,10 @@ impl BundlesDB {
};
}
// we're not even going to update the cache, just purge the paste from the cache
// this also means we don't have to handle any decisions on if the paste custom_url changed or not
self.paste_cache.clear(&custom_url);
// return
return DefaultReturn {
success: true,
@ -1111,7 +1135,7 @@ impl BundlesDB {
/// Update a [`Paste`]'s metadata by its `custom_url`
pub async fn edit_paste_metadata_by_url(
&self,
&mut self,
url: String,
metadata: PasteMetadata,
edit_password: String,
@ -1184,6 +1208,10 @@ impl BundlesDB {
};
}
// we're not even going to update the cache, just purge the paste from the cache
// this also means we don't have to handle any decisions on if the paste custom_url changed or not
self.paste_cache.clear(&url);
// return
return DefaultReturn {
success: true,
@ -1197,7 +1225,7 @@ impl BundlesDB {
/// # Arguments:
/// * `view_as` - The username of the account that viewed the paste
pub async fn add_view_to_url(
&self,
&mut self,
url: &String,
view_as: &String, // username of account that is viewing this paste
) -> DefaultReturn<Option<String>> {
@ -1262,7 +1290,7 @@ impl BundlesDB {
/// Delete a [`Paste`] given its `custom_url` and `edit_password`
pub async fn delete_paste_by_url(
&self,
&mut self,
url: String,
edit_password: String,
delete_as: Option<String>,
@ -1350,6 +1378,9 @@ impl BundlesDB {
};
}
// remove from cache
self.paste_cache.clear(&url);
// return
return DefaultReturn {
success: true,
@ -1365,7 +1396,7 @@ impl BundlesDB {
///
/// # Arguments:
/// * `url` - group name
pub async fn get_group_by_name(&self, url: String) -> DefaultReturn<Option<Group<String>>> {
pub async fn get_group_by_name(&mut self, url: String) -> DefaultReturn<Option<Group<String>>> {
let query: &str = if (self.db._type == "sqlite") | (self.db._type == "mysql") {
"SELECT * FROM \"Groups\" WHERE \"name\" = ?"
} else {
@ -1403,7 +1434,10 @@ impl BundlesDB {
///
/// # Arguments:
/// * `props` - [`Group<GroupMetadata>`](Group)
pub async fn create_group(&self, props: Group<GroupMetadata>) -> DefaultReturn<Option<String>> {
pub async fn create_group(
&mut self,
props: Group<GroupMetadata>,
) -> DefaultReturn<Option<String>> {
let p: &Group<GroupMetadata> = &props; // borrowed props
// make sure group does not exist

26
src/db/cache.rs Normal file
View file

@ -0,0 +1,26 @@
use std::collections::HashMap;
#[derive(Debug, Default, Clone)]
pub struct CacheStore<T> {
pub objects: HashMap<String, T>,
}
impl<T> CacheStore<T> {
pub fn new() -> CacheStore<T> {
CacheStore {
objects: HashMap::new(),
}
}
pub fn store(&mut self, key: String, value: T) -> Option<T> {
self.objects.insert(key, value)
}
pub fn load(&mut self, key: &str) -> Option<&T> {
self.objects.get(key)
}
pub fn clear(&mut self, key: &str) -> () {
self.objects.remove(key);
}
}

View file

@ -1,3 +1,4 @@
//! Database handling
pub mod bundlesdb;
pub mod cache;
pub mod sql;

View file

@ -5,7 +5,6 @@
#![doc(issue_tracker_base_url = "https://code.stellular.org/SentryTwo/bundlrs/issues/")]
pub mod api;
pub mod booklist;
mod components;
mod config;
pub mod db;

View file

@ -1,3 +1,5 @@
use std::sync::Mutex;
use actix_files as fs;
use actix_web::{web, App, HttpResponse, HttpServer};
use dotenv;
@ -16,8 +18,6 @@ mod pages;
mod markdown;
mod ssm;
mod booklist;
use crate::db::bundlesdb::{AppData, BundlesDB};
use crate::db::sql::DatabaseOpts;
@ -25,6 +25,9 @@ use crate::db::sql::DatabaseOpts;
async fn main() -> std::io::Result<()> {
dotenv::dotenv().ok();
// std::env::set_var("RUST_LOG", "debug");
env_logger::init();
// ...
let args: Vec<String> = config::collect_arguments();
@ -79,10 +82,13 @@ async fn main() -> std::io::Result<()> {
// start server
println!("Starting server at: http://localhost:{port}");
// create data
let data = web::Data::new(Mutex::new(AppData { db: db.clone() }));
// serve routes
HttpServer::new(move || {
App::new()
.app_data(web::Data::new(AppData { db: db.clone() }))
.app_data(web::Data::clone(&data))
// middleware
.wrap(actix_web::middleware::Logger::default())
// static dir

View file

@ -1,3 +1,5 @@
use std::sync::Mutex;
use actix_web::HttpRequest;
use actix_web::{get, web, HttpResponse, Responder};
@ -80,7 +82,7 @@ fn build_dashboard_renderer_with_props(props: Props) -> ServerRenderer<Dashboard
/// Available at "/d/atomic"
pub async fn dashboard_request(
req: HttpRequest,
data: web::Data<db::bundlesdb::AppData>,
data: web::Data<Mutex<db::bundlesdb::AppData>>,
) -> impl Responder {
// verify auth status
let token_cookie = req.cookie("__Secure-Token");
@ -88,7 +90,9 @@ pub async fn dashboard_request(
let token_user = if token_cookie.is_some() {
Option::Some(
data.db
data.lock()
.unwrap()
.db
.get_user_by_hashed(token_cookie.as_ref().unwrap().value().to_string()) // if the user is returned, that means the ID is valid
.await,
)
@ -113,6 +117,8 @@ You can create an account at: /d/auth/register",
// fetch pastes
let pastes = data
.lock()
.unwrap()
.db
.get_atomic_pastes_by_owner(token_user.clone().unwrap().payload.unwrap().username)
.await;
@ -183,7 +189,7 @@ fn build_new_renderer_with_props(props: NewProps) -> ServerRenderer<CreateNew> {
/// Available at "/d/atomic/new"
pub async fn new_request(
req: HttpRequest,
data: web::Data<db::bundlesdb::AppData>,
data: web::Data<Mutex<db::bundlesdb::AppData>>,
) -> impl Responder {
// verify auth status
let token_cookie = req.cookie("__Secure-Token");
@ -191,7 +197,9 @@ pub async fn new_request(
let token_user = if token_cookie.is_some() {
Option::Some(
data.db
data.lock()
.unwrap()
.db
.get_user_by_hashed(token_cookie.as_ref().unwrap().value().to_string()) // if the user is returned, that means the ID is valid
.await,
)
@ -302,7 +310,7 @@ fn build_fs_renderer_with_props(props: FSProps) -> ServerRenderer<PasteFiles> {
/// Available at "/d/atomic/{id}"
pub async fn edit_request(
req: HttpRequest,
data: web::Data<db::bundlesdb::AppData>,
data: web::Data<Mutex<db::bundlesdb::AppData>>,
info: web::Query<EditQueryProps>,
) -> impl Responder {
// verify auth status
@ -311,7 +319,9 @@ pub async fn edit_request(
let token_user = if token_cookie.is_some() {
Option::Some(
data.db
data.lock()
.unwrap()
.db
.get_user_by_hashed(token_cookie.as_ref().unwrap().value().to_string()) // if the user is returned, that means the ID is valid
.await,
)
@ -336,7 +346,8 @@ You can create an account at: /d/auth/register",
// get paste
let id: String = req.match_info().get("id").unwrap().to_string();
let paste: bundlesdb::DefaultReturn<Option<Paste<String>>> = data.db.get_paste_by_id(id).await;
let paste: bundlesdb::DefaultReturn<Option<Paste<String>>> =
data.lock().unwrap().db.get_paste_by_id(id).await;
if paste.success == false {
let renderer = ServerRenderer::<crate::pages::errors::_404Page>::new();

View file

@ -1,3 +1,5 @@
use std::sync::Mutex;
use actix_web::HttpResponse;
use actix_web::{get, web, HttpRequest, Responder};
@ -175,13 +177,16 @@ fn build_renderer_with_props(props: Props) -> ServerRenderer<ProfileView> {
#[get("/~{username:.*}")]
/// Available at "/~{username}"
pub async fn profile_view_request(req: HttpRequest, data: web::Data<AppData>) -> impl Responder {
pub async fn profile_view_request(
req: HttpRequest,
data: web::Data<Mutex<AppData>>,
) -> impl Responder {
// get paste
let username: String = req.match_info().get("username").unwrap().to_string();
let username_c = username.clone();
let user: bundlesdb::DefaultReturn<Option<UserState>> =
data.db.get_user_by_username(username).await;
data.lock().unwrap().db.get_user_by_username(username).await;
if user.success == false {
let renderer = ServerRenderer::<crate::pages::errors::_404Page>::new();
@ -201,7 +206,9 @@ pub async fn profile_view_request(req: HttpRequest, data: web::Data<AppData>) ->
let token_user = if token_cookie.is_some() {
Option::Some(
data.db
data.lock()
.unwrap()
.db
.get_user_by_hashed(token_cookie.as_ref().unwrap().value().to_string()) // if the user is returned, that means the ID is valid
.await,
)
@ -217,8 +224,12 @@ pub async fn profile_view_request(req: HttpRequest, data: web::Data<AppData>) ->
}
// ...
let pastes_res: bundlesdb::DefaultReturn<Option<Vec<bundlesdb::PasteIdentifier>>> =
data.db.get_pastes_by_owner(username_c.clone()).await;
let pastes_res: bundlesdb::DefaultReturn<Option<Vec<bundlesdb::PasteIdentifier>>> = data
.lock()
.unwrap()
.db
.get_pastes_by_owner(username_c.clone())
.await;
let renderer = build_renderer_with_props(Props {
user: unwrap.clone(),

View file

@ -1,3 +1,5 @@
use std::sync::Mutex;
use actix_web::HttpRequest;
use actix_web::{get, web, HttpResponse, Responder};
@ -196,7 +198,7 @@ fn build_renderer_with_props(props: Props) -> ServerRenderer<Home> {
/// Available at "/"
pub async fn home_request(
req: HttpRequest,
data: web::Data<db::bundlesdb::AppData>,
data: web::Data<Mutex<db::bundlesdb::AppData>>,
info: web::Query<Props>,
) -> impl Responder {
// verify auth status
@ -205,7 +207,9 @@ pub async fn home_request(
let token_user = if token_cookie.is_some() {
Option::Some(
data.db
data.lock()
.unwrap()
.db
.get_user_by_hashed(token_cookie.as_ref().unwrap().value().to_string()) // if the user is returned, that means the ID is valid
.await,
)
@ -224,7 +228,13 @@ pub async fn home_request(
let str: &Option<String> = &info.editing;
let paste = if str.is_some() {
Option::Some(data.db.get_paste_by_url(str.to_owned().unwrap()).await)
Option::Some(
data.lock()
.unwrap()
.db
.get_paste_by_url(str.to_owned().unwrap())
.await,
)
} else {
Option::None
};

View file

@ -1,3 +1,5 @@
use std::sync::Mutex;
use actix_web::HttpResponse;
use actix_web::{get, web, HttpRequest, Responder};
@ -128,7 +130,7 @@ pub fn build_password_ask_renderer_with_props(props: Props) -> ServerRenderer<Pa
/// Available at "/{custom_url}"
pub async fn paste_view_request(
req: HttpRequest,
data: web::Data<AppData>,
data: web::Data<Mutex<AppData>>,
info: web::Query<PasteViewProps>,
) -> impl Responder {
// get paste
@ -136,7 +138,7 @@ pub async fn paste_view_request(
let url_c = url.clone();
let paste: bundlesdb::DefaultReturn<Option<Paste<String>>> =
data.db.get_paste_by_url(url).await;
data.lock().unwrap().db.get_paste_by_url(url).await;
if paste.success == false {
let renderer = ServerRenderer::<crate::pages::errors::_404Page>::new();
@ -204,7 +206,7 @@ pub async fn paste_view_request(
let token_user = if token_cookie.is_some() {
Option::Some(
data.db
data.lock().unwrap().db
.get_user_by_hashed(token_cookie.as_ref().unwrap().value().to_string()) // if the user is returned, that means the ID is valid
.await,
)
@ -221,7 +223,7 @@ pub async fn paste_view_request(
// count view (this will check for an existing view!)
let payload = &token_user.as_ref().unwrap().payload;
if payload.as_ref().is_some() {
data.db
data.lock().unwrap().db
.add_view_to_url(&url_c, &payload.as_ref().unwrap().username)
.await;
}
@ -298,14 +300,14 @@ pub async fn paste_view_request(
/// Available at "/h/{custom_url}/{file_path}"
pub async fn atomic_paste_view_request(
req: HttpRequest,
data: web::Data<AppData>,
data: web::Data<Mutex<AppData>>,
) -> impl Responder {
// get paste
let url: String = req.match_info().get("url").unwrap().to_string();
let path: String = req.match_info().get("path").unwrap().to_string();
let paste: bundlesdb::DefaultReturn<Option<Paste<String>>> =
data.db.get_paste_by_url(url).await;
data.lock().unwrap().db.get_paste_by_url(url).await;
if paste.success == false {
let renderer = ServerRenderer::<crate::pages::errors::_404Page>::new();

View file

@ -1,3 +1,5 @@
use std::sync::Mutex;
use actix_web::HttpResponse;
use actix_web::{get, web, HttpRequest, Responder};
@ -88,14 +90,19 @@ fn build_user_settings_with_props(props: UserSettingsProps) -> ServerRenderer<Us
#[get("/d/settings")]
/// Available at "/d/settings"
pub async fn user_settings_request(req: HttpRequest, data: web::Data<AppData>) -> impl Responder {
pub async fn user_settings_request(
req: HttpRequest,
data: web::Data<Mutex<AppData>>,
) -> impl Responder {
// verify auth status
let token_cookie = req.cookie("__Secure-Token");
let mut set_cookie: &str = "";
let token_user = if token_cookie.is_some() {
Option::Some(
data.db
data.lock()
.unwrap()
.db