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,43 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub type JobId = Uuid;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum JobStatus {
|
||||
Queued,
|
||||
Dialing,
|
||||
Sending,
|
||||
Completed,
|
||||
Failed(String),
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FaxJob {
|
||||
pub id: JobId,
|
||||
pub recipient: String,
|
||||
pub document_path: String,
|
||||
pub status: JobStatus,
|
||||
pub pages: u32,
|
||||
pub retries: u8,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl FaxJob {
|
||||
pub fn new(recipient: String, document_path: String) -> Self {
|
||||
let now = Utc::now();
|
||||
Self {
|
||||
id: Uuid::new_v4(),
|
||||
recipient,
|
||||
document_path,
|
||||
status: JobStatus::Queued,
|
||||
pages: 0,
|
||||
retries: 0,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
pub mod job;
|
||||
pub mod store;
|
||||
|
||||
pub use job::{FaxJob, JobId, JobStatus};
|
||||
pub use store::FaxQueue;
|
||||
@@ -0,0 +1,121 @@
|
||||
use crate::error::Result;
|
||||
use crate::queue::job::{FaxJob, JobId, JobStatus};
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
use rusqlite::Connection;
|
||||
|
||||
pub enum FaxQueue {
|
||||
InMemory(InMemoryStore),
|
||||
#[cfg(feature = "server")]
|
||||
Sqlite(SqliteStore),
|
||||
}
|
||||
|
||||
pub struct InMemoryStore {
|
||||
jobs: HashMap<JobId, FaxJob>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
pub struct SqliteStore {
|
||||
conn: Connection,
|
||||
}
|
||||
|
||||
impl FaxQueue {
|
||||
pub fn new_in_memory() -> Self {
|
||||
FaxQueue::InMemory(InMemoryStore {
|
||||
jobs: HashMap::new(),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
pub fn new_sqlite(path: &Path) -> Result<Self> {
|
||||
let conn = Connection::open(path)?;
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS fax_jobs (
|
||||
id TEXT PRIMARY KEY,
|
||||
recipient TEXT NOT NULL,
|
||||
document_path TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'Queued',
|
||||
pages INTEGER DEFAULT 0,
|
||||
retries INTEGER DEFAULT 0,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
Ok(FaxQueue::Sqlite(SqliteStore { conn }))
|
||||
}
|
||||
|
||||
pub fn enqueue(&mut self, job: FaxJob) -> Result<()> {
|
||||
match self {
|
||||
FaxQueue::InMemory(s) => {
|
||||
s.jobs.insert(job.id, job);
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(feature = "server")]
|
||||
FaxQueue::Sqlite(s) => {
|
||||
s.conn.execute(
|
||||
"INSERT INTO fax_jobs (id, recipient, document_path, status, pages, retries, created_at, updated_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
|
||||
rusqlite::params![
|
||||
job.id.to_string(),
|
||||
job.recipient,
|
||||
job.document_path,
|
||||
format!("{:?}", job.status),
|
||||
job.pages,
|
||||
job.retries,
|
||||
job.created_at.to_rfc3339(),
|
||||
job.updated_at.to_rfc3339(),
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(&self, id: &JobId) -> Option<FaxJob> {
|
||||
match self {
|
||||
FaxQueue::InMemory(s) => s.jobs.get(id).cloned(),
|
||||
#[cfg(feature = "server")]
|
||||
FaxQueue::Sqlite(_) => None, // TODO
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_status(&mut self, id: &JobId, status: JobStatus) -> Result<()> {
|
||||
match self {
|
||||
FaxQueue::InMemory(s) => {
|
||||
if let Some(job) = s.jobs.get_mut(id) {
|
||||
job.status = status;
|
||||
job.updated_at = chrono::Utc::now();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(feature = "server")]
|
||||
FaxQueue::Sqlite(_s) => {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn list(&self) -> Vec<FaxJob> {
|
||||
match self {
|
||||
FaxQueue::InMemory(s) => s.jobs.values().cloned().collect(),
|
||||
#[cfg(feature = "server")]
|
||||
FaxQueue::Sqlite(_) => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pending_jobs(&self) -> Vec<FaxJob> {
|
||||
match self {
|
||||
FaxQueue::InMemory(s) => s
|
||||
.jobs
|
||||
.values()
|
||||
.filter(|j| matches!(j.status, JobStatus::Queued))
|
||||
.cloned()
|
||||
.collect(),
|
||||
#[cfg(feature = "server")]
|
||||
FaxQueue::Sqlite(_) => Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user