feat: add price handler, commit, push, pull

- price handler for getting or editing price (only applied to main profile)
- routine pull sync recipe repo & backup commit recover

Signed-off-by: Pakin <pakin.t@forth.co.th>
This commit is contained in:
Pakin 2026-05-05 17:03:33 +07:00
parent ab84060ab5
commit 0f857445a4
Notes: pakin 2026-05-05 20:35:20 +07:00
feat: commit
- not support multiple files yet
feat: routine backup commit flush
- not support order of commit yet, this may results in random commit
9 changed files with 610 additions and 23 deletions

View file

@ -4,14 +4,21 @@ use axum::{
routing::{get, post},
serve::ListenerExt,
};
use log::{error, info};
use log::{error, info, warn};
use redis::TypedCommands;
use reqwest::{StatusCode, multipart};
use std::{
collections::{HashMap, VecDeque},
env,
fs::{self, File},
io::BufReader,
sync::Arc,
time::Duration,
};
use tokio::{
fs::read_dir,
sync::{Mutex, mpsc::Sender},
};
use tokio::sync::{Mutex, mpsc::Sender};
#[derive(Clone)]
pub struct Hub {
@ -52,6 +59,18 @@ impl DevConfig {
format!("{}/checkout?path={}", self.get_recipe_url(), path)
}
pub fn get_post_file_to_recipe_repo(&self) -> String {
format!("{}/commit", self.get_recipe_url())
}
pub fn get_pull_recipe_repo(&self) -> String {
format!("{}/pull", self.get_recipe_url())
}
pub fn get_push_recipe_repo(&self) -> String {
format!("{}/push", self.get_recipe_url())
}
pub fn get_api_header(&self) -> (String, String) {
("X-API-Key".to_string(), self.api_key.clone())
}
@ -83,7 +102,7 @@ impl AppState {
let redis_cli_clone = redis_cli.clone();
let tx_new = system_tx.clone();
let result = Arc::new(AppState {
dev_config,
dev_config: dev_config.clone(),
redis_cli,
system_tx,
connectors_mapping: Arc::new(Mutex::new(Hub {
@ -91,6 +110,72 @@ impl AppState {
})),
});
// backup job
let dev_config_backup = dev_config.clone();
tokio::spawn(async move {
let m_cfg = dev_config_backup.clone();
loop {
// auto sync
if invoke_pull_sync_request(m_cfg.clone()).await.is_err() {
warn!("pulling repo unhealthy, retry again in 5 minutes");
continue;
}
match read_dir(".").await {
Ok(mut d) => {
while let Ok(Some(entry)) = d.next_entry().await {
let ent_path = entry.path();
if let Some(filename) = ent_path.file_name()
&& let Some(filename_str) = filename.to_str()
&& filename_str.starts_with("gtx")
&& filename_str.ends_with(".json")
{
// read file
//
let f = match File::open(ent_path.clone()) {
Ok(f) => f,
Err(_) => continue,
};
let buf = BufReader::new(f);
let commit_from_backup: CommitPayload =
match serde_json::from_reader(buf) {
Ok(cm) => cm,
Err(_) => continue,
};
if invoke_pull_sync_request(m_cfg.clone()).await.is_err() {
warn!("pulling repo unhealthy, retry again in 5 minutes");
continue;
}
let _ =
invoke_commit_request(m_cfg.clone(), commit_from_backup).await;
if invoke_push_request(m_cfg.clone()).await.is_ok() {
// push success
info!("push backup success");
if fs::remove_file(ent_path.clone()).is_ok() {
info!("clean backup");
}
}
} else {
continue;
}
}
}
Err(_) => {}
}
info!("[backup] idle");
tokio::time::sleep(Duration::from_mins(5)).await;
}
});
tokio::spawn(async move {
let mut lredis = redis_cli_clone.clone();
let current_queue: crossbeam_queue::ArrayQueue<CommandRequestPayload> =
@ -212,6 +297,82 @@ pub async fn invoke_checkout_request(
}
}
/// Invoke git pull, may takes sometime
pub async fn invoke_pull_sync_request(
config: DevConfig,
) -> Result<String, Box<dyn std::error::Error>> {
let client = reqwest::Client::new();
let req_path = config.get_pull_recipe_repo();
// println!("dbg: {req_path}");
let res = client.get(req_path).send().await?;
if res.status() != StatusCode::OK {
// pull fail
error!(
"invoke pull fail: [{}] {:?}",
res.status(),
res.text().await
);
return Err("pull fail".into());
}
match res.text().await {
Ok(raw) => Ok(raw),
Err(e) => Err(format!("{e}").into()),
}
}
/// Invoke sending from server to server for committing
pub async fn invoke_commit_request(
config: DevConfig,
payload: CommitPayload,
) -> Result<(), Box<dyn std::error::Error>> {
let client = reqwest::Client::new();
let commit_path = config.get_post_file_to_recipe_repo();
let form = multipart::Form::new()
.text("message", payload.message)
.text("signature_username", payload.signature_username)
.text("signature_email", payload.signature_email)
.text("path", payload.path)
.part(
"file",
multipart::Part::bytes(payload.file_bytes)
.mime_str("application/octet-stream")
.unwrap(),
);
let response = client.post(commit_path).multipart(form).send().await?;
info!("commit status: {}", response.status());
Ok(())
}
pub async fn invoke_push_request(config: DevConfig) -> Result<String, Box<dyn std::error::Error>> {
let client = reqwest::Client::new();
let req_path = config.get_push_recipe_repo();
// println!("dbg: {req_path}");
let res = client.get(req_path).send().await?;
if res.status() != StatusCode::OK {
// pull fail
error!(
"invoke push fail: [{}] {:?}",
res.status(),
res.text().await
);
return Err("push fail".into());
}
match res.text().await {
Ok(raw) => Ok(raw),
Err(e) => Err(format!("{e}").into()),
}
}
pub async fn create_recipe_repo_router() -> Router<Arc<AppState>> {
Router::new().route("/ws", get(crate::websocket::handler::websocket_handler))
}