Block adds & sellers, add metrics
This commit is contained in:
parent
57b1113cc3
commit
31e0920a50
36
Cargo.lock
generated
36
Cargo.lock
generated
@ -330,6 +330,21 @@ dependencies = [
|
||||
"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]]
|
||||
name = "actix_derive"
|
||||
version = "0.6.2"
|
||||
@ -1169,6 +1184,7 @@ dependencies = [
|
||||
"actix-rt",
|
||||
"actix-session",
|
||||
"actix-web",
|
||||
"actix-web-prom",
|
||||
"askama",
|
||||
"askama_actix",
|
||||
"chrono",
|
||||
@ -3292,6 +3308,20 @@ dependencies = [
|
||||
"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]]
|
||||
name = "ptr_meta"
|
||||
version = "0.1.4"
|
||||
@ -4650,6 +4680,12 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "strfmt"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a8348af2d9fc3258c8733b8d9d8db2e56f54b2363a4b5b81585c7875ed65e65"
|
||||
|
||||
[[package]]
|
||||
name = "stringprep"
|
||||
version = "0.1.5"
|
||||
|
@ -38,6 +38,7 @@ futures-core = "0.3.31"
|
||||
futures-util = "0.3.31"
|
||||
encoding_rs = "0.8.35"
|
||||
itertools = "0.13.0"
|
||||
actix-web-prom = "0.9.0"
|
||||
|
||||
[build-dependencies]
|
||||
|
||||
|
@ -7,6 +7,7 @@ use actix_session::{storage::RedisSessionStore, SessionMiddleware};
|
||||
use actix_web::cookie::Key;
|
||||
use actix_web::{error, HttpResponse};
|
||||
use actix_web::{web::Data, App, HttpServer};
|
||||
use actix_web_prom::{PrometheusMetrics, PrometheusMetricsBuilder};
|
||||
use sea_orm::Database;
|
||||
use serde_qs as qs;
|
||||
use serde_qs::actix::QsQueryConfig;
|
||||
@ -116,12 +117,19 @@ async fn main() {
|
||||
Search(search_addr)
|
||||
};
|
||||
|
||||
let prometheus = PrometheusMetricsBuilder::new("api")
|
||||
.endpoint("/metrics")
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
// Transform to data
|
||||
let search = Data::new(search);
|
||||
let admins = Data::new(admins);
|
||||
let db = Data::new(db);
|
||||
let redis = Data::new(redis);
|
||||
|
||||
crate::routes::db_cleanup(db.clone());
|
||||
|
||||
let qs_config = QsQueryConfig::default()
|
||||
.error_handler(|err, _req| {
|
||||
// <- create custom error response
|
||||
@ -143,6 +151,7 @@ async fn main() {
|
||||
.cookie_name(SESSION_KEY.to_string())
|
||||
.build(),
|
||||
)
|
||||
.wrap(prometheus.clone())
|
||||
.app_data(qs_config.clone())
|
||||
.app_data(admins.clone())
|
||||
.app_data(db.clone())
|
||||
|
@ -1210,45 +1210,137 @@ async fn styles_css() -> HttpResponse {
|
||||
.body(include_str!("../assets/styles.css"))
|
||||
}
|
||||
|
||||
async fn tmp_cleanup() {
|
||||
let d = tokio::time::Duration::from_secs(10);
|
||||
loop {
|
||||
tokio::time::sleep(d).await;
|
||||
let _ = std::fs::create_dir_all("/tmp/assets/recipies/images");
|
||||
// tracing::info!("Starting files cleanup");
|
||||
fn tmp_cleanup() {
|
||||
actix_rt::spawn(async {
|
||||
let d = tokio::time::Duration::from_secs(10);
|
||||
loop {
|
||||
tokio::time::sleep(d).await;
|
||||
let _ = std::fs::create_dir_all("/tmp/assets/recipies/images");
|
||||
|
||||
let Ok(mut dir) = tokio::fs::read_dir("/tmp/assets/recipies/images").await else {
|
||||
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 {
|
||||
let Ok(mut dir) = tokio::fs::read_dir("/tmp/assets/recipies/images").await else {
|
||||
tracing::info!("Files cleanup failed. No tmp images dir");
|
||||
continue;
|
||||
};
|
||||
let Ok(ts) = meta.modified() else {
|
||||
continue;
|
||||
};
|
||||
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;
|
||||
while let Ok(Some(entry)) = dir.next_entry().await {
|
||||
let Ok(meta) = entry.metadata().await else {
|
||||
continue;
|
||||
};
|
||||
let Ok(ts) = meta.modified() else {
|
||||
continue;
|
||||
};
|
||||
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) {
|
||||
actix_rt::spawn(tmp_cleanup());
|
||||
tmp_cleanup();
|
||||
|
||||
config
|
||||
.service(styles_css)
|
||||
.service(render_sign_in)
|
||||
.service(sign_in)
|
||||
.service(index_html)
|
||||
.service(show)
|
||||
.service(search_page)
|
||||
.service(search_results)
|
||||
.service(recipe_image_update)
|
||||
.service(update_recipe)
|
||||
.service(edit_recipe)
|
||||
.service(recipe_form)
|
||||
@ -1258,5 +1350,12 @@ pub fn configure(config: &mut actix_web::web::ServiceConfig) {
|
||||
.service(ingredients_list)
|
||||
.service(by_tag)
|
||||
.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"));
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ pub type User = String;
|
||||
#[derive(Debug)]
|
||||
pub struct Ingredient {
|
||||
pub id: i32,
|
||||
pub qty: i32,
|
||||
pub qty: String,
|
||||
pub name: String,
|
||||
pub unit: String,
|
||||
}
|
||||
|
@ -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()
|
||||
.map(|s| s.trim())
|
||||
.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),
|
||||
// no unit
|
||||
2 => {
|
||||
let qty = pieces
|
||||
.remove(0)
|
||||
.parse()
|
||||
.map_err(|_| InvalidIngeredientList)?;
|
||||
Ok((pieces.join(" "), None, qty))
|
||||
let qty = pieces.remove(0);
|
||||
Ok((pieces.join(" "), None, qty.to_string()))
|
||||
}
|
||||
_ => {
|
||||
let qty = pieces
|
||||
.remove(0)
|
||||
.parse()
|
||||
.map_err(|_| InvalidIngeredientList)?;
|
||||
let qty = 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()))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -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 serde::{Deserialize, Serialize};
|
||||
@ -8,6 +8,7 @@ use serde::{Deserialize, Serialize};
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
#[sea_orm(unique)]
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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::recipe_ingredients::Entity as RecipeIngredients;
|
||||
|
@ -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 serde::{Deserialize, Serialize};
|
||||
@ -9,7 +9,7 @@ pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
pub ingredient_id: i32,
|
||||
pub qty: i32,
|
||||
pub qty: String,
|
||||
pub unit: String,
|
||||
pub recipe_id: i32,
|
||||
}
|
||||
@ -20,16 +20,16 @@ pub enum Relation {
|
||||
belongs_to = "super::ingredients::Entity",
|
||||
from = "Column::IngredientId",
|
||||
to = "super::ingredients::Column::Id",
|
||||
on_update = "NoAction",
|
||||
on_delete = "NoAction"
|
||||
on_update = "Cascade",
|
||||
on_delete = "Cascade"
|
||||
)]
|
||||
Ingredients,
|
||||
#[sea_orm(
|
||||
belongs_to = "super::recipies::Entity",
|
||||
from = "Column::RecipeId",
|
||||
to = "super::recipies::Column::Id",
|
||||
on_update = "NoAction",
|
||||
on_delete = "NoAction"
|
||||
on_update = "Cascade",
|
||||
on_delete = "Cascade"
|
||||
)]
|
||||
Recipies,
|
||||
}
|
||||
|
@ -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 serde::{Deserialize, Serialize};
|
||||
@ -19,8 +19,8 @@ pub enum Relation {
|
||||
belongs_to = "super::recipies::Entity",
|
||||
from = "Column::RecipeId",
|
||||
to = "super::recipies::Column::Id",
|
||||
on_update = "NoAction",
|
||||
on_delete = "NoAction"
|
||||
on_update = "Cascade",
|
||||
on_delete = "Cascade"
|
||||
)]
|
||||
Recipies,
|
||||
}
|
||||
|
@ -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 serde::{Deserialize, Serialize};
|
||||
@ -18,16 +18,16 @@ pub enum Relation {
|
||||
belongs_to = "super::recipies::Entity",
|
||||
from = "Column::RecipeId",
|
||||
to = "super::recipies::Column::Id",
|
||||
on_update = "NoAction",
|
||||
on_delete = "NoAction"
|
||||
on_update = "Cascade",
|
||||
on_delete = "Cascade"
|
||||
)]
|
||||
Recipies,
|
||||
#[sea_orm(
|
||||
belongs_to = "super::tags::Entity",
|
||||
from = "Column::TagId",
|
||||
to = "super::tags::Column::Id",
|
||||
on_update = "NoAction",
|
||||
on_delete = "NoAction"
|
||||
on_update = "Cascade",
|
||||
on_delete = "Cascade"
|
||||
)]
|
||||
Tags,
|
||||
}
|
||||
|
@ -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 serde::{Deserialize, Serialize};
|
||||
|
@ -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 serde::{Deserialize, Serialize};
|
||||
@ -8,6 +8,7 @@ use serde::{Deserialize, Serialize};
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
#[sea_orm(unique)]
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
|
@ -1,12 +1,16 @@
|
||||
pub use sea_orm_migration::prelude::*;
|
||||
|
||||
mod m20220101_000001_create_table;
|
||||
mod m20220101_000002_change_quantity_type;
|
||||
|
||||
pub struct Migrator;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigratorTrait for Migrator {
|
||||
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),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
41
migration/src/m20220101_000002_change_quantity_type.rs
Normal file
41
migration/src/m20220101_000002_change_quantity_type.rs
Normal 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,
|
||||
}
|
Loading…
Reference in New Issue
Block a user