diff --git a/README.md b/README.md index 36ab327..dd27986 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,4 @@ content (blog posts etc.) repo. Powered by Next.js and Rust, built with passion and love > [!NOTE] -> This system is not production ready, and contains bugs, (security risk potentially). -> But have fun on it =) - -# Todos - -- [ ] the todo list lol -- [ ] and much more... +> This is an unstable branch, checkout `release` for the (guaranteed) working version \ No newline at end of file diff --git a/backend/server/src/modules/blog/components/check_update/index.rs b/backend/server/src/modules/blog/components/check_update/index.rs deleted file mode 100644 index 9cf590c..0000000 --- a/backend/server/src/modules/blog/components/check_update/index.rs +++ /dev/null @@ -1,351 +0,0 @@ -use std::error::Error; -use std::path::Path; - -use markdown::MdFile; - -use crate::modules::blog::components::check_update::utils::get_md_file_basename; -use crate::modules::blog::components::check_update::{error::PostIdxError, utils::parse_date}; -use sqlx::{query, PgPool, Pool, Postgres, Row}; -use tokio::runtime::Runtime; -use tracing::{info, warn}; - -async fn base(db_connection: &PgPool, post: &MdFile) -> Result<(), PostIdxError> { - let post_id = post.file_id; - let title = match post.meta.get("title") { - None => { - return Err(PostIdxError::MissingField { - field: "title".to_string(), - filename: post.filename.clone(), - }); - } - Some(title) => title.as_str().unwrap_or_else(|| { - warn!("invalid title for `{}`", &post.filename); - "invalid title" - }), - }; - let sub_title = post - .meta - .get("sub_title") - .and_then(|v| v.as_str()) - .unwrap_or(""); - let summary = post - .meta - .get("summary") - .and_then(|v| v.as_str()) - .unwrap_or(""); - let content = post.content.as_str(); - let category = match post.meta.get("category") { - None => { - return Err(PostIdxError::MissingField { - field: "category".to_string(), - filename: post.filename.clone(), - }); - } - Some(title) => title.as_str().unwrap_or_else(|| { - warn!("invalid category for `{}`", &post.filename); - "invalid category" - }), - }; - let last_update = { - if let Some(last_update) = post.meta.get("last_update") { - match parse_date(last_update.as_str().unwrap()) { - Ok(date) => Some(date), - Err(e) => { - return Err(PostIdxError::InvalidField { - field_name: "last_update".to_string(), - err_field: last_update.to_string(), - filename: post.filename.clone(), - msg: e.to_string(), - }); - } - } - } else { - None - } - }; - let first_update = match post.meta.get("date") { - None => { - return Err(PostIdxError::MissingField { - field: "date".to_string(), - filename: post.filename.clone(), - }); - } - Some(first_update) => match parse_date(first_update.as_str().unwrap()) { - Ok(date) => date, - Err(e) => { - return Err(PostIdxError::InvalidField { - field_name: "date".to_string(), - err_field: first_update.to_string(), - filename: post.filename.clone(), - msg: e.to_string(), - }); - } - }, - }; - let filename = get_md_file_basename(&post.filename); - - if let Err(e) = query( - r##" -INSERT INTO Post (post_id, title, summary, content, last_update, first_update, sub_title, category, filename) -VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) -ON CONFLICT (post_id) DO -UPDATE SET - title = EXCLUDED.title, - summary = EXCLUDED.summary, - content = EXCLUDED.content, - last_update = EXCLUDED.last_update, - first_update = EXCLUDED.first_update, - sub_title = EXCLUDED.sub_title, - category = EXCLUDED.category, - filename = EXCLUDED.filename; - "##, - ) - .bind(post_id) - .bind(title) - .bind(summary) - .bind(content) - .bind(last_update) - .bind(first_update) - .bind(sub_title) - .bind(category) - .bind(filename) - .execute(db_connection) - .await - { - return Err( - PostIdxError::DBWriteFailure { - data_desc: "post base information".to_string(), - db_table: "Post".to_string(), - id: post_id.to_string(), - msg: e.to_string(), - } - ); - } - Ok(()) -} - -async fn tag(db_connection: &PgPool, post: &MdFile) -> Result<(), PostIdxError> { - if let Some(tags_string) = post.meta.get("tags").to_owned() { - let tags = match tags_string.as_array().map(|vec| { - vec.iter() - .map(|val| val.as_str().unwrap()) - .collect::>() - }) { - None => { - return Err(PostIdxError::MissingField { - field: "tags".to_string(), - filename: post.filename.clone(), - }); - } - Some(val) => val, - }; - - let post_id = post.file_id; - - // remove old record - if let Err(e) = query("DELETE FROM Tag WHERE post_id = $1") - .bind(post_id) - .execute(db_connection) - .await - { - return Err(PostIdxError::DBWriteFailure { - data_desc: "post tag information".to_string(), - db_table: "Tag".to_string(), - id: post_id.to_string(), - msg: e.to_string(), - }); - } - - // add new record - for tag in tags { - match query("INSERT INTO Tag VALUES ($1, $2);") - .bind(post_id) - .bind(tag) - .execute(db_connection) - .await - { - Ok(_) => {} - Err(e) => { - return Err(PostIdxError::DBWriteFailure { - data_desc: "post tag information".to_string(), - db_table: "Tag".to_string(), - id: post_id.to_string(), - msg: e.to_string(), - }); - } - } - } - } - - Ok(()) -} - -async fn hash(db_connection: &PgPool, post: &MdFile) -> Result { - let base64_form = post.hash.clone(); - match query("SELECT hash FROM Hash WHERE post_id = $1") - .bind(post.file_id) - .fetch_optional(db_connection) - .await - { - Ok(row) => { - let db_record: String = match row { - None => "".to_string(), - Some(row) => row.try_get("hash").unwrap_or("".to_string()), - }; - if base64_form == db_record { - Ok(false) - } else { - if let Err(e) = query( - r#" -INSERT INTO Hash (post_id, hash) VALUES ($1, $2) - ON CONFLICT (post_id) DO UPDATE SET hash = EXCLUDED.hash"#, - ) - .bind(post.file_id) - .bind(&base64_form) - .execute(db_connection) - .await - { - return Err(PostIdxError::DBWriteFailure { - data_desc: "post hash".to_string(), - db_table: "Hash".to_string(), - id: post.file_id.to_string(), - msg: e.to_string(), - }); - } - Ok(true) - } - } - Err(e) => Err(PostIdxError::DBReadFailure { - data_desc: "post hash".to_string(), - db_table: "Hash".to_string(), - id: post.file_id.to_string(), - msg: e.to_string(), - }), - } -} - -async fn meta(db_connection: &PgPool, post: &MdFile) -> Result<(), PostIdxError> { - if let Err(e) = query( - // todo: sanitization - format!( - r#" -DO -$$ - DECLARE - _post_id UUID := '{}'; - _meta_cols JSONB := '{}'; - _exists BOOLEAN; - _col_name TEXT; - _col_value TEXT; - _sql TEXT; - _insert_columns TEXT := 'post_id'; - _insert_values TEXT := quote_literal(_post_id); - _update_set TEXT := ''; - _blacklist TEXT[] := ARRAY['tags']; - BEGIN - SELECT EXISTS(SELECT 1 FROM meta WHERE post_id = _post_id) INTO _exists; - - FOR _col_name, _col_value IN SELECT key, value FROM jsonb_each_text(_meta_cols) - LOOP - IF _col_name = ANY(_blacklist) THEN - CONTINUE; - END IF; - - IF NOT EXISTS (SELECT 1 - FROM information_schema.columns - WHERE table_name = 'meta' - AND column_name = _col_name - -- may change in the future - AND table_schema = 'public') THEN - _sql := 'ALTER TABLE meta ADD COLUMN ' || quote_ident(_col_name) || ' TEXT'; - EXECUTE _sql; - END IF; - _insert_columns := _insert_columns || ', ' || quote_ident(_col_name); - _insert_values := _insert_values || ', ' || quote_literal(_col_value); - _update_set := _update_set || ', ' || quote_ident(_col_name) || ' = ' || quote_literal(_col_value); - END LOOP; - - _update_set := substring(_update_set FROM 3); - - IF _exists THEN - _sql := 'UPDATE meta SET ' || _update_set || ' WHERE post_id = ' || quote_literal(_post_id); - EXECUTE _sql; - ELSE - _sql := 'INSERT INTO meta (' || _insert_columns || ') VALUES (' || _insert_values || ')'; - EXECUTE _sql; - END IF; - END; -$$ LANGUAGE plpgsql; - "#, post.file_id.to_string().as_str(), post.meta - ).as_str() - ) - .execute(db_connection) - .await { - Err(PostIdxError::DBWriteFailure { - data_desc: "post meta data".to_string(), - db_table: "Meta".to_string(), - id: post.file_id.to_string(), - msg: e.to_string(), - }) - } else { - Ok(()) - } -} - -pub async fn index_file(path: &Path, db_conn: &Pool) -> Result<(), Box> { - let md_file = MdFile::from_file(path)?; - - async fn trail(db_conn: &Pool, md_file: &MdFile) -> Result<(), PostIdxError> { - base(db_conn, md_file).await?; - tag(db_conn, md_file).await?; - meta(db_conn, md_file).await?; - hash(db_conn, md_file).await.map(|_| ()) - } - let mut cnt = 5; - - while let Err(e) = trail(db_conn, &md_file).await { - match e { - PostIdxError::DBWriteFailure { .. } => { - cnt -= 1; - } - PostIdxError::DBReadFailure { .. } => { - cnt -= 1; - } - _ => return Err(e.into()), - } - if cnt <= 0 { - return Err(e.into()); - } - } - - info!("indexed markdown file: {}", path.display()); - Ok(()) -} - -pub fn drop_index(file_basename: &str, db_conn: &Pool) -> Result<(), Box> { - let rt = Runtime::new().unwrap(); - rt.block_on(async { - if let Err(e) = query( - r#" -WITH deleted_post AS ( - DELETE FROM post - WHERE id = (SELECT id FROM post WHERE filename = $1) - RETURNING id -) -DELETE FROM tag WHERE post_id IN (SELECT id FROM deleted_post); -DELETE FROM meta WHERE post_id IN (SELECT id FROM deleted_post); -DELETE FROM hash WHERE post_id IN (SELECT id FROM deleted_post); - "#, - ) - .bind(file_basename) - .execute(db_conn) - .await - { - Err(Box::from( - format!("failed to drop index: {}", e).to_string(), - )) - } else { - Ok(()) - } - }) -}