Initial commit: telfax - Rust Fax Server
- Modem driver (serialport) with AT command interface - Class 1 fax protocol (T.30 session, HDLC framing) - T.4/T.6 image codec (fax crate integration) - Document conversion (image to fax, TIFF-F output) - REST API (axum) for fax job management - SQLite/InMemory job queue - CLI interface (detect/serve/send/test) - Integration tests passing
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
use axum::{
|
||||
extract::State,
|
||||
http::StatusCode,
|
||||
response::Json,
|
||||
routing::{get, post},
|
||||
Router,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tracing::info;
|
||||
|
||||
use crate::config::FaxConfig;
|
||||
use crate::error::Result as FaxResult;
|
||||
use crate::queue::job::{FaxJob, JobId};
|
||||
use crate::queue::store::FaxQueue;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub config: FaxConfig,
|
||||
pub queue: Arc<Mutex<FaxQueue>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct HealthResponse {
|
||||
pub status: String,
|
||||
pub device: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct SendFaxRequest {
|
||||
pub recipient: String,
|
||||
pub document_path: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct SendFaxResponse {
|
||||
pub job_id: JobId,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct JobResponse {
|
||||
pub id: JobId,
|
||||
pub recipient: String,
|
||||
pub status: String,
|
||||
pub pages: u32,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
async fn health(State(state): State<AppState>) -> Json<HealthResponse> {
|
||||
Json(HealthResponse {
|
||||
status: "ok".to_string(),
|
||||
device: state.config.device.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn send_fax(
|
||||
State(state): State<AppState>,
|
||||
Json(req): Json<SendFaxRequest>,
|
||||
) -> Result<Json<SendFaxResponse>, StatusCode> {
|
||||
let job = FaxJob::new(req.recipient, req.document_path);
|
||||
let job_id = job.id;
|
||||
|
||||
let mut queue = state.queue.lock().unwrap();
|
||||
queue.enqueue(job).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
info!(job_id = %job_id, "Fax job queued");
|
||||
Ok(Json(SendFaxResponse { job_id }))
|
||||
}
|
||||
|
||||
async fn list_jobs(State(state): State<AppState>) -> Json<Vec<JobResponse>> {
|
||||
let queue = state.queue.lock().unwrap();
|
||||
let jobs = queue.list();
|
||||
let responses: Vec<JobResponse> = jobs
|
||||
.into_iter()
|
||||
.map(|j| JobResponse {
|
||||
id: j.id,
|
||||
recipient: j.recipient,
|
||||
status: format!("{:?}", j.status),
|
||||
pages: j.pages,
|
||||
created_at: j.created_at.to_rfc3339(),
|
||||
})
|
||||
.collect();
|
||||
Json(responses)
|
||||
}
|
||||
|
||||
async fn get_job(
|
||||
State(state): State<AppState>,
|
||||
axum::extract::Path(id): axum::extract::Path<JobId>,
|
||||
) -> Result<Json<JobResponse>, StatusCode> {
|
||||
let queue = state.queue.lock().unwrap();
|
||||
queue.get(&id).map(|j| Json(JobResponse {
|
||||
id: j.id,
|
||||
recipient: j.recipient,
|
||||
status: format!("{:?}", j.status),
|
||||
pages: j.pages,
|
||||
created_at: j.created_at.to_rfc3339(),
|
||||
})).ok_or(StatusCode::NOT_FOUND)
|
||||
}
|
||||
|
||||
pub async fn start_server(config: FaxConfig, queue: Arc<Mutex<FaxQueue>>) -> FaxResult<()> {
|
||||
let state = AppState {
|
||||
config: config.clone(),
|
||||
queue,
|
||||
};
|
||||
|
||||
let app = Router::new()
|
||||
.route("/api/health", get(health))
|
||||
.route("/api/fax/send", post(send_fax))
|
||||
.route("/api/fax/jobs", get(list_jobs))
|
||||
.route("/api/fax/jobs/{id}", get(get_job))
|
||||
.with_state(state);
|
||||
|
||||
let addr = config
|
||||
.api_listen
|
||||
.clone()
|
||||
.unwrap_or_else(|| "0.0.0.0:3000".to_string());
|
||||
info!("Starting API server on {}", addr);
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(&addr)
|
||||
.await
|
||||
.map_err(|e| crate::error::FaxError::Other(format!("Failed to bind: {}", e)))?;
|
||||
|
||||
axum::serve(listener, app)
|
||||
.await
|
||||
.map_err(|e| crate::error::FaxError::Other(format!("Server error: {}", e)))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user