feat: Add Rust API Server with REST and GraphQL support

- Implement JWT authentication layer
- Add Engine Registry with 20+ conversion engines
- Implement Conversion Service with background job processing
- REST API endpoints for file conversion operations
- GraphQL API with queries and mutations
- Comprehensive error handling with conversion suggestions
- Integration tests for both REST and GraphQL APIs
- Complete API documentation

Features:
- REST API: /api/v1/* endpoints
- GraphQL API: /graphql endpoint with playground
- JWT Bearer token authentication
- Engine validation with alternative suggestions
- File download via API (not exposing file paths)
- Support for FFmpeg, ImageMagick, LibreOffice, Pandoc, and more
This commit is contained in:
Your Name 2026-01-21 11:40:03 +08:00
parent d0388066a5
commit e083e5d11d
18 changed files with 5235 additions and 0 deletions

215
api-server/src/models.rs Normal file
View file

@ -0,0 +1,215 @@
//! Models module - Data structures for API requests and responses
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use chrono::{DateTime, Utc};
/// Job status enumeration
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum JobStatus {
Pending,
Processing,
Completed,
Failed,
}
impl std::fmt::Display for JobStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
JobStatus::Pending => write!(f, "pending"),
JobStatus::Processing => write!(f, "processing"),
JobStatus::Completed => write!(f, "completed"),
JobStatus::Failed => write!(f, "failed"),
}
}
}
/// Conversion job representation
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConversionJob {
/// Unique job identifier
pub id: Uuid,
/// User who created the job
pub user_id: String,
/// Original filename
pub original_filename: String,
/// Source file format
pub source_format: String,
/// Target file format
pub target_format: String,
/// Engine used for conversion
pub engine: String,
/// Current job status
pub status: JobStatus,
/// Output filename (when completed)
#[serde(skip_serializing_if = "Option::is_none")]
pub output_filename: Option<String>,
/// Error message (when failed)
#[serde(skip_serializing_if = "Option::is_none")]
pub error_message: Option<String>,
/// Job creation timestamp
pub created_at: DateTime<Utc>,
/// Job completion timestamp
#[serde(skip_serializing_if = "Option::is_none")]
pub completed_at: Option<DateTime<Utc>>,
/// Conversion options
#[serde(skip_serializing_if = "Option::is_none")]
pub options: Option<serde_json::Value>,
}
impl ConversionJob {
/// Create a new pending conversion job
pub fn new(
user_id: String,
original_filename: String,
source_format: String,
target_format: String,
engine: String,
options: Option<serde_json::Value>,
) -> Self {
Self {
id: Uuid::new_v4(),
user_id,
original_filename,
source_format,
target_format,
engine,
status: JobStatus::Pending,
output_filename: None,
error_message: None,
created_at: Utc::now(),
completed_at: None,
options,
}
}
/// Mark job as processing
pub fn set_processing(&mut self) {
self.status = JobStatus::Processing;
}
/// Mark job as completed
pub fn set_completed(&mut self, output_filename: String) {
self.status = JobStatus::Completed;
self.output_filename = Some(output_filename);
self.completed_at = Some(Utc::now());
}
/// Mark job as failed
pub fn set_failed(&mut self, error_message: String) {
self.status = JobStatus::Failed;
self.error_message = Some(error_message);
self.completed_at = Some(Utc::now());
}
}
/// Request to create a conversion job
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateJobRequest {
/// Engine to use for conversion (required)
pub engine: String,
/// Target format (required)
pub target_format: String,
/// Optional conversion options
#[serde(skip_serializing_if = "Option::is_none")]
pub options: Option<serde_json::Value>,
}
/// Response after creating a conversion job
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateJobResponse {
/// Job ID
pub job_id: Uuid,
/// Current status
pub status: JobStatus,
/// Message
pub message: String,
}
/// Job status response
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JobStatusResponse {
/// Job ID
pub job_id: Uuid,
/// Current status
pub status: JobStatus,
/// Original filename
pub original_filename: String,
/// Source format
pub source_format: String,
/// Target format
pub target_format: String,
/// Engine used
pub engine: String,
/// Download URL (when completed)
#[serde(skip_serializing_if = "Option::is_none")]
pub download_url: Option<String>,
/// Error message (when failed)
#[serde(skip_serializing_if = "Option::is_none")]
pub error_message: Option<String>,
/// Creation time
pub created_at: DateTime<Utc>,
/// Completion time
#[serde(skip_serializing_if = "Option::is_none")]
pub completed_at: Option<DateTime<Utc>>,
}
impl From<&ConversionJob> for JobStatusResponse {
fn from(job: &ConversionJob) -> Self {
let download_url = if job.status == JobStatus::Completed {
Some(format!("/api/v1/jobs/{}/download", job.id))
} else {
None
};
Self {
job_id: job.id,
status: job.status,
original_filename: job.original_filename.clone(),
source_format: job.source_format.clone(),
target_format: job.target_format.clone(),
engine: job.engine.clone(),
download_url,
error_message: job.error_message.clone(),
created_at: job.created_at,
completed_at: job.completed_at,
}
}
}
/// Engine information
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EngineInfo {
/// Engine identifier
pub id: String,
/// Human-readable name
pub name: String,
/// Description
pub description: String,
/// Supported input formats
pub supported_input_formats: Vec<String>,
/// Supported output formats
pub supported_output_formats: Vec<String>,
}
/// List engines response
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListEnginesResponse {
pub engines: Vec<EngineInfo>,
}
/// Health check response
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HealthResponse {
pub status: String,
pub version: String,
pub timestamp: DateTime<Utc>,
}
/// List jobs response
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListJobsResponse {
pub jobs: Vec<JobStatusResponse>,
pub total: usize,
}