v0.2.0
This commit is contained in:
186
src/web.rs
Normal file
186
src/web.rs
Normal file
@@ -0,0 +1,186 @@
|
||||
use crate::config::Config;
|
||||
use crate::processor::{cleanup_finished_jobs, fetch_all_jobs, process_spreadsheet};
|
||||
use axum::{
|
||||
extract::State,
|
||||
http::StatusCode,
|
||||
response::{Html, IntoResponse, Json},
|
||||
routing::{get, post},
|
||||
Router,
|
||||
};
|
||||
use serde::Serialize;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
use tower_http::trace::TraceLayer;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub config: Config,
|
||||
pub last_generation: Arc<Mutex<Option<chrono::DateTime<chrono::Local>>>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct JobInfo {
|
||||
pub uid: String,
|
||||
pub outfile_name: String,
|
||||
pub state: String,
|
||||
pub created_at: Option<String>,
|
||||
pub updated_at: Option<String>,
|
||||
}
|
||||
|
||||
impl JobInfo {
|
||||
fn from_nexrender_json(job: &serde_json::Value) -> Option<Self> {
|
||||
let uid = job.get("uid")?.as_str()?.to_string();
|
||||
let state = job.get("state")?.as_str()?.to_string();
|
||||
|
||||
// Извлекаем имя выходного файла из postrender actions
|
||||
let outfile_name = job
|
||||
.get("actions")
|
||||
.and_then(|a| a.get("postrender"))
|
||||
.and_then(|p| p.as_array())
|
||||
.and_then(|arr| {
|
||||
// Ищем действие copy (в нём финальный путь)
|
||||
arr.iter()
|
||||
.find_map(|action| {
|
||||
// Проверяем, что это действие copy
|
||||
action
|
||||
.get("module")
|
||||
.and_then(|m| m.as_str())
|
||||
.filter(|&m| m == "@nexrender/action-copy")
|
||||
.and_then(|_| {
|
||||
// Извлекаем output из copy
|
||||
action.get("output").and_then(|o| o.as_str())
|
||||
})
|
||||
})
|
||||
// Если copy не найдено, пробуем encode
|
||||
.or_else(|| {
|
||||
arr.iter()
|
||||
.find_map(|action| action.get("output").and_then(|o| o.as_str()))
|
||||
})
|
||||
})
|
||||
.map(|path| {
|
||||
// Извлекаем только имя файла из полного пути
|
||||
std::path::Path::new(path)
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or(path)
|
||||
.to_string()
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
// Если не удалось извлечь, используем UID
|
||||
format!("job_{}", uid)
|
||||
});
|
||||
|
||||
Some(JobInfo {
|
||||
uid,
|
||||
outfile_name,
|
||||
state,
|
||||
created_at: job
|
||||
.get("createdAt")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string()),
|
||||
updated_at: job
|
||||
.get("updatedAt")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run_web_server(config: Config) -> anyhow::Result<()> {
|
||||
let state = AppState {
|
||||
config,
|
||||
last_generation: Arc::new(Mutex::new(None)),
|
||||
};
|
||||
|
||||
let app = Router::new()
|
||||
.route("/", get(index_page))
|
||||
.route("/api/jobs", get(list_jobs))
|
||||
.route("/api/generate", post(generate_jobs))
|
||||
.route("/api/cleanup", post(cleanup_jobs))
|
||||
.route("/api/status", get(get_status))
|
||||
.layer(TraceLayer::new_for_http())
|
||||
.with_state(state);
|
||||
|
||||
let addr: SocketAddr = "0.0.0.0:3000".parse()?;
|
||||
log::info!("Web server listening on http://{}", addr);
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(addr).await?;
|
||||
axum::serve(listener, app).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Остальные обработчики остаются без изменений...
|
||||
async fn index_page() -> Html<&'static str> {
|
||||
Html(include_str!("static/index.html"))
|
||||
}
|
||||
|
||||
async fn list_jobs(State(state): State<AppState>) -> Result<Json<Vec<JobInfo>>, AppError> {
|
||||
let jobs_json = fetch_all_jobs(&state.config.nexrender_api_url)
|
||||
.await
|
||||
.map_err(|e| AppError(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
let jobs: Vec<JobInfo> = jobs_json
|
||||
.iter()
|
||||
.filter_map(JobInfo::from_nexrender_json)
|
||||
.collect();
|
||||
|
||||
Ok(Json(jobs))
|
||||
}
|
||||
|
||||
async fn generate_jobs(State(state): State<AppState>) -> Result<impl IntoResponse, AppError> {
|
||||
let mut last_gen = state.last_generation.lock().await;
|
||||
if let Some(last) = *last_gen {
|
||||
let elapsed = chrono::Local::now().signed_duration_since(last);
|
||||
if elapsed.num_seconds() < 5 {
|
||||
return Err(AppError(
|
||||
StatusCode::TOO_MANY_REQUESTS,
|
||||
"Generation already in progress or too recent".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
*last_gen = Some(chrono::Local::now());
|
||||
drop(last_gen);
|
||||
|
||||
let config = state.config.clone();
|
||||
let last_gen_clone = state.last_generation.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
match process_spreadsheet(&config).await {
|
||||
Ok(submitted) => {
|
||||
log::info!("Generation completed, {} jobs submitted", submitted.len());
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Generation failed: {}", e);
|
||||
}
|
||||
}
|
||||
*last_gen_clone.lock().await = None;
|
||||
});
|
||||
|
||||
Ok((StatusCode::ACCEPTED, "Job generation started"))
|
||||
}
|
||||
|
||||
async fn cleanup_jobs(State(state): State<AppState>) -> Result<impl IntoResponse, AppError> {
|
||||
cleanup_finished_jobs(&state.config.nexrender_api_url)
|
||||
.await
|
||||
.map_err(|e| AppError(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
Ok((StatusCode::OK, "Cleanup completed"))
|
||||
}
|
||||
|
||||
async fn get_status(State(state): State<AppState>) -> Result<Json<serde_json::Value>, AppError> {
|
||||
let last_gen = *state.last_generation.lock().await;
|
||||
let status = serde_json::json!({
|
||||
"last_generation": last_gen.map(|dt| dt.to_rfc3339()),
|
||||
"nexrender_api": state.config.nexrender_api_url,
|
||||
});
|
||||
Ok(Json(status))
|
||||
}
|
||||
|
||||
// Error handling
|
||||
struct AppError(StatusCode, String);
|
||||
|
||||
impl IntoResponse for AppError {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
(self.0, self.1).into_response()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user