juiceback/routes/
register.rs1use axum::{
5 extract::State,
6 http::HeaderMap,
7 Json,
8};
9use serde::{Deserialize, Serialize};
10use std::sync::Arc;
11
12use crate::db::{self, FileRecord};
13use crate::error::AppError;
14use crate::routes::upload::sanitize_filename;
15use crate::state::AppState;
16
17#[derive(Deserialize)]
19pub struct RegisterRequest {
20 pub id: String,
21 pub filename: String,
22 pub mime_type: String,
23 pub size_bytes: i64,
24 pub ttl_hours: Option<u32>,
25 pub uploader_ip: Option<String>,
26 pub storage_host: Option<String>,
27}
28
29#[derive(Serialize)]
31pub struct RegisterResponse {
32 pub delete_token: String,
33 pub expires_at: i64,
34}
35
36pub async fn register_handler(
37 State(state): State<Arc<AppState>>,
38 headers: HeaderMap,
39 Json(payload): Json<RegisterRequest>,
40) -> Result<Json<RegisterResponse>, AppError> {
41 if !state.config.juicehost_api_key.is_empty() {
42 let provided = headers
43 .get("x-juicehost-api-key")
44 .and_then(|v| v.to_str().ok())
45 .unwrap_or("");
46 if provided != state.config.juicehost_api_key {
47 return Err(AppError::Forbidden);
48 }
49 }
50
51 if payload.id.is_empty() || payload.id.chars().any(|c| !c.is_alphanumeric() && c != '-' && c != '_') {
52 return Err(AppError::InvalidMultipart("Invalid file ID format".into()));
53 }
54
55 let filename = sanitize_filename(&payload.filename);
56
57 let ttl_hours = payload
58 .ttl_hours
59 .unwrap_or(state.config.default_ttl_hours)
60 .clamp(1, state.config.max_ttl_hours);
61
62 let now = chrono::Utc::now().timestamp();
63 let delete_token = uuid::Uuid::new_v4().to_string();
64 let expires_at = now + (ttl_hours as i64 * 3600);
65
66 let file_id = payload.id.clone();
67 let record = FileRecord {
68 id: payload.id,
69 filename,
70 mime_type: payload.mime_type,
71 size_bytes: payload.size_bytes,
72 storage_path: format!("remote-{}", file_id),
73 delete_token: delete_token.clone(),
74 uploaded_at: now,
75 expires_at,
76 uploader_ip: payload.uploader_ip,
77 storage_host: payload.storage_host.unwrap_or_default(),
78 };
79
80 let state2 = Arc::clone(&state);
81 let record2 = record.clone();
82
83 tokio::task::spawn_blocking(move || -> Result<(), AppError> {
84 let db = state2
85 .db
86 .get()
87 .map_err(|e| AppError::DbPoolError(e.to_string()))?;
88 db::insert_file(&db, &record2).map_err(AppError::DatabaseError)
89 })
90 .await
91 .map_err(|_| AppError::TaskPanicked("db task panicked".into()))?
92 .map_err(|e| e)?;
93
94 tracing::info!(
95 "register: id={} size={} mime={} expires_at={}",
96 record.id,
97 record.size_bytes,
98 record.mime_type,
99 record.expires_at,
100 );
101
102 Ok(Json(RegisterResponse {
103 delete_token,
104 expires_at,
105 }))
106}