Block adds & sellers, add metrics

This commit is contained in:
eraden 2024-11-16 08:41:00 +01:00
parent 57b1113cc3
commit 31e0920a50
16 changed files with 243 additions and 57 deletions

36
Cargo.lock generated
View File

@ -330,6 +330,21 @@ dependencies = [
"syn 2.0.82", "syn 2.0.82",
] ]
[[package]]
name = "actix-web-prom"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56a34f1825c3ae06567a9d632466809bbf34963c86002e8921b64f32d48d289d"
dependencies = [
"actix-web",
"futures-core",
"log",
"pin-project-lite",
"prometheus",
"regex",
"strfmt",
]
[[package]] [[package]]
name = "actix_derive" name = "actix_derive"
version = "0.6.2" version = "0.6.2"
@ -1169,6 +1184,7 @@ dependencies = [
"actix-rt", "actix-rt",
"actix-session", "actix-session",
"actix-web", "actix-web",
"actix-web-prom",
"askama", "askama",
"askama_actix", "askama_actix",
"chrono", "chrono",
@ -3292,6 +3308,20 @@ dependencies = [
"yansi", "yansi",
] ]
[[package]]
name = "prometheus"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d33c28a30771f7f96db69893f78b857f7450d7e0237e9c8fc6427a81bae7ed1"
dependencies = [
"cfg-if",
"fnv",
"lazy_static",
"memchr",
"parking_lot",
"thiserror",
]
[[package]] [[package]]
name = "ptr_meta" name = "ptr_meta"
version = "0.1.4" version = "0.1.4"
@ -4650,6 +4680,12 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "strfmt"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a8348af2d9fc3258c8733b8d9d8db2e56f54b2363a4b5b81585c7875ed65e65"
[[package]] [[package]]
name = "stringprep" name = "stringprep"
version = "0.1.5" version = "0.1.5"

View File

@ -38,6 +38,7 @@ futures-core = "0.3.31"
futures-util = "0.3.31" futures-util = "0.3.31"
encoding_rs = "0.8.35" encoding_rs = "0.8.35"
itertools = "0.13.0" itertools = "0.13.0"
actix-web-prom = "0.9.0"
[build-dependencies] [build-dependencies]

View File

@ -7,6 +7,7 @@ use actix_session::{storage::RedisSessionStore, SessionMiddleware};
use actix_web::cookie::Key; use actix_web::cookie::Key;
use actix_web::{error, HttpResponse}; use actix_web::{error, HttpResponse};
use actix_web::{web::Data, App, HttpServer}; use actix_web::{web::Data, App, HttpServer};
use actix_web_prom::{PrometheusMetrics, PrometheusMetricsBuilder};
use sea_orm::Database; use sea_orm::Database;
use serde_qs as qs; use serde_qs as qs;
use serde_qs::actix::QsQueryConfig; use serde_qs::actix::QsQueryConfig;
@ -116,12 +117,19 @@ async fn main() {
Search(search_addr) Search(search_addr)
}; };
let prometheus = PrometheusMetricsBuilder::new("api")
.endpoint("/metrics")
.build()
.unwrap();
// Transform to data // Transform to data
let search = Data::new(search); let search = Data::new(search);
let admins = Data::new(admins); let admins = Data::new(admins);
let db = Data::new(db); let db = Data::new(db);
let redis = Data::new(redis); let redis = Data::new(redis);
crate::routes::db_cleanup(db.clone());
let qs_config = QsQueryConfig::default() let qs_config = QsQueryConfig::default()
.error_handler(|err, _req| { .error_handler(|err, _req| {
// <- create custom error response // <- create custom error response
@ -143,6 +151,7 @@ async fn main() {
.cookie_name(SESSION_KEY.to_string()) .cookie_name(SESSION_KEY.to_string())
.build(), .build(),
) )
.wrap(prometheus.clone())
.app_data(qs_config.clone()) .app_data(qs_config.clone())
.app_data(admins.clone()) .app_data(admins.clone())
.app_data(db.clone()) .app_data(db.clone())

View File

@ -1210,45 +1210,137 @@ async fn styles_css() -> HttpResponse {
.body(include_str!("../assets/styles.css")) .body(include_str!("../assets/styles.css"))
} }
async fn tmp_cleanup() { fn tmp_cleanup() {
let d = tokio::time::Duration::from_secs(10); actix_rt::spawn(async {
loop { let d = tokio::time::Duration::from_secs(10);
tokio::time::sleep(d).await; loop {
let _ = std::fs::create_dir_all("/tmp/assets/recipies/images"); tokio::time::sleep(d).await;
// tracing::info!("Starting files cleanup"); let _ = std::fs::create_dir_all("/tmp/assets/recipies/images");
let Ok(mut dir) = tokio::fs::read_dir("/tmp/assets/recipies/images").await else { let Ok(mut dir) = tokio::fs::read_dir("/tmp/assets/recipies/images").await else {
tracing::info!("Files cleanup failed. No tmp images dir"); tracing::info!("Files cleanup failed. No tmp images dir");
continue;
};
while let Ok(Some(entry)) = dir.next_entry().await {
let Ok(meta) = entry.metadata().await else {
continue; continue;
}; };
let Ok(ts) = meta.modified() else { while let Ok(Some(entry)) = dir.next_entry().await {
continue; let Ok(meta) = entry.metadata().await else {
}; continue;
let valid_until = ts + std::time::Duration::from_days(1); };
let now = std::time::SystemTime::now(); let Ok(ts) = meta.modified() else {
if valid_until < now { continue;
let _ = tokio::fs::remove_file(entry.path()).await; };
let valid_until = ts + std::time::Duration::from_days(1);
let now = std::time::SystemTime::now();
if valid_until < now {
let _ = tokio::fs::remove_file(entry.path()).await;
}
} }
// tracing::info!("Files cleanup done");
} }
// tracing::info!("Files cleanup done"); });
} }
pub fn db_cleanup(db: Data<DatabaseConnection>) {
actix_rt::spawn(async move {
let d = tokio::time::Duration::from_secs(10);
loop {
let ids = Ingredients::find()
.join(
JoinType::Join,
entities::ingredients::Relation::RecipeIngredients.def(),
)
.join(
JoinType::Join,
entities::recipe_ingredients::Relation::Recipies.def(),
)
.select_only()
.column(entities::ingredients::Column::Id)
.column(entities::ingredients::Column::Name)
.column_as(
entities::recipe_ingredients::Column::RecipeId.count(),
"recipe_count",
)
.order_by_asc(entities::ingredients::Column::Name)
.group_by(entities::ingredients::Column::Id)
.into_model::<crate::types::ingredient_with_recipe_count::Model>()
.all(&**db)
.await
.inspect_err(|e| tracing::error!("Failed to load ingredients: {e}"))
.unwrap_or_default()
.into_iter()
.filter(|record| matches!(record.recipe_count, None | Some(0)))
.map(|record| record.id)
.collect::<Vec<_>>();
let _ = Ingredients::delete_many()
.filter(entities::ingredients::Column::Id.is_in(ids))
.exec(&**db)
.await
.inspect_err(|e| {
tracing::error!("Failed clean up ingredients without recipies: {e}")
});
let ids = Tags::find()
.join(JoinType::Join, entities::tags::Relation::RecipeTags.def())
.join(
JoinType::Join,
entities::recipe_tags::Relation::Recipies.def(),
)
.select_only()
.column(entities::tags::Column::Id)
.column(entities::tags::Column::Name)
.column_as(
entities::recipe_tags::Column::RecipeId.count(),
"recipe_count",
)
.order_by_asc(entities::tags::Column::Name)
.group_by(entities::tags::Column::Id)
.into_model::<crate::types::tag_with_recipe_count::Model>()
.all(&**db)
.await
.inspect_err(|e| tracing::error!("Failed to load ingredients: {e}"))
.unwrap_or_default()
.into_iter()
.filter(|record| matches!(record.recipe_count, None | Some(0)))
.map(|record| record.id)
.collect::<Vec<_>>();
let _ = Tags::delete_many()
.filter(entities::ingredients::Column::Id.is_in(ids))
.exec(&**db)
.await
.inspect_err(|e| tracing::error!("Failed clean up tags without recipies: {e}"));
tokio::time::sleep(d).await;
}
});
}
#[get("/app-ads.txt")]
pub async fn no_app_ads() -> HttpResponse {
HttpResponse::Ok().body("")
}
#[get("/ads.txt")]
pub async fn no_ads() -> HttpResponse {
HttpResponse::Ok().body("")
}
#[get("/sellers.json")]
pub async fn no_sellers() -> HttpResponse {
HttpResponse::Ok()
.append_header(("Content-Type", "application/json"))
.body("{}")
}
#[get("/health")]
pub async fn health() -> HttpResponse {
HttpResponse::Ok().finish()
} }
pub fn configure(config: &mut actix_web::web::ServiceConfig) { pub fn configure(config: &mut actix_web::web::ServiceConfig) {
actix_rt::spawn(tmp_cleanup()); tmp_cleanup();
config config
.service(styles_css) .service(styles_css)
.service(render_sign_in) .service(render_sign_in)
.service(sign_in) .service(sign_in)
.service(index_html)
.service(show)
.service(search_page) .service(search_page)
.service(search_results) .service(search_results)
.service(recipe_image_update)
.service(update_recipe) .service(update_recipe)
.service(edit_recipe) .service(edit_recipe)
.service(recipe_form) .service(recipe_form)
@ -1258,5 +1350,12 @@ pub fn configure(config: &mut actix_web::web::ServiceConfig) {
.service(ingredients_list) .service(ingredients_list)
.service(by_tag) .service(by_tag)
.service(tags_list) .service(tags_list)
.service(recipe_image_update)
.service(show)
.service(index_html)
.service(no_app_ads)
.service(no_ads)
.service(no_sellers)
.service(health)
.service(Files::new("/assets", "./assets")); .service(Files::new("/assets", "./assets"));
} }

View File

@ -8,7 +8,7 @@ pub type User = String;
#[derive(Debug)] #[derive(Debug)]
pub struct Ingredient { pub struct Ingredient {
pub id: i32, pub id: i32,
pub qty: i32, pub qty: String,
pub name: String, pub name: String,
pub unit: String, pub unit: String,
} }

View File

@ -28,7 +28,7 @@ pub fn parse_steps(s: &str) -> Result<Vec<(String, Option<String>)>, Error> {
}) })
} }
pub fn parse_ingredients(s: &str) -> Result<Vec<(String, Option<String>, i32)>, Error> { pub fn parse_ingredients(s: &str) -> Result<Vec<(String, Option<String>, String)>, Error> {
s.lines() s.lines()
.map(|s| s.trim()) .map(|s| s.trim())
.filter(|s| !s.is_empty()) .filter(|s| !s.is_empty())
@ -41,19 +41,13 @@ pub fn parse_ingredients(s: &str) -> Result<Vec<(String, Option<String>, i32)>,
0 | 1 => return Err(InvalidIngeredientList), 0 | 1 => return Err(InvalidIngeredientList),
// no unit // no unit
2 => { 2 => {
let qty = pieces let qty = pieces.remove(0);
.remove(0) Ok((pieces.join(" "), None, qty.to_string()))
.parse()
.map_err(|_| InvalidIngeredientList)?;
Ok((pieces.join(" "), None, qty))
} }
_ => { _ => {
let qty = pieces let qty = pieces.remove(0);
.remove(0)
.parse()
.map_err(|_| InvalidIngeredientList)?;
let unit = pieces.remove(0); let unit = pieces.remove(0);
Ok((pieces.join(" "), Some(unit.to_string()), qty)) Ok((pieces.join(" "), Some(unit.to_string()), qty.to_string()))
} }
} }
}) })

View File

@ -1,4 +1,4 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0 //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.11
use sea_orm::entity::prelude::*; use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -8,6 +8,7 @@ use serde::{Deserialize, Serialize};
pub struct Model { pub struct Model {
#[sea_orm(primary_key)] #[sea_orm(primary_key)]
pub id: i32, pub id: i32,
#[sea_orm(unique)]
pub name: String, pub name: String,
} }

View File

@ -1,4 +1,4 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0 //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.11
pub mod prelude; pub mod prelude;

View File

@ -1,4 +1,4 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0 //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.11
pub use super::ingredients::Entity as Ingredients; pub use super::ingredients::Entity as Ingredients;
pub use super::recipe_ingredients::Entity as RecipeIngredients; pub use super::recipe_ingredients::Entity as RecipeIngredients;

View File

@ -1,4 +1,4 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0 //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.11
use sea_orm::entity::prelude::*; use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -9,7 +9,7 @@ pub struct Model {
#[sea_orm(primary_key)] #[sea_orm(primary_key)]
pub id: i32, pub id: i32,
pub ingredient_id: i32, pub ingredient_id: i32,
pub qty: i32, pub qty: String,
pub unit: String, pub unit: String,
pub recipe_id: i32, pub recipe_id: i32,
} }
@ -20,16 +20,16 @@ pub enum Relation {
belongs_to = "super::ingredients::Entity", belongs_to = "super::ingredients::Entity",
from = "Column::IngredientId", from = "Column::IngredientId",
to = "super::ingredients::Column::Id", to = "super::ingredients::Column::Id",
on_update = "NoAction", on_update = "Cascade",
on_delete = "NoAction" on_delete = "Cascade"
)] )]
Ingredients, Ingredients,
#[sea_orm( #[sea_orm(
belongs_to = "super::recipies::Entity", belongs_to = "super::recipies::Entity",
from = "Column::RecipeId", from = "Column::RecipeId",
to = "super::recipies::Column::Id", to = "super::recipies::Column::Id",
on_update = "NoAction", on_update = "Cascade",
on_delete = "NoAction" on_delete = "Cascade"
)] )]
Recipies, Recipies,
} }

View File

@ -1,4 +1,4 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0 //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.11
use sea_orm::entity::prelude::*; use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -19,8 +19,8 @@ pub enum Relation {
belongs_to = "super::recipies::Entity", belongs_to = "super::recipies::Entity",
from = "Column::RecipeId", from = "Column::RecipeId",
to = "super::recipies::Column::Id", to = "super::recipies::Column::Id",
on_update = "NoAction", on_update = "Cascade",
on_delete = "NoAction" on_delete = "Cascade"
)] )]
Recipies, Recipies,
} }

View File

@ -1,4 +1,4 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0 //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.11
use sea_orm::entity::prelude::*; use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -18,16 +18,16 @@ pub enum Relation {
belongs_to = "super::recipies::Entity", belongs_to = "super::recipies::Entity",
from = "Column::RecipeId", from = "Column::RecipeId",
to = "super::recipies::Column::Id", to = "super::recipies::Column::Id",
on_update = "NoAction", on_update = "Cascade",
on_delete = "NoAction" on_delete = "Cascade"
)] )]
Recipies, Recipies,
#[sea_orm( #[sea_orm(
belongs_to = "super::tags::Entity", belongs_to = "super::tags::Entity",
from = "Column::TagId", from = "Column::TagId",
to = "super::tags::Column::Id", to = "super::tags::Column::Id",
on_update = "NoAction", on_update = "Cascade",
on_delete = "NoAction" on_delete = "Cascade"
)] )]
Tags, Tags,
} }

View File

@ -1,4 +1,4 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0 //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.11
use sea_orm::entity::prelude::*; use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};

View File

@ -1,4 +1,4 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0 //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.11
use sea_orm::entity::prelude::*; use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -8,6 +8,7 @@ use serde::{Deserialize, Serialize};
pub struct Model { pub struct Model {
#[sea_orm(primary_key)] #[sea_orm(primary_key)]
pub id: i32, pub id: i32,
#[sea_orm(unique)]
pub name: String, pub name: String,
} }

View File

@ -1,12 +1,16 @@
pub use sea_orm_migration::prelude::*; pub use sea_orm_migration::prelude::*;
mod m20220101_000001_create_table; mod m20220101_000001_create_table;
mod m20220101_000002_change_quantity_type;
pub struct Migrator; pub struct Migrator;
#[async_trait::async_trait] #[async_trait::async_trait]
impl MigratorTrait for Migrator { impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> { fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![Box::new(m20220101_000001_create_table::Migration)] vec![
Box::new(m20220101_000001_create_table::Migration),
Box::new(m20220101_000002_change_quantity_type::Migration),
]
} }
} }

View File

@ -0,0 +1,41 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.alter_table(
Table::alter()
.table(RecipeIngredient::RecipeIngredients)
.modify_column(ColumnDef::new(Alias::new("qty")).string().default("1"))
.to_owned(),
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.alter_table(
Table::alter()
.table(RecipeIngredient::RecipeIngredients)
.modify_column(ColumnDef::new(Alias::new("qty")).integer().default(1))
.to_owned(),
)
.await?;
Ok(())
}
}
#[derive(DeriveIden)]
enum RecipeIngredient {
RecipeIngredients,
Id,
IngredientId,
Qty,
Unit,
RecipeId,
}