use axum::{ extract::{DefaultBodyLimit, 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; use tracing::{error, info, warn}; #[derive(Deserialize, Clone, Debug)] struct PrintSettings { filename: String, pages: String, copies: u32, #[serde(rename = "colorMode")] color_mode: String, format: String, } #[tokio::main] async fn main() { tracing_subscriber::fmt() .with_target(true) .with_thread_ids(true) .with_file(true) .with_line_number(true) .init(); let cors = CorsLayer::permissive(); let app = Router::new() .route("/print", post(handle_print)) .layer(DefaultBodyLimit::max(50 * 1024 * 1024)) // 50 MB .layer(cors); info!("🖨️ Print API запущен на http://0.0.0.0:3000"); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000") .await .expect("Не удалось забиндить порт 3000"); axum::serve(listener, app).await.expect("Ошибка сервера"); } async fn handle_print(mut multipart: Multipart) -> impl IntoResponse { info!("📥 Получен запрос на печать"); 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(); // ── Парсинг multipart ────────────────────────────────────────────── while let Some(field) = multipart.next_field().await.unwrap_or(None) { let field_name = field.name().unwrap_or("").to_string(); if field_name == "settings" { match field.text().await { Ok(text) => { info!(raw_settings = %text, "Получены настройки печати"); match serde_json::from_str::(&text) { Ok(settings) => { info!( filename = %settings.filename, pages = %settings.pages, copies = settings.copies, color_mode = %settings.color_mode, format = %settings.format, "✅ Настройки распарсены" ); settings_map.insert(settings.filename.clone(), settings); } Err(e) => { error!(raw_text = %text, error = %e, "❌ Не удалось распарсить settings JSON"); errors.push(format!("Ошибка парсинга settings: {}", e)); } } } Err(e) => { error!(error = %e, "❌ Не удалось прочитать поле settings"); errors.push(format!("Ошибка чтения settings: {}", e)); } } } else if field_name == "files" { let file_name = field.file_name().unwrap_or("unknown").to_string(); match field.bytes().await { Ok(data) => { info!( file_name = %file_name, size_bytes = data.len(), "📎 Получен файл" ); files_data.push((file_name, data)); } Err(e) => { error!(file_name = %file_name, error = %e, "❌ Не удалось прочитать файл"); errors.push(format!("Ошибка чтения файла {}: {}", file_name, e)); } } } else { warn!(field_name = %field_name, "⚠️ Неизвестное поле в multipart, пропущено"); } } info!( total_files = files_data.len(), total_settings = settings_map.len(), "📊 Итоги парсинга multipart" ); if files_data.is_empty() { error!("❌ В запросе нет файлов для печати"); return ( StatusCode::BAD_REQUEST, "В запросе отсутствуют файлы для печати".to_string(), ); } // ── Обработка каждого файла ──────────────────────────────────────── for (file_name, data) in files_data { info!(file_name = %file_name, "🔄 Начало обработки файла"); // Нормализация расширения let ext = std::path::Path::new(&file_name) .extension() .and_then(|e| e.to_str()) .unwrap_or("") .to_lowercase(); info!(file_name = %file_name, extension = %ext, "Определено расширение файла"); // Создание временного файла 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) => { error!(file_name = %file_name, error = %e, "❌ Не удалось создать temp файл"); errors.push(format!("Не удалось создать temp файл для {}: {}", file_name, e)); continue; } }; let path = temp_file.path().to_path_buf(); info!( file_name = %file_name, temp_path = %path.display(), "📝 Временный файл создан" ); // Запись данных во временный файл if let Err(e) = fs::write(&path, &data).await { error!( file_name = %file_name, temp_path = %path.display(), error = %e, "❌ Ошибка записи во временный файл" ); errors.push(format!("Ошибка записи {}: {}", file_name, e)); continue; } info!( file_name = %file_name, temp_path = %path.display(), bytes_written = data.len(), "✅ Файл записан во временную директорию" ); // Поиск настроек для данного файла let settings = settings_map.get(&file_name).cloned(); if settings.is_none() { warn!( file_name = %file_name, "⚠️ Настройки печати не найдены для файла, используются значения по умолчанию" ); } // Клонируем file_name для использования после spawn_blocking let file_name_for_log = file_name.clone(); // Формирование и выполнение команды lp let print_result = tokio::task::spawn_blocking(move || { let mut cmd = Command::new("lp"); if let Some(ref 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); // Логируем итоговую команду let args: Vec = cmd .get_args() .map(|a| a.to_string_lossy().to_string()) .collect(); info!( file_name = %file_name, command = format!("lp {}", args.join(" ")), "🖨️ Выполнение команды печати" ); cmd.output() }) .await; // Обработка результата — используем file_name_for_log вместо file_name match print_result { Ok(Ok(output)) if output.status.success() => { let stdout = String::from_utf8_lossy(&output.stdout); info!( file_name = %file_name_for_log, stdout = %stdout.trim(), "✅ Файл успешно отправлен на печать" ); printed_files.push(file_name_for_log); } Ok(Ok(output)) => { let stderr = String::from_utf8_lossy(&output.stderr); let stdout = String::from_utf8_lossy(&output.stdout); error!( file_name = %file_name_for_log, exit_code = ?output.status.code(), stderr = %stderr.trim(), stdout = %stdout.trim(), "❌ Команда lp завершилась с ошибкой" ); errors.push(format!( "lp ошибка для {}: {}", file_name_for_log, stderr.trim() )); } Ok(Err(e)) => { error!( file_name = %file_name_for_log, error = %e, "❌ Не удалось запустить процесс lp" ); errors.push(format!( "Не удалось запустить lp для {}: {}", file_name_for_log, e )); } Err(e) => { error!( file_name = %file_name_for_log, error = %e, "❌ Ошибка join blocking task" ); errors.push(format!( "Ошибка выполнения задачи для {}: {}", file_name_for_log, e )); } } } // ── Формирование ответа ──────────────────────────────────────────── info!( printed_count = printed_files.len(), error_count = errors.len(), "📋 Итоговый результат обработки запроса" ); if errors.is_empty() && !printed_files.is_empty() { info!(files = ?printed_files, "🎉 Все файлы успешно напечатаны"); ( StatusCode::OK, format!("Напечатано {} файлов", printed_files.len()), ) } else if printed_files.is_empty() { error!(errors = ?errors, "💥 Все файлы не удалось напечатать"); ( StatusCode::BAD_REQUEST, format!("Ошибки: {:?}", errors), ) } else { warn!( printed = ?printed_files, errors = ?errors, "⚠️ Частичная печать: некоторые файлы не напечатаны" ); ( StatusCode::MULTI_STATUS, format!("Частично: {:?}, Ошибки: {:?}", printed_files, errors), ) } }