mirror of
https://github.com/Lumither/nocturne.git
synced 2026-10-17 10:26:21 +00:00
Compare commits
6
Commits
12d9ddf8b6
...
9270abc4b7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9270abc4b7 | ||
|
|
b11b19b0f5 | ||
|
|
7d988d380b | ||
|
|
8ba348a93e | ||
|
|
10f14312bd | ||
|
|
27d2c86106 |
No files matched your search
@@ -8,7 +8,7 @@ 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::general_purpose, Engine};
|
use base64::{Engine, engine::general_purpose};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ tracing = { version = "0.1.41", features = ["log"] }
|
|||||||
git2 = "0.20.0"
|
git2 = "0.20.0"
|
||||||
chrono = { version = "0.4.39", features = ["serde"] }
|
chrono = { version = "0.4.39", features = ["serde"] }
|
||||||
cron = "0.15.0"
|
cron = "0.15.0"
|
||||||
sqlx = { version = "0.8.3", features = ["runtime-tokio", "tls-native-tls", "postgres", "chrono"] }
|
sqlx = { version = "0.8.3", features = ["runtime-tokio", "tls-native-tls", "postgres", "chrono", "uuid"] }
|
||||||
serde_json = "1.0.133"
|
serde_json = "1.0.133"
|
||||||
serde = { version = "1.0.215", features = ["derive"] }
|
serde = { version = "1.0.215", features = ["derive"] }
|
||||||
tracing-subscriber = "0.3.18"
|
tracing-subscriber = "0.3.18"
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
pub const DEFAULT_TIME_FORMAT: &str = "%Y-%m-%dT%H:%M:%S%z";
|
pub const DEFAULT_TIME_FORMAT: &str = "%Y-%m-%dT%H:%M:%S%z";
|
||||||
pub const DEFAULT_DATE_FORMAT: &str = "%Y-%m-%d%zT%H%M%S";
|
|
||||||
|
pub const DEFAULT_NAIVE_DATE_FORMAT: &str = "%Y-%m-%d";
|
||||||
@@ -7,28 +7,29 @@ use axum::{
|
|||||||
};
|
};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
const GET_POST_BY_I64_ID: &str = include_str!("get_post_by_i64_id.sql");
|
const GET_POST_BY_UUID: &str = include_str!("get_post_by_uuid.sql");
|
||||||
const GET_POST_BY_STR_ID: &str = include_str!("get_post_by_str_id.sql");
|
const GET_POST_BY_STR_ID: &str = include_str!("get_post_by_str_id.sql");
|
||||||
|
|
||||||
pub async fn handler(
|
pub async fn handler(
|
||||||
State(db_connection): State<PgPool>,
|
State(db_connection): State<PgPool>,
|
||||||
Path(identifier): Path<String>,
|
Path(identifier): Path<String>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
if let Ok(id) = identifier.parse::<i64>() {
|
if let Ok(id) = identifier.parse::<Uuid>() {
|
||||||
select_post_with_i64_id(&db_connection, id).await
|
select_post_by_uuid(&db_connection, id).await
|
||||||
} else {
|
} else {
|
||||||
select_post_with_str_id(&db_connection, &identifier).await
|
select_post_by_str_id(&db_connection, &identifier).await
|
||||||
};
|
};
|
||||||
Json::from(json!({"res": "ok"})).into_response()
|
Json::from(json!({"res": "ok"})).into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn select_post_with_i64_id(db: &PgPool, id: i64) {
|
async fn select_post_by_uuid(db: &PgPool, id: Uuid) {
|
||||||
dev_consume!(db, id);
|
dev_consume!(db, id);
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn select_post_with_str_id(db: &PgPool, id: &str) {
|
async fn select_post_by_str_id(db: &PgPool, id: &str) {
|
||||||
dev_consume!(db, id);
|
dev_consume!(db, id);
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
use crate::modules::blog::components::check_update::{
|
||||||
|
changes::{Change, Create, Delete, Move, Update},
|
||||||
|
error::Error,
|
||||||
|
utils::{parse_naive_date_str, parse_post_identifier},
|
||||||
|
};
|
||||||
|
use macros::dev_consume;
|
||||||
|
|
||||||
|
use futures::{
|
||||||
|
future::{join_all, BoxFuture},
|
||||||
|
FutureExt,
|
||||||
|
};
|
||||||
|
use sqlx::{query, PgPool, Postgres, Row, Transaction};
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
|
const DELETE_POST_BY_ID: &str = include_str!("sql/delete_post_by_id.sql");
|
||||||
|
const GET_TAG_ID: &str = include_str!("sql/get_tag_id.sql");
|
||||||
|
const GET_CATEGORY_ID: &str = include_str!("sql/get_category_id.sql");
|
||||||
|
const CREATE_RETURN_TAG_ID: &str = include_str!("sql/create_return_tag_id.sql");
|
||||||
|
const CREATE_RETURN_CATEGORY_ID: &str = include_str!("sql/create_return_category_id.sql");
|
||||||
|
const CREATE_POST: &str = include_str!("sql/create_post.sql");
|
||||||
|
|
||||||
|
pub async fn apply_deltas(db_conn: &PgPool, changes: Vec<Change>) {
|
||||||
|
let handlers = changes
|
||||||
|
.into_iter()
|
||||||
|
.map(|change| match change {
|
||||||
|
Change::Delete(del) => handle_delete(db_conn, del).boxed(),
|
||||||
|
Change::Move(mv) => handle_move(db_conn, mv).boxed(),
|
||||||
|
Change::Update(update) => handle_update(db_conn, update).boxed(),
|
||||||
|
Change::Create(create) => handle_create(db_conn, create).boxed(),
|
||||||
|
})
|
||||||
|
.collect::<Vec<BoxFuture<'_, ()>>>();
|
||||||
|
|
||||||
|
join_all(handlers).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_delete(db: &PgPool, del: Delete) {
|
||||||
|
if let Err(e) = query(DELETE_POST_BY_ID).bind(del.uuid).execute(db).await {
|
||||||
|
error!(
|
||||||
|
"failed to delete post: {}, {}",
|
||||||
|
del.uuid.to_string(),
|
||||||
|
e.to_string()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_move(db: &PgPool, mv: Move) {
|
||||||
|
let identifier = parse_post_identifier(&mv.to).unwrap();
|
||||||
|
|
||||||
|
dev_consume!(db, mv, identifier);
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_update(db: &PgPool, update: Update) {
|
||||||
|
dev_consume!(db, update);
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_create(db: &PgPool, create: Create) {
|
||||||
|
let mut tx = db.begin().await.unwrap();
|
||||||
|
|
||||||
|
let identifier = parse_post_identifier(&create.path).unwrap();
|
||||||
|
|
||||||
|
let tags_json = create.payload.meta["tags"].clone();
|
||||||
|
let tags: Vec<_> = if let Some(tags) = tags_json.as_array() {
|
||||||
|
tags.iter().filter_map(|tag| tag.as_str()).collect()
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
};
|
||||||
|
let mut tag_ids = vec![];
|
||||||
|
for tag in tags {
|
||||||
|
match get_or_create_tag_id(&mut tx, tag).await {
|
||||||
|
Ok(tid) => tag_ids.push(tid),
|
||||||
|
Err(e) => {
|
||||||
|
error!("failed to fetch or create tag ({}): {}", tag, e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let category_json = create.payload.meta["category"].clone();
|
||||||
|
let category = category_json.as_str().unwrap_or("Uncategorized");
|
||||||
|
let category_id = match get_or_create_category_id(&mut tx, category).await {
|
||||||
|
Ok(id) => id,
|
||||||
|
Err(e) => {
|
||||||
|
error!(
|
||||||
|
"failed to fetch category id for post {}: {}",
|
||||||
|
&identifier, e
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let meta = create.payload.meta;
|
||||||
|
let post_id = create.uuid;
|
||||||
|
let post_identifier = &identifier;
|
||||||
|
let post_title = meta
|
||||||
|
.get("title")
|
||||||
|
.map(|t| t.as_str().unwrap())
|
||||||
|
.unwrap_or("Untitled");
|
||||||
|
let post_subtitle = meta
|
||||||
|
.get("sub_title")
|
||||||
|
.map(|t| t.as_str().unwrap())
|
||||||
|
.unwrap_or("");
|
||||||
|
let post_date_created = if let Some(Some(date)) = meta
|
||||||
|
.get("date")
|
||||||
|
.map(|d| parse_naive_date_str(d.as_str().unwrap_or("")))
|
||||||
|
{
|
||||||
|
date
|
||||||
|
} else {
|
||||||
|
error!("failed to parse `date` for post: {}", post_identifier);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let post_date_updated = meta
|
||||||
|
.get("update")
|
||||||
|
.map(|d| parse_naive_date_str(d.as_str().unwrap_or("")))
|
||||||
|
.unwrap_or(None);
|
||||||
|
let post_status = meta.get("status").map(|v| v.as_str()).unwrap_or(None);
|
||||||
|
let post_content = create.payload.content;
|
||||||
|
|
||||||
|
if let Err(e) = query(CREATE_POST)
|
||||||
|
.bind(post_id)
|
||||||
|
.bind(post_identifier)
|
||||||
|
.bind(post_title)
|
||||||
|
.bind(post_subtitle)
|
||||||
|
.bind(post_date_created)
|
||||||
|
.bind(post_date_updated)
|
||||||
|
.bind(post_status)
|
||||||
|
.bind(post_content)
|
||||||
|
.bind(category_id)
|
||||||
|
.bind(tag_ids)
|
||||||
|
.execute(&mut *tx)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
error!("failed to insert post {}: {}", &identifier, e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.commit().await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_or_create_tag_id(
|
||||||
|
db: &mut Transaction<'_, Postgres>,
|
||||||
|
tag_name: &str,
|
||||||
|
) -> Result<i32, Error> {
|
||||||
|
get_create_id(db, tag_name, GET_TAG_ID, CREATE_RETURN_TAG_ID).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_or_create_category_id(
|
||||||
|
db: &mut Transaction<'_, Postgres>,
|
||||||
|
category_name: &str,
|
||||||
|
) -> Result<i32, Error> {
|
||||||
|
get_create_id(
|
||||||
|
db,
|
||||||
|
category_name,
|
||||||
|
GET_CATEGORY_ID,
|
||||||
|
CREATE_RETURN_CATEGORY_ID,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_create_id(
|
||||||
|
db: &mut Transaction<'_, Postgres>,
|
||||||
|
field_name: &str,
|
||||||
|
fetch_query: &str,
|
||||||
|
create_fetch_query: &str,
|
||||||
|
) -> Result<i32, Error> {
|
||||||
|
if let Some(id) = query(fetch_query)
|
||||||
|
.bind(field_name)
|
||||||
|
.fetch_optional(&mut **db)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
Ok(id.get("id"))
|
||||||
|
} else {
|
||||||
|
let id = query(create_fetch_query)
|
||||||
|
.bind(field_name)
|
||||||
|
.fetch_one(&mut **db)
|
||||||
|
.await?
|
||||||
|
.get("id");
|
||||||
|
Ok(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,57 +2,17 @@ use markdown::error::Error as MarkdownError;
|
|||||||
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[error("markdown file error")]
|
#[error("markdown file error")]
|
||||||
MdFile(#[from] MarkdownError),
|
MdFile(#[from] MarkdownError),
|
||||||
|
|
||||||
#[error("missing essential field `{}` for file `{}`", field, filename)]
|
#[error("db error")]
|
||||||
MissingField { field: String, filename: String },
|
Db(#[from] sqlx::Error),
|
||||||
|
|
||||||
#[error(
|
|
||||||
"invalid field `{}` for file `{}`, <{}>: {}",
|
|
||||||
field_name,
|
|
||||||
filename,
|
|
||||||
err_field,
|
|
||||||
msg
|
|
||||||
)]
|
|
||||||
InvalidField {
|
|
||||||
field_name: String,
|
|
||||||
err_field: String,
|
|
||||||
filename: String,
|
|
||||||
msg: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
#[error(
|
|
||||||
"database error on writing `{}` to table `{}`: <{}>: {}",
|
|
||||||
data_desc,
|
|
||||||
db_table,
|
|
||||||
id,
|
|
||||||
msg
|
|
||||||
)]
|
|
||||||
DBWriteFailure {
|
|
||||||
data_desc: String,
|
|
||||||
db_table: String,
|
|
||||||
id: String,
|
|
||||||
msg: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
#[error(
|
|
||||||
"Database error on Reading `{}` from table `{}`: <{}>: {}",
|
|
||||||
data_desc,
|
|
||||||
db_table,
|
|
||||||
id,
|
|
||||||
msg
|
|
||||||
)]
|
|
||||||
DBReadFailure {
|
|
||||||
data_desc: String,
|
|
||||||
db_table: String,
|
|
||||||
id: String,
|
|
||||||
msg: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
#[error("git error")]
|
#[error("git error")]
|
||||||
Git2(#[from] git2::Error),
|
Git2(#[from] git2::Error),
|
||||||
|
|
||||||
|
#[error("git remote url mismatch")]
|
||||||
|
GitUrlMismatch,
|
||||||
}
|
}
|
||||||
@@ -6,16 +6,19 @@ use std::{
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
constants::config::general::{default_value, var_name},
|
constants::config::general::{default_value, var_name},
|
||||||
modules::blog::components::check_update::{pull::fetch_deltas, utils::expand_path},
|
modules::blog::components::check_update::{
|
||||||
|
apply::apply_deltas, pull::fetch_deltas, utils::expand_path,
|
||||||
|
},
|
||||||
scheduler::task_func::AsyncTaskFunc,
|
scheduler::task_func::AsyncTaskFunc,
|
||||||
};
|
};
|
||||||
use macros::panic_with_log;
|
use macros::panic_with_log;
|
||||||
|
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
use tracing::{trace, warn, Level};
|
use tracing::{log::error, trace, warn, Level};
|
||||||
|
|
||||||
// mod error;
|
// mod error;
|
||||||
// mod index;
|
// mod index;
|
||||||
|
mod apply;
|
||||||
mod changes;
|
mod changes;
|
||||||
mod error;
|
mod error;
|
||||||
mod pull;
|
mod pull;
|
||||||
@@ -85,7 +88,10 @@ async fn workflow(args: Arc<Args>) {
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(updates) => updates,
|
Ok(updates) => updates,
|
||||||
Err(_) => return,
|
Err(e) => {
|
||||||
|
error!("delta parsing error: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if deltas.is_empty() {
|
if deltas.is_empty() {
|
||||||
@@ -93,5 +99,5 @@ async fn workflow(args: Arc<Args>) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo: load delta to database
|
apply_deltas(&args.db_connection, deltas).await;
|
||||||
}
|
}
|
||||||
@@ -1,18 +1,16 @@
|
|||||||
use std::collections::HashMap;
|
use std::{collections::HashMap, fs::remove_dir_all, path::PathBuf};
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
modules::blog::components::check_update::{
|
modules::blog::components::check_update::{
|
||||||
changes::{Change, Create, Delete, Update},
|
changes::{Change, Create, CreateDelete2UpdateSlot, Delete, Update},
|
||||||
error::Error,
|
error::Error,
|
||||||
},
|
},
|
||||||
utils::{git, git::FileDelta},
|
utils::{git, git::FileDelta},
|
||||||
};
|
};
|
||||||
use markdown::MdFile;
|
use markdown::MdFile;
|
||||||
|
|
||||||
use crate::modules::blog::components::check_update::changes::CreateDelete2UpdateSlot;
|
|
||||||
use git2::{Delta, Repository};
|
use git2::{Delta, Repository};
|
||||||
use tracing::error;
|
use tracing::{error, warn};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
pub async fn fetch_deltas(
|
pub async fn fetch_deltas(
|
||||||
@@ -32,7 +30,27 @@ pub async fn fetch_deltas(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut remote = repo.find_remote(git_remote_name)?;
|
let mut remote = match repo.find_remote(git_remote_name) {
|
||||||
|
Ok(remote) => remote,
|
||||||
|
Err(e) => {
|
||||||
|
remove_dir_all(git_work_dir).unwrap();
|
||||||
|
warn!(
|
||||||
|
"cannot find git remote ({}), reinitializing: {}",
|
||||||
|
git_remote_name, e
|
||||||
|
);
|
||||||
|
return Err(e.into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if remote.url().is_some_and(|url| url != git_url) {
|
||||||
|
remove_dir_all(git_work_dir).unwrap();
|
||||||
|
warn!(
|
||||||
|
"git remote url mismatch (expected: {}, actual: {}), reinitializing",
|
||||||
|
git_url,
|
||||||
|
remote.url().unwrap()
|
||||||
|
);
|
||||||
|
return Err(Error::GitUrlMismatch);
|
||||||
|
}
|
||||||
|
|
||||||
let fetched_commit = git::do_fetch(&repo, &[git_remote_branch], &mut remote)?;
|
let fetched_commit = git::do_fetch(&repo, &[git_remote_branch], &mut remote)?;
|
||||||
let commit_deltas = git::calculate_diff(
|
let commit_deltas = git::calculate_diff(
|
||||||
&repo,
|
&repo,
|
||||||
@@ -46,6 +64,8 @@ pub async fn fetch_deltas(
|
|||||||
|
|
||||||
// process deleted deltas before merging commit to prevent "file not found" os error
|
// process deleted deltas before merging commit to prevent "file not found" os error
|
||||||
for delta in commit_deltas.into_iter() {
|
for delta in commit_deltas.into_iter() {
|
||||||
|
// todo: limit scanning scope to post only OR extract merging logic to external component
|
||||||
|
|
||||||
if delta.status == Delta::Deleted {
|
if delta.status == Delta::Deleted {
|
||||||
let file_path = &git_work_dir.join(
|
let file_path = &git_work_dir.join(
|
||||||
delta
|
delta
|
||||||
@@ -117,10 +137,7 @@ pub async fn fetch_deltas(
|
|||||||
|
|
||||||
fn extract_post_id(post: &MdFile) -> Option<Uuid> {
|
fn extract_post_id(post: &MdFile) -> Option<Uuid> {
|
||||||
match post.meta["uuid"].as_str() {
|
match post.meta["uuid"].as_str() {
|
||||||
Some(id) => match Uuid::parse_str(id) {
|
Some(id) => Uuid::parse_str(id).ok(),
|
||||||
Ok(id) => Some(id),
|
|
||||||
Err(_) => None,
|
|
||||||
},
|
|
||||||
None => None,
|
None => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
INSERT INTO posts
|
||||||
|
(id, identifier, title, subtitle, date_created, date_updated, status, content, category)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9);
|
||||||
|
|
||||||
|
INSERT INTO post_tag (post, tag)
|
||||||
|
SELECT $1, tag
|
||||||
|
FROM UNNEST($10) AS t(tag);
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
INSERT INTO categories(name)
|
||||||
|
VALUES ($1)
|
||||||
|
RETURNING id;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
INSERT INTO tags(name)
|
||||||
|
VALUES ($1)
|
||||||
|
RETURNING id;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
SELECT id
|
||||||
|
FROM categories
|
||||||
|
WHERE name = $1;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
SELECT id
|
||||||
|
FROM tags
|
||||||
|
WHERE name = $1;
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
|
use std::path::Path;
|
||||||
|
|
||||||
use crate::constants::{
|
use crate::constants::{
|
||||||
config::general::{default_value, var_name},
|
config::general::{default_value, var_name},
|
||||||
time::DEFAULT_DATE_FORMAT,
|
time::DEFAULT_NAIVE_DATE_FORMAT,
|
||||||
};
|
};
|
||||||
|
|
||||||
// use crate::modules::blog::components::check_update::index::index_file;
|
use chrono::NaiveDate;
|
||||||
use chrono::{DateTime, FixedOffset, ParseResult};
|
|
||||||
use dirs::home_dir;
|
use dirs::home_dir;
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
|
|
||||||
@@ -30,6 +31,28 @@ pub fn expand_path(path: String) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_date(date_str: &str) -> ParseResult<DateTime<FixedOffset>> {
|
pub fn parse_post_identifier(path: &Path) -> Option<String> {
|
||||||
DateTime::parse_from_str(format!("{}T120000", date_str).as_str(), DEFAULT_DATE_FORMAT)
|
if let Some(comp) = path.components().nth_back(1) {
|
||||||
|
comp.as_os_str().to_str().map(|s| s.to_string())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_naive_date_str(date_str: &str) -> Option<NaiveDate> {
|
||||||
|
NaiveDate::parse_from_str(date_str, DEFAULT_NAIVE_DATE_FORMAT).ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::modules::blog::components::check_update::utils::parse_naive_date_str;
|
||||||
|
use chrono::NaiveDate;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_naive_date() {
|
||||||
|
assert_eq!(
|
||||||
|
NaiveDate::from_ymd_opt(2015, 1, 1),
|
||||||
|
parse_naive_date_str("2015-1-01")
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@ use crate::modules::blog::components::check_update;
|
|||||||
use crate::scheduler::tasks::async_basic::AsyncBasic;
|
use crate::scheduler::tasks::async_basic::AsyncBasic;
|
||||||
use crate::{
|
use crate::{
|
||||||
modules::{
|
modules::{
|
||||||
|
Module,
|
||||||
blog::{
|
blog::{
|
||||||
api::{
|
api::{
|
||||||
get_page_count,
|
get_page_count,
|
||||||
@@ -13,12 +14,11 @@ use crate::{
|
|||||||
},
|
},
|
||||||
// cron::check_update,
|
// cron::check_update,
|
||||||
},
|
},
|
||||||
Module,
|
|
||||||
},
|
},
|
||||||
scheduler::tasks::CronTask,
|
scheduler::tasks::CronTask,
|
||||||
};
|
};
|
||||||
|
|
||||||
use axum::{routing::get, Router};
|
use axum::{Router, routing::get};
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
|
|
||||||
const MOUNT_POINT: &str = "/blog";
|
const MOUNT_POINT: &str = "/blog";
|
||||||
|
|||||||
@@ -12,30 +12,54 @@ CREATE TABLE categories
|
|||||||
description TEXT DEFAULT ''
|
description TEXT DEFAULT ''
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE post_status
|
||||||
|
(
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
name VARCHAR(20) UNIQUE NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO post_status (name)
|
||||||
|
VALUES ('draft'),
|
||||||
|
('published'),
|
||||||
|
('planned'),
|
||||||
|
('hidden');
|
||||||
|
|
||||||
CREATE TABLE posts
|
CREATE TABLE posts
|
||||||
(
|
(
|
||||||
id UUID UNIQUE NOT NULL,
|
id UUID PRIMARY KEY,
|
||||||
identifier VARCHAR(100) 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,
|
date_created DATE NOT NULL,
|
||||||
last_update DATE,
|
date_updated DATE,
|
||||||
content TEXT NOT NULL,
|
status INTEGER REFERENCES post_status (id) DEFAULT 1 NOT NULL,
|
||||||
|
content TEXT NOT NULL,
|
||||||
category SERIAL REFERENCES categories (id) NOT NULL
|
category INTEGER REFERENCES categories (id) NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE post_tag
|
CREATE TABLE post_tag
|
||||||
(
|
(
|
||||||
post UUID REFERENCES posts (id),
|
post UUID REFERENCES posts (id) ON DELETE CASCADE,
|
||||||
tag SERIAL REFERENCES tags (id)
|
tag INTEGER REFERENCES tags (id) ON DELETE CASCADE,
|
||||||
|
|
||||||
|
CONSTRAINT post_tag_pk PRIMARY KEY (post, tag)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE metadata
|
CREATE TABLE metadata
|
||||||
(
|
(
|
||||||
pid UUID REFERENCES posts (id) NOT NULL,
|
pid UUID REFERENCES posts (id) ON DELETE CASCADE NOT NULL,
|
||||||
meta_key VARCHAR(50) NOT NULL,
|
meta_key VARCHAR(50) NOT NULL,
|
||||||
meta_value TEXT NOT NULL,
|
meta_value TEXT NOT NULL,
|
||||||
|
|
||||||
CONSTRAINT meta_pk PRIMARY KEY (pid, meta_key)
|
CONSTRAINT metadata_pk PRIMARY KEY (pid, meta_key)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- index
|
||||||
|
CREATE INDEX idx_tags_name ON tags (name);
|
||||||
|
CREATE INDEX idx_categories_name ON categories (name);
|
||||||
|
CREATE INDEX idx_posts_identifier ON posts (identifier);
|
||||||
|
CREATE INDEX idx_posts_status ON posts (status);
|
||||||
|
CREATE INDEX idx_posts_category ON posts (category);
|
||||||
|
CREATE INDEX idx_post_tag_post ON post_tag (post);
|
||||||
|
CREATE INDEX idx_post_tag_tag ON post_tag (tag);
|
||||||
|
CREATE INDEX idx_metadata_pid ON metadata (pid);
|
||||||
@@ -3,12 +3,11 @@
|
|||||||
/// https://github.com/rust-lang/git2-rs/commit/f3b87baed1e33d6c2d94fe1fa6aa6503a071d837
|
/// https://github.com/rust-lang/git2-rs/commit/f3b87baed1e33d6c2d94fe1fa6aa6503a071d837
|
||||||
///
|
///
|
||||||
use git2::{
|
use git2::{
|
||||||
build, AnnotatedCommit, AutotagOption, Delta, FetchOptions, Reference, Remote, Repository,
|
AnnotatedCommit, AutotagOption, Delta, FetchOptions, Reference, Remote, Repository, build,
|
||||||
};
|
};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use tracing::{error, warn};
|
use tracing::{error, warn};
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq)]
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
pub struct FileDelta {
|
pub struct FileDelta {
|
||||||
pub old_path: Option<PathBuf>,
|
pub old_path: Option<PathBuf>,
|
||||||
|
|||||||
Reference in new issue
Block a user