7 Commits
99 changed files with 943 additions and 825 deletions

No files matched your search

+10 -28
View File
@@ -1,34 +1,16 @@
# `Nocturne: personal website & CMS` # `Nocturne: Personal Website`
> This is the last working version This is the code repo of my [personal website](https://lumither.com),
and [here](https://github.com/Lumither/blog-posts/) is the link to the
This is the code repo of my personal website, and [here](https://github.com/Lumither/blog-posts/) is the link of the content (blog posts etc.) repo.
content repo.
Powered by Next.js and Rust, built with passion and love Powered by Next.js and Rust, built with passion and love
*will add more here in the future > [!NOTE]
> This system is not production ready, and contains bugs, (security risk potentially).
> But have fun on it =)
## Nav: # Todos
<todo>
## Todos:
### Backend
- [ ] Add: log system
- [ ] Add: search api
- [ ] Add: tag filter (api)
- [ ] Abandon: UUID mechanism
- [ ] Fix: db connection issue
### Frontend
- [ ] Page: Home
- [ ] Page: About
- [ ] Page: Tags
- [ ] Page: Archive
- [ ] Page: Search
- [ ] Add: Right sidebar
- [ ] the todo list lol
- [ ] and much more...
+1 -2
View File
@@ -1,8 +1,7 @@
[workspace] [workspace]
resolver = "2" resolver = "2"
members = [ members = [
"api_server", "server",
"constants",
"macros", "macros",
"markdown", "markdown",
] ]
-25
View File
@@ -1,25 +0,0 @@
[package]
name = "api_server"
version = "0.1.0"
edition = "2021"
[dependencies]
macros = { path = "../macros" }
constants = { path = "../constants" }
markdown = { path = "../markdown" }
chrono = { version = "0.4.38", features = ["serde", "alloc"] }
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "fmt"] }
tracing-appender = "0.2.3"
dotenv = "0.15.0"
dirs = "5.0.1"
tokio = { version = "1.40.0", features = ["full"] }
sqlx = { version = "0.8.2", features = ["runtime-tokio", "tls-native-tls", "postgres", "chrono", "uuid"] }
axum = { version = "0.7.5", features = ["macros"] }
serde = { version = "1.0.199", features = ["derive"] }
serde_json = "1.0.128"
uuid = { version = "1.10.0", features = ["v4", "fast-rng", "serde"] }
futures = { version = "0.3.30", features = ["executor"] }
cron = "0.13.0"
git2 = "0.19.0"
thiserror = "2.0.3"
@@ -1,26 +0,0 @@
use 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) => {
error!("Failed to get blog page count: {}", e.to_string());
Err(Json::from(json!({"error": e.to_string()})))
}
}
}
-153
View File
@@ -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)))
}
-14
View File
@@ -1,14 +0,0 @@
use crate::scheduler::tasks::basic::BasicTask;
use crate::scheduler::tasks::Task;
use sqlx::{Pool, Postgres};
pub mod check_update;
pub fn get_tasks(db_connection: Pool<Postgres>) -> Vec<Box<dyn Task>> {
let db_conn_clone = db_connection.clone();
vec![
BasicTask::new(move || check_update::task(&db_conn_clone), "0/15 * * * * *")
.unwrap()
.to_task(),
]
}
-17
View File
@@ -1,17 +0,0 @@
mod api;
pub mod cron;
use crate::blog::api::{
get_page_count::get_page_count, get_post::get_post, get_post_list::get_post_list,
};
use axum::routing::get;
use axum::Router;
use sqlx::{Pool, Postgres};
pub fn get_router() -> Router<Pool<Postgres>> {
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))
}
-12
View File
@@ -1,12 +0,0 @@
use crate::blog;
use crate::scheduler::Scheduler;
use sqlx::{Pool, Postgres};
pub fn start(db_pool: Pool<Postgres>) {
let scheduler = Scheduler::new();
let _ = vec![blog::cron::get_tasks(db_pool.clone())]
.into_iter()
.map(|list| scheduler.insert_list(list))
.collect::<Vec<_>>();
scheduler.start().unwrap()
}
-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");
}
-52
View File
@@ -1,52 +0,0 @@
use std::env;
use std::error::Error;
use std::num::ParseIntError;
use std::process::exit;
use crate::blog;
use axum::Router;
use constants::config::server::{default_value, var_name};
use macros::panic_with_log;
use sqlx::{Pool, Postgres};
use tracing::{error, info, warn, Level};
pub async fn start(db_pool: Pool<Postgres>) -> Result<(), Box<dyn Error>> {
let app = Router::new()
.nest("/blog", blog::get_router())
.with_state(db_pool);
let port: u32 = match env::var(var_name::PORT) {
Ok(value) => value.parse().unwrap_or_else(|e: ParseIntError| {
warn!(
"failed to parse `{}`, using default port: {}",
var_name::PORT,
e.to_string()
);
default_value::PORT
}),
Err(_) => default_value::PORT,
};
let listener =
match tokio::net::TcpListener::bind(format!("{}:{}", default_value::HOST, port)).await {
Ok(listener) => {
info!("server started on {}:{}", default_value::HOST, port);
listener
}
Err(e) => {
panic_with_log!(
Level::ERROR,
"failed to start server on {}:{}: {}",
default_value::HOST,
port,
e.to_string()
);
}
};
axum::serve(listener, app).await.unwrap_or_else(|e| {
error!("failed to start axum: {}", e.to_string());
exit(1);
});
Ok(())
}
-13
View File
@@ -1,13 +0,0 @@
mod blog;
mod init;
mod scheduler;
mod utils;
#[tokio::main]
async fn main() {
init::load_env();
let _guards = init::logger::init();
init::start().await;
}
+24
View File
@@ -0,0 +1,24 @@
/// Includes a UTF-8 encoded file as a string
///
/// The file is located relative to `CARGO_MANIFEST_DIR`.
///
/// # Example
/// ```rust
/// use macros::include_str_from_crate_root;
///
/// let str: &str = include_str_from_crate_root!("/query/scheme.sql");
///
// todo: impl
// const PATH_PREFIX: &str = "/src";
// let str: &str = include_str_from_crate_root!("{}/query/scheme.sql", PATH_PREFIX);
/// ```
#[macro_export]
macro_rules! include_str_from_crate_root {
// ($path:literal $(, $($arg:tt)*)?) => {
// ($path:expr, $($args:tt)*) => {
// include_str!(concat!(env!("CARGO_MANIFEST_DIR"), format_args!($path, $($args)*)))
// };
($path:expr) => {
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), $path))
};
}
+1
View File
@@ -1 +1,2 @@
mod include_str;
mod log; mod log;
+20 -1
View File
@@ -6,7 +6,7 @@
/// use macros::panic_with_log; /// use macros::panic_with_log;
/// use tracing::Level; /// use tracing::Level;
/// ///
/// panic_with_log!(Level::ERROR, "Test panicked: {}", 42); /// panic_with_log!(Level::ERROR, "test panicked: {}", 42);
/// ``` /// ```
#[macro_export] #[macro_export]
macro_rules! panic_with_log { macro_rules! panic_with_log {
@@ -18,6 +18,25 @@ macro_rules! panic_with_log {
}}; }};
} }
/// log the message with `Level::ERROR` and exit(1)
///
/// # Example
/// ```rust
///
/// use macros::error_panic;
///
/// error_panic!("test panicked: {}", 42);
/// ```
#[macro_export]
macro_rules! error_panic {
($msg:literal $(, $($arg:tt)*)?) => {{
use std::process;
use tracing::{event, Level};
event!(Level::ERROR, $msg $(, $($arg)*)?);
process::exit(1);
}};
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
#[test] #[test]
+29
View File
@@ -0,0 +1,29 @@
[package]
name = "server"
version = "0.1.0"
edition = "2021"
[dependencies]
macros = { path = "../macros" }
markdown = { path = "../markdown" }
axum = { version = "0.8.1", features = ["macros"] }
tracing = { version = "0.1.41", features = ["log"] }
git2 = "0.20.0"
chrono = { version = "0.4.39", features = ["serde"] }
cron = "0.15.0"
sqlx = { version = "0.8.3", features = ["runtime-tokio", "tls-native-tls", "postgres", "chrono"] }
serde_json = "1.0.133"
serde = { version = "1.0.215", features = ["derive"] }
tracing-subscriber = "0.3.18"
tracing-appender = "0.2.3"
dirs = "6.0.0"
thiserror = "2.0.11"
tokio = { version = "1.42.0", features = ["rt", "rt-multi-thread", "macros"] }
dotenv = "0.15.0"
uuid = { version = "1.13.2", features = ["v4"] }
futures = "0.3.31"
[build-dependencies]
git2 = "0.20.0"
chrono = "0.4.39"
+16
View File
@@ -0,0 +1,16 @@
use chrono::Utc;
use git2::Repository;
fn main() {
let repo = Repository::open("../..").expect("failed to open repository");
let commit = repo
.head()
.expect("failed to get HEAD")
.peel_to_commit()
.expect("failed to get HEAD commit");
let hash = commit.id().to_string();
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);
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -2,7 +2,8 @@ use std::env;
use std::error::Error; use std::error::Error;
use std::str::FromStr; use std::str::FromStr;
use constants::config::database::{default_value, var_name}; use crate::constants::config::database::{default_value, var_name};
use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; use sqlx::postgres::{PgConnectOptions, PgPoolOptions};
use sqlx::{ConnectOptions, Pool, Postgres}; use sqlx::{ConnectOptions, Pool, Postgres};
use tracing::info; use tracing::info;
@@ -12,7 +13,7 @@ pub async fn init() -> Result<Pool<Postgres>, Box<dyn Error>> {
.unwrap() .unwrap()
.disable_statement_logging(); .disable_statement_logging();
Ok(PgPoolOptions::new() Ok(PgPoolOptions::new()
.max_connections(1) // todo: temporal solution, to be fixed .max_connections(1) // todo: temporary solution, to be fixed
.connect_with(db_connect_option) .connect_with(db_connect_option)
.await?) .await?)
} }
@@ -1,8 +1,9 @@
use std::path::Path; use std::path::Path;
use std::{env, io}; use std::{env, io};
use crate::constants::config::general::{default_value, var_name};
use chrono::Local; use chrono::Local;
use constants::config::general::{default_value, var_name};
use dirs::home_dir; use dirs::home_dir;
use tracing::info; use tracing::info;
use tracing_appender::{non_blocking, non_blocking::WorkerGuard, rolling}; use tracing_appender::{non_blocking, non_blocking::WorkerGuard, rolling};
@@ -18,7 +19,7 @@ impl FormatTime for LocalTimer {
write!( write!(
w, w,
"{}", "{}",
Local::now().format(constants::time::DEFAULT_TIME_FORMAT) Local::now().format(crate::constants::time::DEFAULT_TIME_FORMAT)
) )
} }
} }
File renamed without changes.
@@ -1,13 +1,13 @@
mod cron;
mod database; mod database;
pub mod logger; pub mod logger;
mod misc; mod misc;
mod schema;
pub mod server;
use macros::panic_with_log;
use std::{env, panic}; use std::{env, panic};
use tracing::{info, Level};
use crate::modules::{blog::Blog, ModuleTree};
use macros::error_panic;
use tracing::info;
pub fn load_env() { pub fn load_env() {
if let Some(env_file) = env::args().nth(1) { if let Some(env_file) = env::args().nth(1) {
@@ -34,25 +34,17 @@ pub async fn start() {
pool pool
} }
Err(e) => { Err(e) => {
panic_with_log!( error_panic!("database connection pool failed to initialize: {}", e);
Level::ERROR,
"database connection pool failed to initialize: {}",
e
);
} }
}; };
// schema init // // schema init
schema::init(&db_pool).await; // schema::init(&db_pool).await;
// load cron task ModuleTree::new()
cron::start(db_pool.clone()); .add_module(Blog {
db_handler: db_pool,
// api serve })
match server::start(db_pool).await { .blocking_serve()
Ok(_) => {} .await
Err(e) => {
panic_with_log!(Level::ERROR, "server panicked: {}", e)
}
}
} }
+23
View File
@@ -0,0 +1,23 @@
#[allow(dead_code)]
mod constants;
#[allow(dead_code)]
mod scheduler;
mod init;
mod modules;
mod utils;
use crate::init::{logger, start};
const BUILD_ID: &str = env!("BUILD_ID");
#[tokio::main]
async fn main() {
println!("{}", BUILD_ID);
init::load_env();
let _guards = logger::init();
start().await;
}
@@ -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;
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,6 +1,6 @@
use std::error::Error; use std::error::Error;
use constants::blog::PAGE_SIZE; use crate::constants::blog::PAGE_SIZE;
use axum::{extract::Query, extract::State, http::StatusCode, Json}; use axum::{extract::Query, extract::State, http::StatusCode, Json};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
@@ -1,3 +1,3 @@
pub mod get_page_count; pub mod get_page_count;
pub mod get_post; pub mod get_post;
pub mod get_post_list; // pub mod get_post_list;
@@ -1,8 +1,10 @@
use std::error::Error; use std::error::Error;
use std::path::Path; use std::path::Path;
use crate::blog::cron::check_update::utils::get_md_file_basename; use crate::{
use crate::blog::cron::check_update::{error::PostIdxError, utils::parse_date}; modules::blog::cron::check_update::utils::get_md_file_basename,
modules::blog::cron::check_update::{error::PostIdxError, utils::parse_date},
};
use markdown::MdFile; use markdown::MdFile;
use sqlx::{query, PgPool, Pool, Postgres, Row}; use sqlx::{query, PgPool, Pool, Postgres, Row};
@@ -1,15 +1,17 @@
use std::env; use std::{
use std::fs::remove_dir_all; env,
use std::path::{Path, PathBuf}; fs::remove_dir_all,
path::{Path, PathBuf},
};
use crate::{ use crate::{
blog::cron::check_update::{ constants::config::general::{default_value, var_name},
modules::blog::cron::check_update::{
index::drop_index, index::drop_index,
utils::{expand_path, get_md_file_basename, index_files, search_md}, utils::{expand_path, get_md_file_basename, index_files, search_md},
}, },
utils::git, utils::git,
}; };
use constants::config::general::{default_value, var_name};
use macros::panic_with_log; use macros::panic_with_log;
use git2::{Delta, Repository}; use git2::{Delta, Repository};
@@ -1,16 +1,18 @@
use dirs::home_dir;
use std::path::PathBuf; use std::path::PathBuf;
use crate::blog::cron::check_update::index::index_file; use crate::{
use chrono::{DateTime, FixedOffset, ParseResult}; constants::{
use constants::config::general::{default_value, var_name}; config::general::{default_value, var_name},
use constants::time::DEFAULT_DATE_FORMAT; time::DEFAULT_DATE_FORMAT,
},
modules::blog::cron::check_update::index::index_file,
};
use futures::executor::block_on; use chrono::{DateTime, FixedOffset, ParseResult};
use futures::future::join_all; use dirs::home_dir;
use futures::{executor::block_on, future::join_all};
use sqlx::{Pool, Postgres}; use sqlx::{Pool, Postgres};
use tokio::runtime::Runtime; use tokio::{runtime::Runtime, spawn};
use tokio::spawn;
use tracing::{error, warn}; use tracing::{error, warn};
pub fn search_md(entry: PathBuf) -> Vec<PathBuf> { pub fn search_md(entry: PathBuf) -> Vec<PathBuf> {
@@ -0,0 +1 @@
// pub mod check_update;
+52
View File
@@ -0,0 +1,52 @@
mod api;
mod cron;
use crate::{
modules::{
blog::{
api::{
get_page_count,
get_post,
// get_post_list::get_post_list,
},
// cron::check_update,
},
Module,
},
scheduler::tasks::CronTask,
};
use axum::{routing::get, Router};
use sqlx::PgPool;
const MOUNT_POINT: &str = "/blog";
pub struct Blog {
pub db_handler: PgPool,
}
impl Module for Blog {
fn get_server_router(&self) -> Router {
let db_handler = self.db_handler.clone();
// 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::handler))
.route("/get_post/{identifier}", get(get_post::handler))
.with_state(db_handler)
}
fn get_mount_point(&self) -> &'static str {
MOUNT_POINT
}
fn get_cron_tasks(&self) -> Vec<Box<dyn CronTask>> {
// let db_handler = self.db_handler.clone();
vec![
// 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)
);
+84
View File
@@ -0,0 +1,84 @@
pub mod blog;
use std::env;
use std::num::ParseIntError;
use crate::{
constants::config::server::{default_value, var_name},
scheduler::{tasks::CronTask, Scheduler},
};
use macros::error_panic;
use axum::Router;
use tracing::{info, warn};
// todo: merge `get_server_router` & `get_mount_point`, make Option
pub trait Module {
fn get_server_router(&self) -> Router;
fn get_mount_point(&self) -> &'static str;
fn get_cron_tasks(&self) -> Vec<Box<dyn CronTask>>;
}
pub struct ModuleTree {
pub modules: Vec<Box<dyn Module>>,
}
impl ModuleTree {
pub fn new() -> Self {
ModuleTree { modules: vec![] }
}
pub fn add_module(&mut self, module: impl Module + 'static) -> &mut Self {
self.modules.push(Box::new(module));
self
}
pub async fn blocking_serve(&self) {
let scheduler = Scheduler::new();
let mut app = Router::new();
let port: u32 = match env::var(var_name::PORT) {
Ok(value) => value.parse().unwrap_or_else(|e: ParseIntError| {
warn!(
"failed to parse `{}`, using default port: {}",
var_name::PORT,
e.to_string()
);
default_value::PORT
}),
Err(_) => default_value::PORT,
};
let listener = match tokio::net::TcpListener::bind(format!(
"{}:{}",
default_value::HOST,
port
))
.await
{
Ok(listener) => {
info!("server started on {}:{}", default_value::HOST, port);
listener
}
Err(e) => {
error_panic!(
"failed to start server on {}:{}: {}",
default_value::HOST,
port,
e.to_string()
);
}
};
for module in &self.modules {
let _ = scheduler.insert_list(module.get_cron_tasks());
app = app.nest(module.get_mount_point(), module.get_server_router());
}
axum::serve(listener, app).await.unwrap_or_else(|e| {
error_panic!("failed to start axum: {}", e.to_string());
});
}
}
@@ -1,12 +1,20 @@
use crate::{ use std::{
scheduler::scheduler_error::SchedulerError, collections::HashMap,
scheduler::scheduler_error::SchedulerError::IdAlreadyRunning, sync::{
scheduler::scheduler_error::SchedulerError::IdNotFound, scheduler::sleeper::SkippableSleeper, atomic::{AtomicBool, Ordering},
scheduler::tasks::Task, Arc, RwLock,
},
thread,
}; };
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::RwLock; use crate::scheduler::{
use std::{collections::HashMap, sync::Arc, thread}; scheduler_error::{
SchedulerError, SchedulerError::IdAlreadyRunning, SchedulerError::IdNotFound,
},
sleeper::SkippableSleeper,
tasks::CronTask,
};
use tracing::log::error; use tracing::log::error;
use uuid::Uuid; use uuid::Uuid;
@@ -22,7 +30,7 @@ struct TaskStatus {
#[derive(Default)] #[derive(Default)]
pub struct Scheduler { pub struct Scheduler {
tasks: Arc<RwLock<HashMap<Uuid, Arc<Box<dyn Task>>>>>, tasks: Arc<RwLock<HashMap<Uuid, Arc<Box<dyn CronTask>>>>>,
running: Arc<RwLock<HashMap<Uuid, Arc<TaskStatus>>>>, running: Arc<RwLock<HashMap<Uuid, Arc<TaskStatus>>>>,
} }
@@ -41,7 +49,7 @@ impl Scheduler {
fn run_task( fn run_task(
running: Arc<RwLock<HashMap<Uuid, Arc<TaskStatus>>>>, running: Arc<RwLock<HashMap<Uuid, Arc<TaskStatus>>>>,
id: Uuid, id: Uuid,
task: Arc<Box<dyn Task>>, task: Arc<Box<dyn CronTask>>,
) -> Result<(), SchedulerError> { ) -> Result<(), SchedulerError> {
if running.read()?.get(&id).is_some() { if running.read()?.get(&id).is_some() {
return Err(IdAlreadyRunning); return Err(IdAlreadyRunning);
@@ -151,13 +159,13 @@ impl Scheduler {
} }
} }
pub fn insert(&self, task: Box<dyn Task>) -> Result<Uuid, SchedulerError> { pub fn insert(&self, task: Box<dyn CronTask>) -> Result<Uuid, SchedulerError> {
let id = Uuid::new_v4(); let id = Uuid::new_v4();
self.tasks.write()?.insert(id, Arc::new(task)); self.tasks.write()?.insert(id, Arc::new(task));
Ok(id) Ok(id)
} }
pub fn insert_list(&self, tasks: Vec<Box<dyn Task>>) -> Result<Vec<Uuid>, SchedulerError> { pub fn insert_list(&self, tasks: Vec<Box<dyn CronTask>>) -> Result<Vec<Uuid>, SchedulerError> {
tasks.into_iter().map(|t| self.insert(t)).collect() tasks.into_iter().map(|t| self.insert(t)).collect()
} }
@@ -1,8 +1,12 @@
use crate::scheduler::scheduler_error::SchedulerError::MutexPoison; use std::{
use std::error::Error; error::Error,
use std::fmt::{Debug, Display, Formatter}; fmt::{Debug, Display, Formatter},
use std::sync::PoisonError; sync::PoisonError,
};
use crate::scheduler::scheduler_error::SchedulerError::MutexPoison;
// todo: refactor with thiserror
pub enum SchedulerError { pub enum SchedulerError {
IdNotFound, IdNotFound,
IdAlreadyRunning, IdAlreadyRunning,
@@ -1,7 +1,10 @@
use std::{
sync::{Arc, Condvar, Mutex},
thread,
time::Duration,
};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use std::sync::{Arc, Condvar, Mutex};
use std::thread;
use std::time::Duration;
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub struct SkippableSleeper { pub struct SkippableSleeper {
@@ -1,10 +1,11 @@
use crate::scheduler::task_func::TaskFunc;
use crate::scheduler::tasks::Task;
use chrono::{DateTime, Utc};
use cron::Schedule;
use std::error::Error; use std::error::Error;
use std::str::FromStr; use std::str::FromStr;
use crate::scheduler::{task_func::TaskFunc, tasks::CronTask};
use chrono::{DateTime, Utc};
use cron::Schedule;
pub struct BasicTask { pub struct BasicTask {
schedule: Schedule, schedule: Schedule,
func: Box<dyn TaskFunc>, func: Box<dyn TaskFunc>,
@@ -21,12 +22,12 @@ impl BasicTask {
}) })
} }
pub fn to_task(self) -> Box<dyn Task> { pub fn to_task(self) -> Box<dyn CronTask> {
Box::new(self) Box::new(self)
} }
} }
impl Task for BasicTask { impl CronTask for BasicTask {
fn get_next_execution(&self) -> Option<DateTime<Utc>> { fn get_next_execution(&self) -> Option<DateTime<Utc>> {
self.schedule.upcoming(Utc).next() self.schedule.upcoming(Utc).next()
} }
@@ -2,7 +2,7 @@ use chrono::{DateTime, Utc};
pub mod basic; pub mod basic;
pub trait Task: Send + Sync { pub trait CronTask: Send + Sync {
fn get_next_execution(&self) -> Option<DateTime<Utc>>; fn get_next_execution(&self) -> Option<DateTime<Utc>>;
fn call(&self); fn call(&self);
} }
@@ -16,6 +16,7 @@ pub struct FileDelta {
pub status: Delta, pub status: Delta,
} }
#[allow(dead_code)]
fn do_fetch<'a>( fn do_fetch<'a>(
repo: &'a Repository, repo: &'a Repository,
refs: &[&str], refs: &[&str],
@@ -23,8 +24,6 @@ fn do_fetch<'a>(
) -> Result<AnnotatedCommit<'a>, git2::Error> { ) -> Result<AnnotatedCommit<'a>, git2::Error> {
let mut fetch_option = FetchOptions::new(); let mut fetch_option = FetchOptions::new();
// Always fetch all tags.
// Perform a download and also update tips
fetch_option.download_tags(AutotagOption::All); fetch_option.download_tags(AutotagOption::All);
remote.fetch(refs, Some(&mut fetch_option), None)?; remote.fetch(refs, Some(&mut fetch_option), None)?;
@@ -32,6 +31,7 @@ fn do_fetch<'a>(
repo.reference_to_annotated_commit(&fetch_head) repo.reference_to_annotated_commit(&fetch_head)
} }
#[allow(dead_code)]
fn fast_forward( fn fast_forward(
repo: &Repository, repo: &Repository,
lb: &mut Reference, lb: &mut Reference,
@@ -45,16 +45,11 @@ fn fast_forward(
// println!("{}", msg); // 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( repo.checkout_head(Some(build::CheckoutBuilder::default().force()))?;
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(),
))?;
Ok(()) Ok(())
} }
#[allow(dead_code)]
fn normal_merge( fn normal_merge(
repo: &Repository, repo: &Repository,
local: &AnnotatedCommit, local: &AnnotatedCommit,
@@ -73,13 +68,12 @@ fn normal_merge(
return Ok(()); return Ok(());
} }
let result_tree = repo.find_tree(idx.write_tree_to(repo)?)?; 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 msg = format!("merge({}) -> {}", remote.id(), local.id());
let sig = repo.signature()?; let sig = repo.signature()?;
let local_commit = repo.find_commit(local.id())?; let local_commit = repo.find_commit(local.id())?;
let remote_commit = repo.find_commit(remote.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( let _merge_commit = repo.commit(
Some("HEAD"), Some("HEAD"),
&sig, &sig,
@@ -88,34 +82,26 @@ fn normal_merge(
&result_tree, &result_tree,
&[&local_commit, &remote_commit], &[&local_commit, &remote_commit],
)?; )?;
// Set working tree to match head.
repo.checkout_head(None)?; repo.checkout_head(None)?;
Ok(()) Ok(())
} }
#[allow(dead_code)]
fn do_merge<'a>( 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<Vec<FileDelta>, git2::Error> {
// 1. do a merge analysis
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()?)?;
// 2. Do the appropriate merge
if analysis.0.is_fast_forward() { if analysis.0.is_fast_forward() {
// println!("Doing a fast-forward");
// do a fast-forward
let ref_name = format!("refs/heads/{}", remote_branch); let ref_name = format!("refs/heads/{}", remote_branch);
match repo.find_reference(&ref_name) { match repo.find_reference(&ref_name) {
Ok(mut r) => { Ok(mut r) => {
fast_forward(repo, &mut r, &fetch_commit)?; fast_forward(repo, &mut r, &fetch_commit)?;
} }
Err(_) => { 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( repo.reference(
&ref_name, &ref_name,
fetch_commit.id(), fetch_commit.id(),
@@ -132,17 +118,12 @@ fn do_merge<'a>(
} }
}; };
} else if analysis.0.is_normal() { } else if analysis.0.is_normal() {
// do a normal merge
normal_merge(repo, &head_commit, &fetch_commit)?; normal_merge(repo, &head_commit, &fetch_commit)?;
} }
// else {
// // already up-to-date
// println!("Nothing to do...");
// }
calculate_diff(repo, &head_commit, &fetch_commit) calculate_diff(repo, &head_commit, &fetch_commit)
} }
#[allow(dead_code)]
fn calculate_diff( fn calculate_diff(
repo: &Repository, repo: &Repository,
local: &AnnotatedCommit, local: &AnnotatedCommit,
@@ -156,23 +137,12 @@ fn calculate_diff(
if let Err(e) = diff.foreach( if let Err(e) = diff.foreach(
&mut |d, _| { &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 { updated_files.push(FileDelta {
old_path: d.old_file().path().map(PathBuf::from), old_path: d.old_file().path().map(PathBuf::from),
new_path: d.new_file().path().map(PathBuf::from), new_path: d.new_file().path().map(PathBuf::from),
status: d.status(), status: d.status(),
}); });
true // Continue true
}, },
None, None,
None, None,
@@ -184,6 +154,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,
File renamed without changes.
@@ -1,4 +1,10 @@
import React from 'react'; import React from 'react';
import { Metadata } from 'next';
import { SITE_CONFIG } from '@/src/constants';
export const metadata: Metadata = {
title: `About - ${ SITE_CONFIG.name }`
};
function Layout({ function Layout({
children children
@@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import MarkdownRenderer from '@/app/public/MarkdownRenderer'; import MarkdownRenderer from '@/app/(pages)/public/MarkdownRenderer';
import { fetchText } from '@/app/fetch'; import { fetchText } from '@/app/fetch';
const Me = async () => { const Me = async () => {
File renamed without changes.
@@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import MarkdownRenderer from '@/app/public/MarkdownRenderer'; import MarkdownRenderer from '@/app/(pages)/public/MarkdownRenderer';
import { fetchText } from '@/app/fetch'; import { fetchText } from '@/app/fetch';
const Website = async () => { const Website = async () => {
File renamed without changes.
@@ -3,7 +3,7 @@
import React from 'react'; import React from 'react';
import { Pagination as NextUIPagination } from '@nextui-org/react'; import { Pagination as NextUIPagination } from '@nextui-org/react';
import { useRouter, useSearchParams } from 'next/navigation'; import { useRouter, useSearchParams } from 'next/navigation';
import { useScreenSizeTrigger } from '@/app/public/screenSizeTrigger'; import { useScreenSizeTrigger } from '@/app/(pages)/public/screenSizeTrigger';
const Pagination = ({ total }: { const Pagination = ({ total }: {
total: number, total: number,
@@ -1,11 +1,12 @@
'use server'; 'use server';
import React from 'react'; import React from 'react';
import { fetchNocturneApi } from '@/app/blog/api'; import { fetchNocturneApi } from '@/app/(pages)/blog/api';
import { MotionDiv } from '@/app/public/MotionDiv'; import BlogPostCard from '@/app/(pages)/blog/BlogPostCard';
import BlogPostCard from '@/app/blog/BlogPostCard';
import { Card, CardBody } from '@nextui-org/card'; import { Card, CardBody } from '@nextui-org/card';
import * as motion from 'motion/react-client';
const PostList = async ({ page }: { page: number }) => { const PostList = async ({ page }: { page: number }) => {
try { try {
const res = await fetchNocturneApi(`/blog/get_post_list?page=${ page }`); const res = await fetchNocturneApi(`/blog/get_post_list?page=${ page }`);
@@ -16,13 +17,13 @@ const PostList = async ({ page }: { page: number }) => {
<ul className={ 'flex flex-col items-center w-full space-y-7' }> <ul className={ 'flex flex-col items-center w-full space-y-7' }>
{ posts.map((post: JSON, key: number) => ( { posts.map((post: JSON, key: number) => (
<li key={ key } className={ 'w-full' }> <li key={ key } className={ 'w-full' }>
<MotionDiv <motion.div
initial={ { y: 20, opacity: 0 } } initial={ { y: 20, opacity: 0 } }
animate={ { y: 0, opacity: 1 } } animate={ { y: 0, opacity: 1 } }
transition={ { ease: 'easeInOut', duration: 0.5, delay: key * .2 + .25 } } transition={ { ease: 'easeInOut', duration: 0.5, delay: key * .2 + .25 } }
> >
<BlogPostCard post={ post } /> <BlogPostCard post={ post } />
</MotionDiv> </motion.div>
</li>) </li>)
) } ) }
</ul> </ul>
File renamed without changes.
@@ -1,4 +1,10 @@
import React from 'react'; import React from 'react';
import { Metadata } from 'next';
import { SITE_CONFIG } from '@/src/constants';
export const metadata: Metadata = {
title: `Blog - ${ SITE_CONFIG.name }`
};
function Layout({ function Layout({
children children
@@ -1,6 +1,6 @@
import PostList from '@/app/blog/PostList'; import PostList from '@/app/(pages)/blog/PostList';
import Pagination from '@/app/blog/Pagination'; import Pagination from '@/app/(pages)/blog/Pagination';
import { fetchNocturneApi } from '@/app/blog/api'; import { fetchNocturneApi } from '@/app/(pages)/blog/api';
async function Blog({ async function Blog({
searchParams searchParams
@@ -1,24 +1,32 @@
import React from 'react'; import React from 'react';
import MinifyPostCard from '@/app/blog/post/[id]/MinifyPostCard'; import MinifyPostCard from '@/app/(pages)/blog/post/[id]/MinifyPostCard';
import Link from 'next/link'; import Link from 'next/link';
import { FaCreativeCommons } from 'react-icons/fa6'; import { FaCreativeCommons } from 'react-icons/fa6';
type Props = { type Props = {
post: any // post: any
post_id: string;
adjacent_posts: any
perm_link: string | undefined;
author: string | undefined;
author_link: string | undefined;
first_update: string;
last_update: string | undefined;
cc: string | undefined;
title: string;
} }
const Footer = (props: Props) => { const Footer = (props: Props) => {
const { post } = props; const { adjacent_posts, perm_link, post_id, first_update, last_update, title } = props;
const adjacent_posts = post['adj']; const prev_post = adjacent_posts.prev;
const prev_post = adjacent_posts['prev']; const next_post = adjacent_posts.next;
const next_post = adjacent_posts['next']; const link = perm_link ?? `https://lumither.com/blog/post/${ post_id }`;
const link = post['meta']['link'] ?? `https://lumither.com/blog/post/${ post['meta']['id'] }`; const author = props.author ?? 'Tao';
const author = post['meta']['author'] ?? 'Tao'; const author_link = props.author_link ?? '/about/me';
const author_link = post['meta']['author_link'] ?? '/about'; const post_date = new Date(first_update);
const post_date = new Date(post['first_update']); const update_date: Date | undefined = last_update ? new Date(last_update) : undefined;
const update_date: Date | undefined = post['last_update'] ? new Date(post['last_update']) : undefined; const cc = props.cc ?? 'by-nc-sa';
const cc = post['meta']['cc'] ?? 'by-nc-sa';
return ( return (
@@ -35,7 +43,7 @@ const Footer = (props: Props) => {
<div className={ 'flex flex-col space-y-4 relative' }> <div className={ 'flex flex-col space-y-4 relative' }>
<div className={ 'z-[2]' }> <div className={ 'z-[2]' }>
<p className={ 'text-xl font-bold' }>{ post['meta']['title'] }</p> <p className={ 'text-xl font-bold' }>{ title }</p>
<Link href={ link }> <Link href={ link }>
<p className={ 'text-sm underline decoration-indigo-500 dark:decoration-sky-500' }>{ link }</p> <p className={ 'text-sm underline decoration-indigo-500 dark:decoration-sky-500' }>{ link }</p>
</Link> </Link>
@@ -3,57 +3,57 @@ import Image from 'next/image';
import { Chip } from '@nextui-org/react'; import { Chip } from '@nextui-org/react';
import { MdCalendarMonth } from 'react-icons/md'; import { MdCalendarMonth } from 'react-icons/md';
// type Props = {
// post: any
// };
type Props = { type Props = {
post: any title: string;
}; subtitle: string;
summary: string | undefined;
category: string;
tags: string[];
first_update: string;
header_img: string | undefined;
}
const Headers = (props: Props) => { const Headers = (post: Props) => {
let post = props.post;
let title = post['title'];
let sub_title = post['sub_title'];
// let summary = (post as any)['summary'];
let category = post['category'];
let tags = post['tags'];
// let last_update = new Date((post as any)['last_update']);
let first_update = new Date(post['first_update']);
// let post_id = (post as any)['post_id'];
let header_img = post['meta']['header_img'] as string | null;
return ( return (
<> <>
{ header_img && <div className={ `w-full overflow h-96 relative` }> { post.header_img && <div className={ `w-full overflow h-96 relative` }>
<Image <Image
alt={ 'cover picture' } alt={ 'cover picture' }
src={ header_img } src={ post.header_img }
className={ `w-full object-cover rounded-b-none z-[5]` } className={ `w-full object-cover rounded-b-none z-[5]` }
fill fill
></Image> ></Image>
<Image <Image
alt={ 'cover picture' } alt={ 'cover picture' }
src={ header_img } src={ post.header_img }
className={ `w-full relative scale-110 blur-3xl object-cover rounded-b-none` } className={ `w-full relative scale-110 blur-3xl object-cover rounded-b-none` }
fill fill
></Image> ></Image>
</div> } </div> }
<div <div
className={ header_img ? className={ post.header_img ?
'mx-8 -mt-16 backdrop-blur transition bg-zinc-300/80 dark:bg-gray-600/25 rounded-2xl z-30' 'mx-8 -mt-16 backdrop-blur transition bg-zinc-300/80 dark:bg-gray-600/25 rounded-2xl z-30'
: 'mx-6 mt-6' : 'mx-6 mt-6'
}> }>
<div className={ header_img ? `p-4` : 'p-4 pt-2' }> <div className={ post.header_img ? `p-4` : 'p-4 pt-2' }>
<Chip className={ `mb-2` } radius={ `sm` } color={ `secondary` } <Chip className={ `mb-2` } radius={ `sm` } color={ `secondary` }
variant={ `solid` }>{ category }</Chip> variant={ `solid` }>{ post.category }</Chip>
<p className={ `text-4xl font-bold` }> { title }</p> <p className={ `text-4xl font-bold` }> { post.title }</p>
<p className={ `text-xl` }> { sub_title }</p> <p className={ `text-xl` }> { post.subtitle }</p>
</div> </div>
<div className={ `mx-2 -mt-2 mb-2 flex flex-row flex-wrap justify-between w-full` }> <div className={ `mx-2 -mt-2 mb-2 flex flex-row flex-wrap justify-between w-full` }>
<div> <div>
<ul className={ `flex flex-row flex-wrap` }> <ul className={ `flex flex-row flex-wrap` }>
{ {
tags.map((tag: string, key: any) => ( post.tags.map((tag: string, key: any) => (
<li key={ key } className={ `mx-1 mb-1` }> <li key={ key } className={ `mx-1 mb-1` }>
<Chip>{ tag }</Chip> <Chip>{ tag }</Chip>
</li> </li>
@@ -64,7 +64,7 @@ const Headers = (props: Props) => {
<div className={ `flex flex-row place-content-end mb-1 mr-6 items-center mx-1` }> <div className={ `flex flex-row place-content-end mb-1 mr-6 items-center mx-1` }>
<MdCalendarMonth /> <MdCalendarMonth />
<p className={ `text-zinc-500 dark:text-zinc-400 ml-1` }> <p className={ `text-zinc-500 dark:text-zinc-400 ml-1` }>
{ first_update.toLocaleString('default', { { (new Date(post.first_update)).toLocaleString('default', {
month: `long`, month: `long`,
day: `numeric`, day: `numeric`,
year: `numeric` year: `numeric`
@@ -43,10 +43,10 @@ const MinifyPostCard = (props: Props) => {
} }
{ !img_url && { !img_url &&
<div className={ 'h-full grid p-6 place-content-center' }> <div className={ 'h-full grid p-6 place-content-center' }>
<div className={ 'flex flex-row items-center text-center max-h-fit' }> <div className={ 'flex flex-col max-h-fit text-left' }>
<p className={ 'text-3xl font-bold' }>{ post['title'] }</p> <p className={ 'text-3xl font-bold text-balance' }>{ post['title'] }</p>
<div className={ 'h-full w-1 mx-2 bg-zinc-500 dark:bg-zinc-400' }></div> <div className={ 'w-1/4 h-[2px] my-2 bg-zinc-500 dark:bg-zinc-400' }></div>
<p className={ 'text-xl' }>{ post['sub_title'] }</p> <p className={ 'text-xl text-pretty break-words' }>{ post['sub_title'] }</p>
</div> </div>
</div> </div>
} }
@@ -0,0 +1,55 @@
import React from 'react';
import { Card, CardBody } from '@nextui-org/card';
import Headers from '@/app/(pages)/blog/post/[id]/Headers';
import Footer from '@/app/(pages)/blog/post/[id]/Footer';
import MarkdownRenderer from '@/app/(pages)/public/MarkdownRenderer';
type Props = {
post: any
}
async function PostCard(props: Props) {
let post = props.post;
return (
<div>
<Card className={ `max-w-full relative` }>
{/* card headers */ }
<Headers title={ post.title }
subtitle={ post.sub_title }
summary={ post.summary }
category={ post.category }
tags={ post.tags }
first_update={ post.first_update }
header_img={ post.meta.header_img }
/>
{/* card body */ }
<CardBody>
<div className={ 'p-5' }>
<MarkdownRenderer>
{ post.content }
</MarkdownRenderer>
</div>
</CardBody>
{/* card footer */ }
<Footer
adjacent_posts={ post.adj }
perm_link={ post.meta.link }
author={ post.meta.author }
author_link={ post.meta.author_link }
first_update={ post.first_update }
last_update={ post.last_update }
cc={ post.meta.cc }
post_id={ post.meta.id }
title={ post.title }
/>
</Card>
</div>
);
}
export default PostCard;
@@ -0,0 +1,70 @@
import React, { cache } from 'react';
import PostCard from '@/app/(pages)/blog/post/[id]/PostCard';
import ButtonGoBack from '@/app/(pages)/blog/post/[id]/ButtonGoBack';
import * as motion from 'motion/react-client';
import { fetchNocturneApi } from '@/app/(pages)/blog/api';
import { Metadata } from 'next';
import { SITE_CONFIG } from '@/src/constants';
const fetchPost = cache(async (id: string) => {
const res = await fetchNocturneApi(`/blog/get_post/${ id }`);
// throw Error('Not Found');
return await res.json();
});
export async function generateMetadata(
{ params }: { params: Promise<{ id: string }> })
: Promise<Metadata> {
try {
const title = (await fetchPost((await params).id)).title as string;
return {
title: `${ title } - ${ SITE_CONFIG.name }`
};
} catch (e) {
return {
title: 'Unexpected Error'
};
}
}
async function BlogReader({
params
}: {
params: Promise<{ id: string }>
}) {
try {
let post = await fetchPost((await params).id);
return (
<motion.div
className={ `max-w-full` }
initial={ { y: 20, opacity: 0 } }
animate={ { y: 0, opacity: 1 } }
transition={ { ease: 'easeInOut', duration: 0.5 } }
>
<div className={ `max-w-full` }>
<PostCard post={ post } />
</div>
<div className={ 'mt-3' }>
<ButtonGoBack />
</div>
</motion.div>
);
} catch (e: any) {
return (
<div className={ `flex h-full w-full justify-center items-center` }>
<div className={ `flex-row` }>
<p className={ `text-xl font-bold` }>
{ 'Unexpected Error:' }
</p>
<p className={ `font-bold` }>
{ e.message }
</p>
</div>
</div>
);
}
}
export default BlogReader;
@@ -1,4 +1,10 @@
import React from 'react'; import React from 'react';
import { Metadata } from 'next';
import { SITE_CONFIG } from '@/src/constants';
export const metadata: Metadata = {
title: `Friends - ${ SITE_CONFIG.name }`
};
function Layout({ function Layout({
children children
File renamed without changes.
+30
View File
@@ -0,0 +1,30 @@
import React from 'react';
import Navbar from '@/components/Navbar';
import Footer from '@/components/Footer';
import * as motion from 'motion/react-client';
function Layout({
children
}: {
children: React.ReactNode;
}) {
return (
<div>
<motion.div
initial={ { opacity: 0 } }
animate={ { opacity: 1 } }
transition={ { ease: 'easeInOut', duration: 0.3, delay: 0.5 } }
className={ `justify-center flex flex-row w-full max-w-full` }>
<Navbar />
<div className={ `flex flex-col flex-1 max-w-[1024px] min-w-0` }>
{ children }
<Footer />
</div>
</motion.div>
</div>
)
;
}
export default Layout;
Whitespace-only changes.
File renamed without changes.
+43
View File
@@ -0,0 +1,43 @@
import React, { useEffect, useState } from 'react';
import { Activity } from '@/src/HomePageActivities/activity';
import { getGhActivities } from '@/src/HomePageActivities/ghActivity';
import * as motion from 'motion/react-client';
const Activities = () => {
const [ activities, setActivities ] = useState<Activity[] | null>(null);
useEffect(() => {
getGhActivities().then(data => setActivities(data));
}, []);
if (!activities) {
return (
<div>
<p>loading</p>
</div>
);
} else {
return (
<div>
<ul className={ 'flex flex-col gap-y-3 items-end' }>
{ activities.map((activity, key) => (
<motion.div
key={ key }
initial={ { opacity: 0, y: 20 } }
animate={ { opacity: 1, y: 0 } }
transition={ { ease: 'easeInOut', duration: 0.2 } }
>
<li className={ 'backdrop-blur bg-white/75 dark:bg-black/45 rounded-xl w-fit' }>
{ activity.toElement() }
</li>
</motion.div>
)) }
</ul>
</div>
);
}
};
export default Activities;
-53
View File
@@ -1,53 +0,0 @@
import React from 'react';
import { Card, CardBody } from '@nextui-org/card';
import Headers from '@/app/blog/post/[id]/Headers';
import Footer from '@/app/blog/post/[id]/Footer';
import { fetchNocturneApi } from '@/app/blog/api';
import MarkdownRenderer from '@/app/public/MarkdownRenderer';
type Props = {
id: string
}
async function PostCard(props: Props) {
try {
const res = await fetchNocturneApi(`/blog/get_post/${ props.id }`);
const ret: any = await res.json();
return (
<div>
<Card className={ `max-w-full relative` }>
{/* card headers */ }
<Headers post={ ret } />
{/* card body */ }
<CardBody>
<div className={ 'p-5' }>
<MarkdownRenderer>
{ ret['content'] }
</MarkdownRenderer>
</div>
</CardBody>
{/* card footer */ }
<Footer post={ ret } />
</Card>
</div>
);
} catch (e: any) {
return <div>
<Card className={ `w-full` }>
<CardBody>
Fatal: { e.toString() }
</CardBody>
</Card>
</div>;
}
}
export default PostCard;
-29
View File
@@ -1,29 +0,0 @@
import React from 'react';
import PostCard from '@/app/blog/post/[id]/PostCard';
import { MotionDiv } from '@/app/public/MotionDiv';
import ButtonGoBack from '@/app/blog/post/[id]/ButtonGoBack';
async function BlogReader({
params
}: {
params: Promise<{ id: string }>
}) {
return (
<MotionDiv
className={ `max-w-full` }
initial={ { y: 20, opacity: 0 } }
animate={ { y: 0, opacity: 1 } }
transition={ { ease: 'easeInOut', duration: 0.5 } }
>
<div className={ `max-w-full` }>
<PostCard id={ (await params).id } />
</div>
<div className={ 'mt-3' }>
<ButtonGoBack />
</div>
</MotionDiv>
);
}
export default BlogReader;
+56
View File
@@ -0,0 +1,56 @@
import React from 'react';
import {
IoFlask,
IoHome,
IoInformationCircle,
IoLink,
IoLogoGithub,
IoMail,
IoPencil,
IoSearch
} from 'react-icons/io5';
export const entries: { display_name: string; href: string; icon: React.ReactNode }[] = [
{
icon: <IoHome size={ `20px` }></IoHome>,
display_name: 'Home',
href: '/'
},
{
icon: <IoInformationCircle size={ `20px` }></IoInformationCircle>,
display_name: 'About',
href: '/about'
},
{
icon: <IoPencil size={ `20px` }></IoPencil>,
display_name: 'Blog',
href: '/blog'
},
{
icon: <IoLink size={ `20px` }></IoLink>,
display_name: 'Friends',
href: '/friends'
},
{
icon: <IoSearch size={ `20px` }></IoSearch>,
display_name: 'Search',
href: '/search'
}, {
icon: <IoFlask size={ '20px' } />,
display_name: 'Lab',
href: 'https://lab.lmt.moe'
}
];
export const connections: { label: string, href: string, icon: React.ReactNode }[] = [
{
label: 'Github',
href: 'https://github.com/Lumither',
icon: <IoLogoGithub size={ `30px` } />
},
{
label: 'email',
href: 'mailto:lumither@outlook.com',
icon: <IoMail size={ `30px` } />
}
];
+6 -11
View File
@@ -2,13 +2,14 @@ import type { Metadata } from 'next';
import './globals.css'; import './globals.css';
import React from 'react'; import React from 'react';
import { ThemeProvider as NextThemesProvider } from 'next-themes'; import { ThemeProvider as NextThemesProvider } from 'next-themes';
import Navbar from '@/components/Navbar'; import { SITE_CONFIG } from '@/src/constants';
import Footer from '@/components/Footer';
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Lumitherの酒馆', openGraph: {
description: '一间位于时间夹缝中的酒馆,静候下一位旅者的到来...' siteName: SITE_CONFIG.name
},
title: SITE_CONFIG.name,
description: SITE_CONFIG.desc
}; };
export default function RootLayout({ export default function RootLayout({
@@ -20,13 +21,7 @@ export default function RootLayout({
<html lang="en" suppressHydrationWarning> <html lang="en" suppressHydrationWarning>
<body className={ `transition duration-300 dark:bg-[#282830] min-h-screen` }> <body className={ `transition duration-300 dark:bg-[#282830] min-h-screen` }>
<NextThemesProvider attribute="class" defaultTheme="system"> <NextThemesProvider attribute="class" defaultTheme="system">
<div className={ `justify-center flex flex-row w-full max-w-full` }>
<Navbar />
<div className={ `flex flex-col flex-1 max-w-[1024px] min-w-0` }>
{ children } { children }
<Footer />
</div>
</div>
</NextThemesProvider> </NextThemesProvider>
</body> </body>
</html> </html>
+44 -3
View File
@@ -1,10 +1,51 @@
import ConstructionPlaceHolder from '@/components/ConstructionPlaceHolder'; 'use client';
import React from 'react'; import React from 'react';
import { useScreenSizeTrigger } from '@/app/(pages)/public/screenSizeTrigger';
import { Button } from '@nextui-org/react';
import { entries } from '@/app/entries';
import Link from 'next/link';
import { useLocalStorage } from 'react-use';
export default function Home() { export default function Home() {
const isMobile = useScreenSizeTrigger('lg');
const [ test, setTest, removeTest ] = useLocalStorage('test', 'test_value');
console.log(test);
// setTest('test value 2');
// console.log(test);
// removeTest();
// console.log(test);
return ( return (
<div className={ `h-dvh` }> <div className={ `h-dvh relative` }>
<ConstructionPlaceHolder /> <ul className={ `flex flex-col space-y-2 w-full` }>
{
entries.map((meta, key) => (
<li key={ key }>
<Button
as={ Link }
variant={ `light` }
color={ `default` }
fullWidth
isIconOnly={ isMobile }
className={ 'flex lg:justify-start w-fit lg:w-full' }
aria-label={ `navbar: ${ meta.display_name }` }
href={ meta.href }
>
{ meta.icon }
<p className={ `font-bold hidden lg:block` }>{ meta.display_name }</p>
</Button>
</li>
))
}
</ul>
<div
className={ 'absolute bottom-0 w-full h-60 bg-yellow-500 p-12' }
>
<div className={ 'bg-blue-500 h-full w-full rounded-3xl backdrop-blur' }></div>
</div>
</div> </div>
); );
} }
-5
View File
@@ -1,5 +0,0 @@
'use client';
import { motion } from 'framer-motion';
export const MotionDiv = motion.div;
+2 -55
View File
@@ -3,65 +3,12 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Button } from '@nextui-org/react'; import { Button } from '@nextui-org/react';
import Image from 'next/image'; import Image from 'next/image';
import {
IoFlask,
IoHome,
IoInformationCircle,
IoLink,
IoLogoGithub,
IoMail,
IoPencil,
IoSearch
} from 'react-icons/io5';
import Link from 'next/link'; import Link from 'next/link';
import ThemeSwitcher from '@/components/ThemeSwitcher'; import ThemeSwitcher from '@/components/ThemeSwitcher';
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation';
import { useScreenSizeTrigger } from '@/app/public/screenSizeTrigger'; import { useScreenSizeTrigger } from '@/app/(pages)/public/screenSizeTrigger';
import { connections, entries } from '@/app/entries';
const entries: { display_name: string; href: string; icon: React.ReactNode }[] = [
{
icon: <IoHome size={ `20px` }></IoHome>,
display_name: 'Home',
href: '/'
},
{
icon: <IoInformationCircle size={ `20px` }></IoInformationCircle>,
display_name: 'About',
href: '/about'
},
{
icon: <IoPencil size={ `20px` }></IoPencil>,
display_name: 'Blog',
href: '/blog'
},
{
icon: <IoLink size={ `20px` }></IoLink>,
display_name: 'Friends',
href: '/friends'
},
{
icon: <IoSearch size={ `20px` }></IoSearch>,
display_name: 'Search',
href: '/search'
}, {
icon: <IoFlask size={ '20px' } />,
display_name: 'Lab',
href: 'https://lab.lmt.moe'
}
];
const connections: { label: string, href: string, icon: React.ReactNode }[] = [
{
label: 'Github',
href: 'https://github.com/Lumither',
icon: <IoLogoGithub size={ `30px` } />
},
{
label: 'email',
href: 'mailto:lumither@outlook.com',
icon: <IoMail size={ `30px` } />
}
];
function Navbar() { function Navbar() {
+1 -1
View File
@@ -4,7 +4,7 @@ import React, { useEffect, useState } from 'react';
import { useTheme } from 'next-themes'; import { useTheme } from 'next-themes';
import { Button, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger } from '@nextui-org/react'; import { Button, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger } from '@nextui-org/react';
import { MdOutlineDarkMode, MdOutlineDesktopWindows, MdOutlineLightMode } from 'react-icons/md'; import { MdOutlineDarkMode, MdOutlineDesktopWindows, MdOutlineLightMode } from 'react-icons/md';
import { useScreenSizeTrigger } from '@/app/public/screenSizeTrigger'; import { useScreenSizeTrigger } from '@/app/(pages)/public/screenSizeTrigger';
const switcherElements = new Map( const switcherElements = new Map(
[ [
+2 -1
View File
@@ -11,8 +11,8 @@
"dependencies": { "dependencies": {
"@nextui-org/react": "^2.4.6", "@nextui-org/react": "^2.4.6",
"@types/react-syntax-highlighter": "^15.5.13", "@types/react-syntax-highlighter": "^15.5.13",
"framer-motion": "^11.3.24",
"lxgw-wenkai-webfont": "^1.7.0", "lxgw-wenkai-webfont": "^1.7.0",
"motion": "^12.3.1",
"next": "^15.0.3", "next": "^15.0.3",
"next-themes": "^0.3.0", "next-themes": "^0.3.0",
"react": "^18.3.1", "react": "^18.3.1",
@@ -20,6 +20,7 @@
"react-icons": "^5.2.1", "react-icons": "^5.2.1",
"react-markdown": "^9.0.1", "react-markdown": "^9.0.1",
"react-syntax-highlighter": "^15.5.0", "react-syntax-highlighter": "^15.5.0",
"react-use": "^17.6.0",
"rehype-raw": "^7.0.0", "rehype-raw": "^7.0.0",
"rehype-sanitize": "^6.0.0", "rehype-sanitize": "^6.0.0",
"remark-frontmatter": "^5.0.0", "remark-frontmatter": "^5.0.0",
@@ -0,0 +1,18 @@
import React from 'react';
export abstract class Activity {
protected constructor(
readonly time: Date
) {
}
abstract toElement(): React.ReactElement;
}
export abstract class GhActivity extends Activity {
protected constructor(time: Date) {
super(time);
}
}
@@ -0,0 +1 @@
export const GH_USER_NAME = 'Lumither';
@@ -0,0 +1,34 @@
import { GhActivity } from '@/src/HomePageActivities/activity';
import { Card, CardBody } from '@nextui-org/card';
import Link from 'next/link';
export class StarRepo extends GhActivity {
constructor(
readonly repoName: string,
readonly repoUrl: string,
time: Date
) {
super(time);
}
toElement() {
return (
// <div>
// <Link
// href={ this.repoUrl }
// >{ `${ GH_USER_NAME } started ${ this.repoName } at ${ this.time }` }</Link>
// </div>
<Card
isHoverable={ true }
as={ Link }
href={ this.repoUrl }
className={ 'bg-black/20' }
>
<CardBody>
<p className={ 'text-sm' }>{ `Stared GitHub repository '${ this.repoName }'` }</p>
</CardBody>
</Card>
);
}
}
@@ -0,0 +1,32 @@
import { GhActivity } from '@/src/HomePageActivities/activity';
import { match } from 'ts-pattern';
import { StarRepo } from '@/src/HomePageActivities/ghActivities/starRepo';
import { GH_USER_NAME } from '@/src/HomePageActivities/constants';
export const getGhActivities: () => Promise<GhActivity[]> = async () => {
const data = await fetch(`https://api.github.com/users/${ GH_USER_NAME }/events`, {
headers: {
'Accept': 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28'
}
});
return data
.json()
.then((ret: any): GhActivity[] => (
ret
.map((res: any) => (
match(res)
.returnType<GhActivity | undefined>()
// star repo
.with(
{ type: 'WatchEvent', payload: { action: 'started' } }
, () => (
new StarRepo(res.repo.name, res.repo.url, res.created_at)
))
// to be continued
.otherwise(() => undefined))
)
.filter((res: GhActivity | undefined) => res !== undefined)
));
};
+4
View File
@@ -0,0 +1,4 @@
export const SITE_CONFIG = {
name: 'Atelier Lumiaethor',
desc: ''
};
+3
View File
@@ -0,0 +1,3 @@
target/
Cargo.lock
@@ -1,5 +1,5 @@
[package] [package]
name = "constants" name = "gateway"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
+3
View File
@@ -0,0 +1,3 @@
fn main() {
println!("Hello, world!");
}
+6 -13
View File
@@ -1,24 +1,17 @@
# server {
# listen 80;
# server_name lumither.com;
# return 301 https://$host$request_uri;
# }
server { server {
listen 80; listen 80;
# listen 443 ssl; listen 443 ssl;
# server_name lumither.com; server_name lumither.com;
# ssl_certificate certs/cert.pem; # ssl_certificate certs/cert.pem;
# ssl_certificate_key certs/key.pem; # ssl_certificate_key certs/key.pem;
# ssl_session_cache shared:SSL:1m; ssl_session_cache shared:SSL:1m;
# ssl_session_timeout 5m; ssl_session_timeout 5m;
# ssl_ciphers HIGH:!aNULL:!MD5; ssl_ciphers HIGH:!aNULL:!MD5;
# ssl_prefer_server_ciphers on; ssl_prefer_server_ciphers on;
location / { location / {
default_type text/html; default_type text/html;
-4
View File
@@ -1,4 +0,0 @@
#!/bin/bash
rm -rf /opt/openresty/nginx/lua
cp -r ./lua /opt/openresty/nginx
-1
View File
@@ -1 +0,0 @@
ngx.say("hello world")
-14
View File
@@ -1,14 +0,0 @@
#!/bin/bash
# refresh env
/bin/bash ./flush_env.sh
# start frontend
cd frontend || exit
screen -S nocturne_frontend -d -m bun dev
cd ..
# start backend
cd backend || exit
screen -S nocturne_backend -d -m cargo r
cd ..
-6
View File
@@ -1,6 +0,0 @@
#!/bin/env bash
cp .env ./backend
cp .env ./frontend
cp .env.local ./backend
cp .env.local ./frontend
-42
View File
@@ -1,42 +0,0 @@
#!/bin/bash
source .env
if [ -z "$BLOG_ENDPOINT" ]; then
echo "BLOG_ENDPOINT is not set in the .env file"
exit 1
fi
if [ -z "$GIT_WORK_DIR" ]; then
echo "GIT_WORK_DIR is not set in the .env file"
exit 1
fi
if [ -z "$BACKEND_PORT" ]; then
echo "BACKEND_PORT is not set in the .env file"
exit 1
fi
check_for_updates() {
cd "$GIT_WORK_DIR" || exit
res=$(git pull)
if echo "$res" | grep -q "Already up to date"; then
echo "Repo is up to date."
else
echo "Repo has updated; calling db update"
curl --request POST -sL \
--url "localhost:$BACKEND_PORT"
#todo: to be continued
fi
}
if [ -d "$GIT_WORK_DIR" ]; then
check_for_updates
else
git clone "$BLOG_ENDPOINT" "$GIT_WORK_DIR"
if [ $? -eq 0 ]; then
check_for_updates
else
echo "Failed to clone the repo"
fi
fi
-15
View File
@@ -1,15 +0,0 @@
#!/bin/bash
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
. "$DIR/.env"
sudo groupadd nocturne
USERNAME="$(whoami)"
echo "$USERNAME"
sudo usermod -aG nocturne "$USERNAME"
sudo mkdir -p "$LOG_ROOT_DIR/backend" "$LOG_ROOT_DIR/frontend"
sudo chown -R :nocturne "$LOG_ROOT_DIR"
sudo chmod -R 775 "$LOG_ROOT_DIR"
-20
View File
@@ -1,20 +0,0 @@
#!/bin/bash
# refresh env
/bin/bash ./flush_env.sh
# start frontend
(
cd frontend || exit
bun i
bun next build
screen -S nocturne_frontend -d -m bun start
cd ..
) &
# start backend
(
cd backend || exit
screen -S nocturne_backend -d -m cargo r -r
cd ..
) & wait