feat(blog::components::check+update) update

This commit is contained in:
lumither committed 2025-04-02 13:48:20 -07:00
1 parent 1038351f4a
commit 40abec2e90
17 files changed
+234 -86

No files matched your search

@@ -0,0 +1,30 @@
use git2::Delta;
use std::path::PathBuf;
use uuid::Uuid;
#[derive(Debug)]
pub enum Change {
DELETE {
uuid: Uuid,
},
RENAME {
uuid: Uuid,
from: PathBuf,
to: PathBuf,
},
UPDATE {
uuid: Uuid,
content: String,
},
CREATE {
uuid: Uuid,
path: PathBuf,
content: String,
},
}
#[derive(Debug, Clone)]
pub struct DeltaIdx {
pub delta_type: Delta,
pub content: String,
}
@@ -1,7 +1,7 @@
use thiserror::Error;
#[derive(Error, Debug)]
pub enum PostIdxError {
pub enum Error {
#[error("missing essential field `{}` for file `{}`", field, filename)]
MissingField { field: String, filename: String },
@@ -46,4 +46,7 @@ pub enum PostIdxError {
id: String,
msg: String,
},
#[error("git error")]
Git2(#[from] git2::Error),
}
@@ -6,6 +6,7 @@ use std::{
path::{Path, PathBuf},
};
use crate::modules::blog::components::check_update::pull::{analyze_deltas, fetch_deltas};
use crate::modules::blog::components::check_update::{
// index::drop_index,
// utils::{expand_path, index_files, search_md},
@@ -13,14 +14,18 @@ use crate::modules::blog::components::check_update::{
};
use crate::scheduler::task_func::AsyncTaskFunc;
use sqlx::PgPool;
use tracing::{info, warn, Level};
use tracing::{info, trace, warn, Level};
// mod error;
// mod index;
mod changes;
mod error;
mod pull;
mod utils;
#[derive(Debug)]
struct Args {
db_connection: PgPool,
git_work_dir: PathBuf,
git_url: String,
git_remote_name: String,
@@ -45,7 +50,6 @@ pub fn task(db_connection: PgPool) -> impl AsyncTaskFunc {
};
// clone & check update
let git_url = match env::var(var_name::BLOG_GIT_URL) {
Ok(url) => url,
Err(e) => {
@@ -63,16 +67,33 @@ pub fn task(db_connection: PgPool) -> impl AsyncTaskFunc {
.unwrap_or(default_value::BLOG_GIT_REMOTE_BRANCH.to_string());
let arc_args = Arc::new(Args {
db_connection,
git_work_dir,
git_url,
git_remote_name,
git_remote_branch,
});
move || Box::pin(test(arc_args.clone()))
move || Box::pin(workflow(arc_args.clone()))
}
async fn test(args: Arc<Args>) {
info!("test");
dbg!(&args);
async fn workflow(args: Arc<Args>) {
let deltas = match fetch_deltas(
&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;
}
let updates = analyze_deltas(deltas, &args.git_work_dir).await;
}
@@ -0,0 +1,147 @@
use crate::modules::blog::components::check_update::changes::{Change, DeltaIdx};
use crate::modules::blog::components::check_update::error::Error;
use crate::utils::git;
use crate::utils::git::FileDelta;
use git2::{Delta, Repository};
use markdown::MdFile;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use tracing::error;
use uuid::Uuid;
pub async fn fetch_deltas(
git_url: &str,
git_remote_name: &str,
git_branch: &str,
git_work_dir: &PathBuf,
) -> Result<Vec<FileDelta>, 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 updates = match git::sync(git_remote_name, git_branch, &repo) {
Ok(updates) => updates,
Err(e) => {
error!(
"failed to sync blog content git({}) updates: {}",
&git_url, e
);
return Err(e.into());
}
};
Ok(updates)
}
pub async fn analyze_deltas(file_deltas: Vec<FileDelta>, git_work_dir: &Path) -> Vec<Change> {
dbg!(&file_deltas);
let mut changes: Vec<Change> = vec![];
let mut merge_deltas: Vec<FileDelta> = vec![];
for delta in file_deltas.into_iter() {
if delta.status == Delta::Modified {
let file_path = &git_work_dir.join(
delta
.new_path
.expect("cannot parse modified post `new_path`"),
);
let post = match MdFile::from_file(file_path) {
Ok(post) => post,
Err(e) => {
error!("md file parsing error: {}", e);
continue;
}
};
let id = match extract_post_id(&post) {
Some(id) => id,
None => {
// todo: add to unresolved error list
error!(
"failed to parse uuid on post {}",
file_path.to_string_lossy()
);
continue;
}
};
changes.push(Change::UPDATE {
uuid: id,
content: post.content,
})
} else {
merge_deltas.push(delta)
};
}
// merge some addition and remove deltas to rename/move changes
let delta_idx = {
let mut delta_idx: HashMap<Uuid, Vec<DeltaIdx>> = HashMap::new();
for file_delta in merge_deltas.into_iter() {
let file_path = &git_work_dir.join(
file_delta
.new_path
.expect("cannot parse modified post `new_path`"),
);
match file_delta.status {
// todo: split impl
Delta::Added | Delta::Deleted => {
let file_path_str = file_path.to_string_lossy();
let post = match MdFile::from_file(file_path) {
Ok(post) => post,
Err(e) => {
error!("md file parsing error ({}): {}", file_path_str, e);
continue;
}
};
let id = match extract_post_id(&post) {
Some(id) => id,
None => {
// todo: add to unresolved error list
error!("failed to parse uuid on post {}", file_path_str);
continue;
}
};
let file_delta_idx = DeltaIdx {
delta_type: file_delta.status,
content: post.content,
};
delta_idx
.entry(id)
// todo: change to shallow copy
.and_modify(|v| v.push(file_delta_idx.clone()))
.or_insert(vec![file_delta_idx]);
}
_ => {
error!("unsupported delta on file {}", file_path.to_string_lossy());
continue;
}
};
}
delta_idx
};
dbg!(&delta_idx);
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,
}
}
@@ -8,48 +8,6 @@ use chrono::{DateTime, FixedOffset, ParseResult};
use dirs::home_dir;
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 {
if path.starts_with('~') {
home_dir()
@@ -75,7 +33,3 @@ pub fn expand_path(path: String) -> String {
pub fn parse_date(date_str: &str) -> ParseResult<DateTime<FixedOffset>> {
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", "")
// }