feat(backend) update (unfinished)

This commit is contained in:
lumither committed 2025-02-22 00:00:13 -08:00
1 parent 84ec754c65
commit 8358a8ae33
20 files changed
+131 -317

No files matched your search

+3 -2
View File
@@ -9,7 +9,8 @@ fn main() {
.peel_to_commit()
.expect("failed to get HEAD commit");
let hash = commit.id().to_string();
let utc_date = Utc::now().date_naive().format("%Y%m%d").to_string();
let build_id = format!("{}@{}", utc_date, &hash[..8]);
let utc_timestamp = Utc::now().date_naive().format("%Y%m%d").to_string();
let pkg_version = env!("CARGO_PKG_VERSION");
let build_id = format!("{}:{}@{}", pkg_version, &hash[..8], utc_timestamp);
println!("cargo:rustc-env=BUILD_ID={}", build_id);
}
-32
View File
@@ -1,32 +0,0 @@
CREATE TABLE tag
(
id SERIAL PRIMARY KEY,
name VARCHAR(20) UNIQUE NOT NULL,
description TEXT
);
CREATE TABLE category
(
id SERIAL PRIMARY KEY,
name VARCHAR(20) UNIQUE NOT NULL,
description TEXT
);
CREATE TABLE post
(
id SERIAL PRIMARY KEY,
identifier VARCHAR(50) UNIQUE NOT NULL,
title VARCHAR(255) NOT NULL,
subtitle VARCHAR(255) NOT NULL,
published_date DATE NOT NULL,
last_update DATE,
content TEXT NOT NULL,
category SERIAL REFERENCES category (id) NOT NULL
);
CREATE TABLE post_tag
(
post SERIAL REFERENCES post (id),
tag SERIAL REFERENCES tag (id)
)
-1
View File
@@ -1,7 +1,6 @@
mod database;
pub mod logger;
mod misc;
mod schema;
use std::{env, panic};
-49
View File
@@ -1,49 +0,0 @@
use sqlx::{query, Pool, Postgres};
use tracing::info;
//
// const POST_BASE_MODEL: &str = r##"
// CREATE TABLE IF NOT EXISTS Post
// (
// post_id UUID PRIMARY KEY,
// title VARCHAR(255) NOT NULL,
// category VARCHAR(255) NOT NULL,
// filename TEXT UNIQUE NOT NULL,
// summary TEXT,
// sub_title TEXT,
// content TEXT NOT NULL,
// last_update TIMESTAMPTZ,
// first_update TIMESTAMPTZ NOT NULL
// );
// "##;
//
// // base model, will add more cols at runtime
// const POST_META_MODEL: &str = r##"
// CREATE TABLE IF NOT EXISTS Meta
// (
// post_id UUID PRIMARY KEY
// );
// "##;
//
// const POST_HASH_MODEL: &str = r##"
// CREATE TABLE IF NOT EXISTS Hash
// (
// post_id UUID PRIMARY KEY,
// hash VARCHAR(44)
// );
// "##;
//
// const TAG_MODEL: &str = r##"
// CREATE TABLE IF NOT EXISTS Tag
// (
// post_id UUID NOT NULL,
// tag VARCHAR(255) NOT NULL
// );
// "##;
pub async fn init(db_connection: &Pool<Postgres>) {
// query(POST_BASE_MODEL).execute(db_connection).await.unwrap();
// query(POST_META_MODEL).execute(db_connection).await.unwrap();
// query(TAG_MODEL).execute(db_connection).await.unwrap();
// query(POST_HASH_MODEL).execute(db_connection).await.unwrap();
info!("database schema initialized");
}
+5 -1
View File
@@ -1,7 +1,11 @@
#[allow(dead_code)]
mod constants;
#[allow(dead_code)]
mod scheduler;
mod init;
mod modules;
mod scheduler;
mod utils;
use crate::init::{logger, start};
@@ -1,27 +0,0 @@
use crate::constants::blog::PAGE_SIZE;
use axum::extract::State;
use axum::Json;
use serde_json::{json, Value};
use sqlx::{query, PgPool, Row};
use tracing::error;
pub async fn get_page_count(
State(db_connection): State<PgPool>,
) -> Result<Json<Value>, Json<Value>> {
match query("select cast(ceil(count(*) / $1) as integer) as res from post")
.bind(PAGE_SIZE as f32)
.fetch_one(&db_connection)
.await
{
Ok(res) => {
let res: i32 = res.get("res");
Ok(Json::from(json!({"res": res})))
}
Err(e) => {
// todo: rename
error!("Failed to get blog page count: {}", e.to_string());
Err(Json::from(json!({"error": e.to_string()})))
}
}
}
@@ -0,0 +1,25 @@
use crate::constants::blog::PAGE_SIZE;
use axum::{extract::State, Json};
use serde_json::{json, Value};
use sqlx::{query, PgPool, Row};
use tracing::error;
const GET_PAGE_COUNT_QUERY: &str = include_str!("post_page_count.sql");
pub async fn handler(State(db_connection): State<PgPool>) -> Result<Json<Value>, Json<Value>> {
match query(GET_PAGE_COUNT_QUERY)
.bind(PAGE_SIZE as i64)
.fetch_one(&db_connection)
.await
{
Ok(res) => {
let res: i32 = res.get("count");
Ok(Json::from(json!({"res": res})))
}
Err(e) => {
error!("failed to get blog page count: {}", e.to_string());
Err(Json::from(json!({"error": e.to_string()})))
}
}
}
@@ -0,0 +1,2 @@
SELECT cast(ceil(count(*) / cast($1 AS FLOAT)) AS INTEGER) AS count
FROM posts;
@@ -1,153 +0,0 @@
use axum::{
extract::{Path, State},
http::StatusCode,
Json,
};
use serde_json::{json, Value};
use sqlx::{query, Error, PgPool, Row};
use std::str::FromStr;
use tracing::error;
use uuid::Uuid;
pub async fn get_post(
State(db_connection): State<PgPool>,
Path(post_id): Path<String>,
) -> Result<Json<Value>, (StatusCode, Json<Value>)> {
let post_id = match Uuid::from_str(post_id.as_str()) {
Ok(id) => id,
Err(e) => {
error!(
"Failed to parse UUID <{}>: {}",
post_id.as_str(),
e.to_string()
);
return Err((
StatusCode::BAD_REQUEST,
Json::from(json!(("error", e.to_string()))),
));
}
};
let post = match query(
r#"
SELECT jsonb_strip_nulls(to_jsonb(t.*)) - 'post_id' AS post
FROM post t
WHERE post_id = $1;
"#,
)
.bind(post_id)
.fetch_one(&db_connection)
.await
{
Ok(value) => value.get::<Value, _>("post"),
Err(e) => {
error!(
"Database error on Reading `base post information` from table `post`: <{}>: {}",
post_id.to_string(),
e.to_string()
);
return match e {
Error::RowNotFound => Err((
StatusCode::BAD_REQUEST,
Json::from(json!({"error": e.to_string()})),
)),
_ => Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json::from(json!({"error": e.to_string()})),
)),
};
}
};
let tags: Vec<_> = match query("SELECT tag FROM Tag WHERE post_id = $1;")
.bind(post_id)
.fetch_all(&db_connection)
.await
{
Ok(tags) => tags.iter().map(|tag| tag.get::<String, _>("tag")).collect(),
Err(e) => {
error!("Database query error :{}", e.to_string());
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json::from(json!({"error": e.to_string()})),
));
}
};
let meta = match query(
r#"
SELECT jsonb_strip_nulls(to_jsonb(t.*)) - 'post_id' AS meta
FROM meta t
WHERE post_id = $1;
"#,
)
.bind(post_id)
.fetch_one(&db_connection)
.await
{
Ok(value) => value.get::<Value, _>("meta"),
Err(e) => {
error!(
"Database error on Reading `{}` from table `{}`: <{}>: {}",
"post meta data",
"meta",
post_id.to_string(),
e.to_string()
);
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json::from(json!({"error": e.to_string()})),
));
}
};
let adjacent = match query(r"
WITH target_post AS (SELECT t.date FROM meta t WHERE post_id = $1),
prev AS (SELECT post_id, t.title, t.sub_title, t.header_img
FROM meta t
WHERE t.date < (SELECT date FROM target_post)
ORDER BY t.date DESC
LIMIT 1),
next AS (SELECT post_id, t.title, t.sub_title, t.header_img
FROM meta t
WHERE t.date > (SELECT date FROM target_post)
ORDER BY t.date
LIMIT 1)
SELECT json_strip_nulls(json_build_object(
'prev',
(SELECT json_build_object('id', post_id, 'title', title, 'sub_title', sub_title, 'header_img', header_img)
FROM prev),
'next',
(SELECT json_build_object('id', post_id, 'title', title, 'sub_title', sub_title, 'header_img', header_img)
FROM next)
)) AS result;
"
)
.bind(post_id)
.fetch_one(&db_connection)
.await
{
Ok(res) => {res.get::<Value, _>("result")}
Err(e) => {
error!(
"Database error on Reading `{}` from table `{}`: <{}>: {}",
"adjoint posts data",
"meta",
post_id.to_string(),
e.to_string()
);
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json::from(json!({"error": e.to_string()})),
));
}
};
let mut res = post.as_object().unwrap().to_owned();
res.insert("tags".to_string(), tags.into());
res.insert("meta".to_string(), meta);
res.insert("adj".to_string(), adjacent);
Ok(Json::from(json!(res)))
}
Whitespace-only changes.
Whitespace-only changes.
@@ -0,0 +1,30 @@
use axum::response::IntoResponse;
use axum::{
extract::{Path, State},
Json,
};
use serde_json::json;
use sqlx::PgPool;
const GET_POST_BY_I64_ID: &str = include_str!("get_post_by_i64_id.sql");
const GET_POST_BY_STR_ID: &str = include_str!("get_post_by_str_id.sql");
pub async fn handler(
State(db_connection): State<PgPool>,
Path(identifier): Path<String>,
) -> impl IntoResponse {
let post = if let Ok(id) = identifier.parse::<i64>() {
select_post_with_i64_id(&db_connection, id).await
} else {
select_post_with_str_id(&db_connection, &identifier).await
};
Json::from(json!({"res": "ok"})).into_response()
}
async fn select_post_with_i64_id(db: &PgPool, id: i64) {
todo!()
}
async fn select_post_with_str_id(db: &PgPool, id: &str) {
todo!()
}
+1 -1
View File
@@ -1,3 +1,3 @@
pub mod get_page_count;
pub mod get_post;
pub mod get_post_list;
// pub mod get_post_list;
+1 -1
View File
@@ -1 +1 @@
pub mod check_update;
// pub mod check_update;
+13 -11
View File
@@ -5,13 +5,14 @@ use crate::{
modules::{
blog::{
api::{
get_page_count::get_page_count, get_post::get_post, get_post_list::get_post_list,
get_page_count,
get_post,
// get_post_list::get_post_list,
},
cron::check_update,
// cron::check_update,
},
Module,
},
scheduler::tasks::basic::BasicTask,
scheduler::tasks::CronTask,
};
@@ -27,11 +28,12 @@ pub struct Blog {
impl Module for Blog {
fn get_server_router(&self) -> Router {
let db_handler = self.db_handler.clone();
// todo: remove new in get func?
// todo: move resource init to new func
// todo: remove debug comment
Router::new()
.route("/get_post_list", get(get_post_list))
.route("/get_page_count", get(get_page_count))
.route("/get_post/:post_id", get(get_post))
// .route("/get_post_list", get(get_post_list))
.route("/get_page_count", get(get_page_count::handler))
.route("/get_post/{identifier}", get(get_post::handler))
.with_state(db_handler)
}
@@ -40,11 +42,11 @@ impl Module for Blog {
}
fn get_cron_tasks(&self) -> Vec<Box<dyn CronTask>> {
let db_handler = self.db_handler.clone();
// let db_handler = self.db_handler.clone();
vec![
BasicTask::new(move || check_update::task(&db_handler), "0/15 * * * * *")
.unwrap()
.to_task(),
// BasicTask::new(move || check_update::task(&db_handler), "0/15 * * * * *")
// .unwrap()
// .to_task(),
]
}
}
@@ -0,0 +1,41 @@
CREATE TABLE tags
(
id SERIAL PRIMARY KEY,
name VARCHAR(20) UNIQUE NOT NULL,
description TEXT
);
CREATE TABLE categories
(
id SERIAL PRIMARY KEY,
name VARCHAR(20) UNIQUE NOT NULL,
description TEXT
);
CREATE TABLE posts
(
id SERIAL PRIMARY KEY,
identifier VARCHAR(50) UNIQUE NOT NULL,
title VARCHAR(255) NOT NULL,
subtitle VARCHAR(255) NOT NULL,
published_date DATE NOT NULL,
last_update DATE,
content TEXT NOT NULL,
category SERIAL REFERENCES categories (id) NOT NULL
);
CREATE TABLE post_tag
(
post SERIAL REFERENCES posts (id),
tag SERIAL REFERENCES tags (id)
);
CREATE TABLE metadata
(
pid SERIAL REFERENCES posts (id) NOT NULL,
meta_key VARCHAR(50) NOT NULL,
meta_value TEXT NOT NULL,
CONSTRAINT meta_pk PRIMARY KEY (pid, meta_key)
);
+9 -38
View File
@@ -16,6 +16,7 @@ pub struct FileDelta {
pub status: Delta,
}
#[allow(dead_code)]
fn do_fetch<'a>(
repo: &'a Repository,
refs: &[&str],
@@ -23,8 +24,6 @@ fn do_fetch<'a>(
) -> Result<AnnotatedCommit<'a>, git2::Error> {
let mut fetch_option = FetchOptions::new();
// Always fetch all tags.
// Perform a download and also update tips
fetch_option.download_tags(AutotagOption::All);
remote.fetch(refs, Some(&mut fetch_option), None)?;
@@ -32,6 +31,7 @@ fn do_fetch<'a>(
repo.reference_to_annotated_commit(&fetch_head)
}
#[allow(dead_code)]
fn fast_forward(
repo: &Repository,
lb: &mut Reference,
@@ -45,16 +45,11 @@ fn fast_forward(
// println!("{}", msg);
lb.set_target(rc.id(), &msg)?;
repo.set_head(&name)?;
repo.checkout_head(Some(
build::CheckoutBuilder::default()
// For some reason, the force is required to make the working directory actually get updated
// I suspect we should be adding some logic to handle dirty working directory states,
// but this is just an example so maybe not.
.force(),
))?;
repo.checkout_head(Some(build::CheckoutBuilder::default().force()))?;
Ok(())
}
#[allow(dead_code)]
fn normal_merge(
repo: &Repository,
local: &AnnotatedCommit,
@@ -73,13 +68,12 @@ fn normal_merge(
return Ok(());
}
let result_tree = repo.find_tree(idx.write_tree_to(repo)?)?;
// now create the merge commit
let msg = format!("merge({}) -> {}", remote.id(), local.id());
let sig = repo.signature()?;
let local_commit = repo.find_commit(local.id())?;
let remote_commit = repo.find_commit(remote.id())?;
// Do our merge commit and set the current branch head to that commit.
let _merge_commit = repo.commit(
Some("HEAD"),
&sig,
@@ -88,34 +82,26 @@ fn normal_merge(
&result_tree,
&[&local_commit, &remote_commit],
)?;
// Set working tree to match head.
repo.checkout_head(None)?;
Ok(())
}
#[allow(dead_code)]
fn do_merge<'a>(
repo: &'a Repository,
remote_branch: &str,
fetch_commit: AnnotatedCommit<'a>,
) -> Result<Vec<FileDelta>, git2::Error> {
// 1. do a merge analysis
let analysis = repo.merge_analysis(&[&fetch_commit])?;
let head_commit = repo.reference_to_annotated_commit(&repo.head()?)?;
// 2. Do the appropriate merge
if analysis.0.is_fast_forward() {
// println!("Doing a fast-forward");
// do a fast-forward
let ref_name = format!("refs/heads/{}", remote_branch);
match repo.find_reference(&ref_name) {
Ok(mut r) => {
fast_forward(repo, &mut r, &fetch_commit)?;
}
Err(_) => {
// The branch doesn't exist, so set the reference to the
// commit directly. Usually this is because you are pulling
// into an empty repository.
repo.reference(
&ref_name,
fetch_commit.id(),
@@ -132,17 +118,12 @@ fn do_merge<'a>(
}
};
} else if analysis.0.is_normal() {
// do a normal merge
normal_merge(repo, &head_commit, &fetch_commit)?;
}
// else {
// // already up-to-date
// println!("Nothing to do...");
// }
calculate_diff(repo, &head_commit, &fetch_commit)
}
#[allow(dead_code)]
fn calculate_diff(
repo: &Repository,
local: &AnnotatedCommit,
@@ -156,23 +137,12 @@ fn calculate_diff(
if let Err(e) = diff.foreach(
&mut |d, _| {
// let delta = d.status();
//
// let n_path = d.new_file().path();
// let o_path = d.old_file().path();
//
// if let Some(path) = n_path {
// updated_files.insert(path.to_owned());
// }
// if let Some(path) = o_path {
// updated_files.insert(path.to_owned());
// }
updated_files.push(FileDelta {
old_path: d.old_file().path().map(PathBuf::from),
new_path: d.new_file().path().map(PathBuf::from),
status: d.status(),
});
true // Continue
true
},
None,
None,
@@ -184,6 +154,7 @@ fn calculate_diff(
Ok(updated_files.into_iter().collect())
}
#[allow(dead_code)]
pub fn sync(
remote_name: &str,
remote_branch: &str,
File renamed without changes.
@@ -1,5 +1,5 @@
[package]
name = "loadbalancer"
name = "gateway"
version = "0.1.0"
edition = "2021"
File renamed without changes.