polykit_cache/
server.rs

1//! HTTP server for artifact cache.
2
3use std::sync::Arc;
4
5use axum::{
6    extract::{Path, State},
7    http::StatusCode,
8    response::{IntoResponse, Response},
9    routing::{get, head, put},
10    Router,
11};
12use tower_http::trace::TraceLayer;
13
14use crate::storage::Storage;
15use crate::verification::Verifier;
16
17/// Server state shared across handlers.
18#[derive(Clone)]
19pub struct AppState {
20    storage: Arc<Storage>,
21    verifier: Arc<Verifier>,
22}
23
24impl AppState {
25    /// Creates new app state.
26    pub fn new(storage: Storage, verifier: Verifier) -> Self {
27        Self {
28            storage: Arc::new(storage),
29            verifier: Arc::new(verifier),
30        }
31    }
32}
33
34/// Creates the HTTP router.
35pub fn create_router(state: AppState) -> Router {
36    Router::new()
37        .route("/v1/artifacts/:cache_key", put(upload_artifact))
38        .route("/v1/artifacts/:cache_key", get(download_artifact))
39        .route("/v1/artifacts/:cache_key", head(check_artifact))
40        .layer(TraceLayer::new_for_http())
41        .with_state(state)
42}
43
44/// Uploads an artifact.
45///
46/// PUT /v1/artifacts/{cache_key}
47async fn upload_artifact(
48    State(state): State<AppState>,
49    Path(cache_key): Path<String>,
50    body: axum::body::Body,
51) -> Result<Response, ServerError> {
52    // Validate cache key format
53    if !cache_key.chars().all(|c| c.is_ascii_hexdigit()) || cache_key.len() < 32 {
54        return Err(ServerError::BadRequest(format!(
55            "Invalid cache key format: {}",
56            cache_key
57        )));
58    }
59
60    // Stream body to bytes with size limit
61    let max_size = state.storage.max_artifact_size() as usize;
62    let bytes = axum::body::to_bytes(body, max_size)
63        .await
64        .map_err(|e| {
65            if e.to_string().contains("too large") {
66                ServerError::PayloadTooLarge(format!(
67                    "Artifact size exceeds maximum {}",
68                    max_size
69                ))
70            } else {
71                ServerError::Internal(format!("Failed to read request body: {}", e))
72            }
73        })?;
74
75    // Verify artifact
76    let (artifact, hash) = state
77        .verifier
78        .verify_upload(&bytes, &cache_key)
79        .map_err(|e| ServerError::UnprocessableEntity(e.to_string()))?;
80
81    // Store artifact
82    state
83        .storage
84        .store_artifact(&cache_key, bytes.to_vec(), hash, &artifact)
85        .await
86        .map_err(|e| {
87            if e.to_string().contains("already exists") {
88                ServerError::Conflict(format!("Artifact {} already exists", cache_key))
89            } else {
90                ServerError::Internal(format!("Failed to store artifact: {}", e))
91            }
92        })?;
93
94    Ok(StatusCode::CREATED.into_response())
95}
96
97/// Downloads an artifact.
98///
99/// GET /v1/artifacts/{cache_key}
100async fn download_artifact(
101    State(state): State<AppState>,
102    Path(cache_key): Path<String>,
103) -> Result<Response, ServerError> {
104    // Validate cache key format
105    if !cache_key.chars().all(|c| c.is_ascii_hexdigit()) || cache_key.len() < 32 {
106        return Err(ServerError::BadRequest(format!(
107            "Invalid cache key format: {}",
108            cache_key
109        )));
110    }
111
112    // Check if artifact exists
113    if !state.storage.has_artifact(&cache_key) {
114        return Err(ServerError::NotFound);
115    }
116
117    // Read artifact
118    let data = state
119        .storage
120        .read_artifact(&cache_key)
121        .await
122        .map_err(|e| ServerError::Internal(format!("Failed to read artifact: {}", e)))?;
123
124    // Read metadata for headers
125    let metadata = state
126        .storage
127        .read_metadata(&cache_key)
128        .await
129        .map_err(|e| ServerError::Internal(format!("Failed to read metadata: {}", e)))?;
130
131    // Create response with proper headers
132    let response = Response::builder()
133        .status(StatusCode::OK)
134        .header("Content-Type", "application/zstd")
135        .header("Content-Length", data.len())
136        .header("X-Artifact-Hash", &metadata.hash)
137        .body(axum::body::Body::from(data))
138        .map_err(|e| ServerError::Internal(format!("Failed to create response: {}", e)))?;
139
140    Ok(response)
141}
142
143/// Checks if an artifact exists.
144///
145/// HEAD /v1/artifacts/{cache_key}
146async fn check_artifact(
147    State(state): State<AppState>,
148    Path(cache_key): Path<String>,
149) -> Result<Response, ServerError> {
150    // Validate cache key format
151    if !cache_key.chars().all(|c| c.is_ascii_hexdigit()) || cache_key.len() < 32 {
152        return Err(ServerError::BadRequest(format!(
153            "Invalid cache key format: {}",
154            cache_key
155        )));
156    }
157
158    if state.storage.has_artifact(&cache_key) {
159        // Read metadata for headers
160        if let Ok(metadata) = state.storage.read_metadata(&cache_key).await {
161            let response = Response::builder()
162                .status(StatusCode::OK)
163                .header("Content-Type", "application/zstd")
164                .header("Content-Length", metadata.size)
165                .header("X-Artifact-Hash", &metadata.hash)
166                .body(axum::body::Body::empty())
167                .map_err(|e| ServerError::Internal(format!("Failed to create response: {}", e)))?;
168
169            return Ok(response);
170        }
171    }
172
173    Err(ServerError::NotFound)
174}
175
176/// Server error types.
177#[derive(Debug)]
178pub enum ServerError {
179    BadRequest(String),
180    Conflict(String),
181    PayloadTooLarge(String),
182    UnprocessableEntity(String),
183    NotFound,
184    Internal(String),
185}
186
187impl IntoResponse for ServerError {
188    fn into_response(self) -> Response {
189        let (status, message) = match self {
190            ServerError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg),
191            ServerError::Conflict(msg) => (StatusCode::CONFLICT, msg),
192            ServerError::PayloadTooLarge(msg) => (StatusCode::PAYLOAD_TOO_LARGE, msg),
193            ServerError::UnprocessableEntity(msg) => (StatusCode::UNPROCESSABLE_ENTITY, msg),
194            ServerError::NotFound => (StatusCode::NOT_FOUND, "Artifact not found".to_string()),
195            ServerError::Internal(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg),
196        };
197
198        let body = axum::Json(serde_json::json!({ "error": message }));
199        (status, body).into_response()
200    }
201}