use axum::{ extract::Multipart, http::StatusCode, response::IntoResponse, routing::post, Router, }; use serde::Deserialize; use std::process::Command; use tempfile::NamedTempFile; use tokio::fs; use tower_http::cors::CorsLayer; #[derive(Deserialize, Clone)] // Добавили Clone struct PrintSettings { filename: String, pages: String, copies: u32, #[serde(rename = "colorMode")] color_mode: String, format: String, } #[tokio::main] async fn main() { tracing_subscriber::fmt::init(); let cors = CorsLayer::permissive(); let app = Router::new() .route("/print", post(handle_print)) .layer(cors); println!("🖨️ Print API запущен на http://0.0.0.0:3000"); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000") .await .unwrap(); axum::serve(listener, app).await.unwrap(); } async fn handle_print(mut multipart: Multipart) -> impl IntoResponse { let mut printed_files: Vec = Vec::new(); let mut errors: Vec = Vec::new(); let mut settings_map: std::collections::HashMap = std::collections::HashMap::new(); let mut files_data: Vec<(String, bytes::Bytes)> = Vec::new(); while let Some(field) = multipart.next_field().await.unwrap_or(None) { let field_name = field.name().unwrap_or("").to_string(); if field_name == "settings" { if let Ok(text) = field.text().await { if let Ok(settings) = serde_json::from_str::(&text) { settings_map.insert(settings.filename.clone(), settings); } } } else if field_name == "files" { let file_name = field.file_name().unwrap_or("unknown").to_string(); if let Ok(data) = field.bytes().await { files_data.push((file_name, data)); } } } for (file_name, data) in files_data { // Клонируем настройки перед использованием let settings = settings_map.get(&file_name).cloned(); let ext = std::path::Path::new(&file_name) .extension() .and_then(|e| e.to_str()) .unwrap_or(""); let temp_file = if ext.is_empty() { NamedTempFile::new() } else { NamedTempFile::with_suffix(&format!(".{}", ext)) }; let temp_file = match temp_file { Ok(f) => f, Err(e) => { errors.push(format!("Не удалось создать temp файл для {}: {}", file_name, e)); continue; } }; let path = temp_file.path().to_path_buf(); if let Err(e) = fs::write(&path, &data).await { errors.push(format!("Ошибка записи {}: {}", file_name, e)); continue; } // Теперь settings это Option, а не ссылка let print_result = tokio::task::spawn_blocking(move || { let mut cmd = Command::new("lp"); if let Some(s) = settings { cmd.arg("-n").arg(s.copies.to_string()); if s.pages != "all" && !s.pages.is_empty() { cmd.arg("-P").arg(&s.pages); } if s.color_mode == "bw" { cmd.arg("-o").arg("ColorModel=Gray"); } cmd.arg("-o").arg(format!("PageSize={}", s.format)); } cmd.arg(&path).output() }) .await; match print_result { Ok(Ok(output)) if output.status.success() => { printed_files.push(file_name); } Ok(Ok(output)) => { let stderr = String::from_utf8_lossy(&output.stderr); errors.push(format!("lp ошибка для {}: {}", file_name, stderr)); } _ => { errors.push(format!("Не удалось запустить lp для {}", file_name)); } } } if errors.is_empty() && !printed_files.is_empty() { (StatusCode::OK, format!("Напечатано {} файлов", printed_files.len())) } else if printed_files.is_empty() { (StatusCode::BAD_REQUEST, format!("Ошибки: {:?}", errors)) } else { ( StatusCode::MULTI_STATUS, format!("Частично: {:?}, Ошибки: {:?}", printed_files, errors), ) } }