3 Commits
Author SHA1 Message Date
lumither 12d9ddf8b6 feat(scheduler) fix rustc warning 2025-04-05 18:37:47 -07:00
lumither ac8a2359b0 feat(blog::components::check+update) update 2025-04-05 18:37:23 -07:00
lumither 40abec2e90 feat(blog::components::check+update) update 2025-04-02 13:48:20 -07:00
19 changed files with 304 additions and 97 deletions

No files matched your search

+3 -3
View File
@@ -1,4 +1,4 @@
mod error; pub mod error;
mod utils; mod utils;
use std::{fs, path::Path, str::FromStr}; use std::{fs, path::Path, str::FromStr};
@@ -8,12 +8,12 @@ use crate::{
utils::front_matter::{parse_front_matter, split_md_front_matter}, utils::front_matter::{parse_front_matter, split_md_front_matter},
}; };
use base64::{Engine, engine::general_purpose}; use base64::{engine::general_purpose, Engine};
use serde::Serialize; use serde::Serialize;
use serde_json::Value; use serde_json::Value;
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
#[derive(Default, Debug, Serialize)] #[derive(Default, Debug, Serialize, Clone)]
pub struct MdFile { pub struct MdFile {
pub meta: Value, pub meta: Value,
pub content: String, pub content: String,
+2
View File
@@ -1,3 +1,5 @@
// todo: rebuild with static OnceCell
// Database related // Database related
pub mod database { pub mod database {
pub mod var_name { pub mod var_name {
@@ -0,0 +1 @@
SELECT
@@ -1,9 +1,9 @@
use macros::dev_consume; use macros::dev_consume;
use axum::{ use axum::{
Json,
extract::{Path, State}, extract::{Path, State},
response::IntoResponse, response::IntoResponse,
Json,
}; };
use serde_json::json; use serde_json::json;
use sqlx::PgPool; use sqlx::PgPool;
@@ -0,0 +1,67 @@
use std::path::PathBuf;
use markdown::MdFile;
use uuid::Uuid;
#[allow(dead_code)]
#[derive(Debug)]
pub enum Change {
Delete(Delete),
Move(Move),
Update(Update),
Create(Create),
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct Delete {
pub uuid: Uuid,
pub path: PathBuf,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct Move {
pub uuid: Uuid,
pub from: PathBuf,
pub to: PathBuf,
pub payload: MdFile,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct Update {
pub uuid: Uuid,
pub payload: MdFile,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct Create {
pub uuid: Uuid,
pub path: PathBuf,
pub payload: MdFile,
}
#[derive(Debug, Clone, Default)]
pub struct CreateDelete2UpdateSlot {
pub create: Option<Create>,
pub delete: Option<Delete>,
}
impl CreateDelete2UpdateSlot {
pub fn to_change(&self) -> Option<Change> {
match (&self.create, &self.delete) {
(Some(create), Some(delete)) => Some(Change::Move(Move {
uuid: create.uuid,
from: delete.path.clone(),
to: create.path.clone(),
payload: create.payload.clone(),
})),
(Some(create), None) => Some(Change::Create(create.clone())),
(None, Some(delete)) => Some(Change::Delete(delete.clone())),
(None, None) => None,
}
}
}
@@ -1,7 +1,13 @@
use markdown::error::Error as MarkdownError;
use thiserror::Error; use thiserror::Error;
#[allow(dead_code)]
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum PostIdxError { pub enum Error {
#[error("markdown file error")]
MdFile(#[from] MarkdownError),
#[error("missing essential field `{}` for file `{}`", field, filename)] #[error("missing essential field `{}` for file `{}`", field, filename)]
MissingField { field: String, filename: String }, MissingField { field: String, filename: String },
@@ -46,4 +52,7 @@ pub enum PostIdxError {
id: String, id: String,
msg: String, msg: String,
}, },
#[error("git error")]
Git2(#[from] git2::Error),
} }
@@ -1,26 +1,29 @@
use crate::constants::config::general::{default_value, var_name};
use macros::panic_with_log;
use std::sync::Arc;
use std::{ use std::{
env, env,
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::Arc,
}; };
use crate::modules::blog::components::check_update::{ use crate::{
// index::drop_index, constants::config::general::{default_value, var_name},
// utils::{expand_path, index_files, search_md}, modules::blog::components::check_update::{pull::fetch_deltas, utils::expand_path},
utils::expand_path, scheduler::task_func::AsyncTaskFunc,
}; };
use crate::scheduler::task_func::AsyncTaskFunc; use macros::panic_with_log;
use sqlx::PgPool; use sqlx::PgPool;
use tracing::{info, warn, Level}; use tracing::{trace, warn, Level};
// mod error; // mod error;
// mod index; // mod index;
mod changes;
mod error;
mod pull;
mod utils; mod utils;
#[derive(Debug)] #[derive(Debug)]
struct Args { struct Args {
db_connection: PgPool,
git_work_dir: PathBuf, git_work_dir: PathBuf,
git_url: String, git_url: String,
git_remote_name: String, git_remote_name: String,
@@ -45,7 +48,6 @@ pub fn task(db_connection: PgPool) -> impl AsyncTaskFunc {
}; };
// clone & check update // clone & check update
let git_url = match env::var(var_name::BLOG_GIT_URL) { let git_url = match env::var(var_name::BLOG_GIT_URL) {
Ok(url) => url, Ok(url) => url,
Err(e) => { Err(e) => {
@@ -63,16 +65,33 @@ pub fn task(db_connection: PgPool) -> impl AsyncTaskFunc {
.unwrap_or(default_value::BLOG_GIT_REMOTE_BRANCH.to_string()); .unwrap_or(default_value::BLOG_GIT_REMOTE_BRANCH.to_string());
let arc_args = Arc::new(Args { let arc_args = Arc::new(Args {
db_connection,
git_work_dir, git_work_dir,
git_url, git_url,
git_remote_name, git_remote_name,
git_remote_branch, git_remote_branch,
}); });
move || Box::pin(test(arc_args.clone())) move || Box::pin(workflow(arc_args.clone()))
} }
async fn test(args: Arc<Args>) { async fn workflow(args: Arc<Args>) {
info!("test"); let deltas = match fetch_deltas(
dbg!(&args); &args.git_url,
&args.git_remote_name,
&args.git_remote_branch,
&args.git_work_dir,
)
.await
{
Ok(updates) => updates,
Err(_) => return,
};
if deltas.is_empty() {
trace!("blog git already up-to-date");
return;
}
// todo: load delta to database
} }
@@ -0,0 +1,153 @@
use std::collections::HashMap;
use std::path::PathBuf;
use crate::{
modules::blog::components::check_update::{
changes::{Change, Create, Delete, Update},
error::Error,
},
utils::{git, git::FileDelta},
};
use markdown::MdFile;
use crate::modules::blog::components::check_update::changes::CreateDelete2UpdateSlot;
use git2::{Delta, Repository};
use tracing::error;
use uuid::Uuid;
pub async fn fetch_deltas(
git_url: &str,
git_remote_name: &str,
git_remote_branch: &str,
git_work_dir: &PathBuf,
) -> Result<Vec<Change>, Error> {
let repo = match Repository::open(git_work_dir) {
Ok(repo) => repo,
Err(_) => match Repository::clone(git_url, git_work_dir) {
Ok(repo) => repo,
Err(e) => {
error!("failed to clone blog content git({}): {}", &git_url, e);
return Err(e.into());
}
},
};
let mut remote = repo.find_remote(git_remote_name)?;
let fetched_commit = git::do_fetch(&repo, &[git_remote_branch], &mut remote)?;
let commit_deltas = git::calculate_diff(
&repo,
&repo.reference_to_annotated_commit(&repo.head()?)?,
&fetched_commit,
)?;
// dbg!(&commit_deltas);
let mut changes: Vec<Change> = vec![];
let mut non_delete_deltas: Vec<FileDelta> = vec![];
// process deleted deltas before merging commit to prevent "file not found" os error
for delta in commit_deltas.into_iter() {
if delta.status == Delta::Deleted {
let file_path = &git_work_dir.join(
delta
.new_path
.expect("cannot parse modified post `new_path`"),
);
if let Some(id) = extract_post_id(&MdFile::from_file(file_path)?) {
changes.push(Change::Delete(Delete {
uuid: id,
path: file_path.clone(),
}))
} else {
error!("failed to parse uuid for file {}", file_path.display());
// todo: add to unresolved error list
}
} else {
non_delete_deltas.push(delta);
}
}
git::do_merge(&repo, git_remote_branch, fetched_commit)?;
// process adds and modifies deltas
for delta in non_delete_deltas.into_iter() {
let file_path = &git_work_dir.join(
delta
.new_path
.expect("cannot parse modified post `new_path`"),
);
match delta.status {
Delta::Added => {
let md_file = MdFile::from_file(file_path)?;
if let Some(id) = extract_post_id(&md_file) {
changes.push(Change::Create(Create {
uuid: id,
path: file_path.clone(),
payload: md_file,
}))
} else {
error!("failed to parse uuid for file {}", file_path.display());
// todo: add to unresolved error list
}
}
Delta::Modified => {
let md_file = MdFile::from_file(file_path)?;
if let Some(id) = extract_post_id(&md_file) {
changes.push(Change::Update(Update {
uuid: id,
payload: md_file,
}))
} else {
error!("failed to parse uuid for file {}", file_path.display());
// todo: add to unresolved error list
}
}
Delta::Deleted => continue,
_ => {
error!("unsupported delta type on file {}", file_path.display());
continue;
}
}
}
// merge adds and deletes deltas to moves
let changes = dbg!(merge_changes(changes));
Ok(changes)
}
fn extract_post_id(post: &MdFile) -> Option<Uuid> {
match post.meta["uuid"].as_str() {
Some(id) => match Uuid::parse_str(id) {
Ok(id) => Some(id),
Err(_) => None,
},
None => None,
}
}
fn merge_changes(changes: Vec<Change>) -> Vec<Change> {
let mut slots: HashMap<Uuid, CreateDelete2UpdateSlot> = HashMap::new();
let mut merged: Vec<Change> = Vec::new();
for change in changes {
match change {
Change::Create(create) => {
let id = create.uuid;
slots.entry(id).or_default().create = Some(create);
}
Change::Delete(delete) => {
let id = delete.uuid;
slots.entry(id).or_default().delete = Some(delete);
}
_ => merged.push(change),
}
}
for slot in slots.values() {
if let Some(change) = slot.to_change() {
merged.push(change);
}
}
merged
}
@@ -8,48 +8,6 @@ use chrono::{DateTime, FixedOffset, ParseResult};
use dirs::home_dir; use dirs::home_dir;
use tracing::warn; use tracing::warn;
// pub fn search_md(entry: PathBuf) -> Vec<PathBuf> {
// if entry.ends_with(".git") {
// return vec![];
// }
// if entry.is_dir() {
// if let Ok(files) = entry.read_dir() {
// files
// .flat_map(|file| {
// let file_path = file.unwrap().path();
// search_md(file_path)
// })
// .collect()
// } else {
// vec![]
// }
// } else if entry.extension().is_some_and(|ext| ext == "md") {
// vec![entry]
// } else {
// vec![]
// }
// }
// pub fn index_files(files: &[PathBuf], db_conn: &Pool<Postgres>) {
// let rt = Runtime::new().unwrap();
// rt.block_on(async {
// let files_clone = files.to_owned();
// let handlers = files_clone
// .into_iter()
// .map(|file: PathBuf| {
// let conn_clone = db_conn.clone();
// spawn(async move {
// if let Err(e) = index_file(&file, &conn_clone).await {
// error!("failed to index markdown file `{}`: {}", file.display(), e);
// }
// })
// })
// .collect::<Vec<_>>();
//
// block_on(join_all(handlers));
// });
// }
pub fn expand_path(path: String) -> String { pub fn expand_path(path: String) -> String {
if path.starts_with('~') { if path.starts_with('~') {
home_dir() home_dir()
@@ -75,7 +33,3 @@ pub fn expand_path(path: String) -> String {
pub fn parse_date(date_str: &str) -> ParseResult<DateTime<FixedOffset>> { pub fn parse_date(date_str: &str) -> ParseResult<DateTime<FixedOffset>> {
DateTime::parse_from_str(format!("{}T120000", date_str).as_str(), DEFAULT_DATE_FORMAT) DateTime::parse_from_str(format!("{}T120000", date_str).as_str(), DEFAULT_DATE_FORMAT)
} }
// pub fn get_md_file_basename(f_name: &str) -> String {
// f_name.replace(".md", "")
// }
@@ -1,2 +1,3 @@
pub mod check_update; pub mod check_update;
mod consistency_guard;
mod rag; mod rag;
@@ -0,0 +1 @@
+5 -14
View File
@@ -1,6 +1,8 @@
mod api; mod api;
mod components; mod components;
use crate::modules::blog::components::check_update;
use crate::scheduler::tasks::async_basic::AsyncBasic;
use crate::{ use crate::{
modules::{ modules::{
blog::{ blog::{
@@ -16,9 +18,6 @@ use crate::{
scheduler::tasks::CronTask, scheduler::tasks::CronTask,
}; };
use crate::modules::blog::components::check_update;
use crate::scheduler::tasks::async_basic::AsyncBasic;
use crate::scheduler::tasks::basic::BasicTask;
use axum::{routing::get, Router}; use axum::{routing::get, Router};
use sqlx::PgPool; use sqlx::PgPool;
@@ -46,19 +45,11 @@ impl Module for Blog {
fn get_cron_tasks(&self) -> Vec<(&str, Box<dyn CronTask>)> { fn get_cron_tasks(&self) -> Vec<(&str, Box<dyn CronTask>)> {
let db_handler = self.db_handler.clone(); let db_handler = self.db_handler.clone();
vec![ vec![(
(
"Blog Check Update", "Blog Check Update",
AsyncBasic::new(check_update::task(db_handler.clone()), "* * * * * *") AsyncBasic::new(check_update::task(db_handler.clone()), "*/5 * * * * *")
.unwrap() .unwrap()
.to_task(), .to_task(),
), )]
(
"Sync test task",
BasicTask::new(|| println!("test lol"), "* * * * * *")
.unwrap()
.to_task(),
),
]
} }
} }
+6 -6
View File
@@ -2,20 +2,20 @@ CREATE TABLE tags
( (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
name VARCHAR(20) UNIQUE NOT NULL, name VARCHAR(20) UNIQUE NOT NULL,
description TEXT description TEXT DEFAULT ''
); );
CREATE TABLE categories CREATE TABLE categories
( (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
name VARCHAR(20) UNIQUE NOT NULL, name VARCHAR(20) UNIQUE NOT NULL,
description TEXT description TEXT DEFAULT ''
); );
CREATE TABLE posts CREATE TABLE posts
( (
id SERIAL PRIMARY KEY, id UUID UNIQUE NOT NULL,
identifier VARCHAR(50) UNIQUE NOT NULL, identifier VARCHAR(100) UNIQUE NOT NULL,
title VARCHAR(255) NOT NULL, title VARCHAR(255) NOT NULL,
subtitle VARCHAR(255) NOT NULL, subtitle VARCHAR(255) NOT NULL,
published_date DATE NOT NULL, published_date DATE NOT NULL,
@@ -27,13 +27,13 @@ CREATE TABLE posts
CREATE TABLE post_tag CREATE TABLE post_tag
( (
post SERIAL REFERENCES posts (id), post UUID REFERENCES posts (id),
tag SERIAL REFERENCES tags (id) tag SERIAL REFERENCES tags (id)
); );
CREATE TABLE metadata CREATE TABLE metadata
( (
pid SERIAL REFERENCES posts (id) NOT NULL, pid UUID REFERENCES posts (id) NOT NULL,
meta_key VARCHAR(50) NOT NULL, meta_key VARCHAR(50) NOT NULL,
meta_value TEXT NOT NULL, meta_value TEXT NOT NULL,
+1 -1
View File
@@ -2,7 +2,7 @@ use std::{env, num::ParseIntError};
use crate::{ use crate::{
constants::config::server::{default_value, var_name}, constants::config::server::{default_value, var_name},
scheduler::{tasks::CronTask, Scheduler}, scheduler::{Scheduler, tasks::CronTask},
}; };
use macros::error_panic; use macros::error_panic;
-2
View File
@@ -20,8 +20,6 @@ pub struct Scheduler {
impl Scheduler { impl Scheduler {
/// Start all tasks in the scheduler /// Start all tasks in the scheduler
pub async fn start(&self) { pub async fn start(&self) {
dbg!("entering");
let fut = self let fut = self
.task_pool .task_pool
.clone() .clone()
@@ -2,7 +2,10 @@ use std::pin::Pin;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
#[allow(dead_code)]
pub mod async_basic; pub mod async_basic;
#[allow(dead_code)]
pub mod basic; pub mod basic;
pub trait CronTask: Send + Sync { pub trait CronTask: Send + Sync {
+1 -1
View File
@@ -3,7 +3,7 @@ use std::{sync::Arc, time::Duration};
use crate::scheduler::{error::SchedulerError, tasks::CronTask}; use crate::scheduler::{error::SchedulerError, tasks::CronTask};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use tokio::time::{sleep, Sleep}; use tokio::time::{Sleep, sleep};
pub fn duration_from_now(future: DateTime<Utc>) -> Duration { pub fn duration_from_now(future: DateTime<Utc>) -> Duration {
future future
+15 -8
View File
@@ -1,4 +1,4 @@
/// git merge /// git operations
/// based on https://github.com/rust-lang/git2-rs/blob/master/examples/pull.rs /// based on https://github.com/rust-lang/git2-rs/blob/master/examples/pull.rs
/// https://github.com/rust-lang/git2-rs/commit/f3b87baed1e33d6c2d94fe1fa6aa6503a071d837 /// https://github.com/rust-lang/git2-rs/commit/f3b87baed1e33d6c2d94fe1fa6aa6503a071d837
/// ///
@@ -16,7 +16,7 @@ pub struct FileDelta {
pub status: Delta, pub status: Delta,
} }
fn do_fetch<'a>( pub fn do_fetch<'a>(
repo: &'a Repository, repo: &'a Repository,
refs: &[&str], refs: &[&str],
remote: &'a mut Remote, remote: &'a mut Remote,
@@ -40,7 +40,6 @@ fn fast_forward(
None => String::from_utf8_lossy(lb.name_bytes()).to_string(), None => String::from_utf8_lossy(lb.name_bytes()).to_string(),
}; };
let msg = format!("fast-forward({}) -> {}", name, rc.id()); let msg = format!("fast-forward({}) -> {}", name, rc.id());
// println!("{}", msg);
lb.set_target(rc.id(), &msg)?; lb.set_target(rc.id(), &msg)?;
repo.set_head(&name)?; repo.set_head(&name)?;
repo.checkout_head(Some(build::CheckoutBuilder::default().force()))?; repo.checkout_head(Some(build::CheckoutBuilder::default().force()))?;
@@ -83,11 +82,11 @@ fn normal_merge(
Ok(()) Ok(())
} }
fn do_merge<'a>( pub fn do_merge<'a>(
repo: &'a Repository, repo: &'a Repository,
remote_branch: &str, remote_branch: &str,
fetch_commit: AnnotatedCommit<'a>, fetch_commit: AnnotatedCommit<'a>,
) -> Result<Vec<FileDelta>, git2::Error> { ) -> Result<(), git2::Error> {
let analysis = repo.merge_analysis(&[&fetch_commit])?; let analysis = repo.merge_analysis(&[&fetch_commit])?;
let head_commit = repo.reference_to_annotated_commit(&repo.head()?)?; let head_commit = repo.reference_to_annotated_commit(&repo.head()?)?;
@@ -116,10 +115,11 @@ fn do_merge<'a>(
} else if analysis.0.is_normal() { } else if analysis.0.is_normal() {
normal_merge(repo, &head_commit, &fetch_commit)?; normal_merge(repo, &head_commit, &fetch_commit)?;
} }
calculate_diff(repo, &head_commit, &fetch_commit)
Ok(())
} }
fn calculate_diff( pub fn calculate_diff(
repo: &Repository, repo: &Repository,
local: &AnnotatedCommit, local: &AnnotatedCommit,
remote: &AnnotatedCommit, remote: &AnnotatedCommit,
@@ -149,6 +149,7 @@ fn calculate_diff(
Ok(updated_files.into_iter().collect()) Ok(updated_files.into_iter().collect())
} }
#[allow(dead_code)]
pub fn sync( pub fn sync(
remote_name: &str, remote_name: &str,
remote_branch: &str, remote_branch: &str,
@@ -156,5 +157,11 @@ pub fn sync(
) -> Result<Vec<FileDelta>, git2::Error> { ) -> Result<Vec<FileDelta>, git2::Error> {
let mut remote = repo.find_remote(remote_name)?; let mut remote = repo.find_remote(remote_name)?;
let fetch_commit = do_fetch(repo, &[remote_branch], &mut remote)?; let fetch_commit = do_fetch(repo, &[remote_branch], &mut remote)?;
do_merge(repo, remote_branch, fetch_commit) let delta = calculate_diff(
repo,
&repo.reference_to_annotated_commit(&repo.head()?)?,
&fetch_commit,
);
do_merge(repo, remote_branch, fetch_commit)?;
delta
} }