diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..61dfc77 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,5 @@ + +[alias] +b = "build --release" +br = "build --release" +r = "run --release" diff --git a/Cargo.lock b/Cargo.lock index 52e603d..236ea96 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -72,6 +72,7 @@ dependencies = [ "tempfile", "tokio", "tower-http", + "tracing", "tracing-subscriber", ] @@ -471,7 +472,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.4", ] [[package]] @@ -557,6 +558,17 @@ version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "3.0.4" @@ -621,7 +633,7 @@ checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.4", ] [[package]] @@ -676,9 +688,21 @@ checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "log", "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "tracing-core" version = "0.1.36" diff --git a/Cargo.toml b/Cargo.toml index 8452bc3..5fe54c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,3 +12,4 @@ tower-http = { version = "0.5", features = ["cors"] } serde = { version = "1", features = ["derive"] } serde_json = "1" bytes = "1" +tracing = "0.1" diff --git a/src/main.rs b/src/main.rs index 3edca39..9284fc6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,17 +1,19 @@ use axum::{ - extract::Multipart, + 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)] // Добавили Clone +#[derive(Deserialize, Clone, Debug)] struct PrintSettings { filename: String, pages: String, @@ -23,56 +25,119 @@ struct PrintSettings { #[tokio::main] async fn main() { - tracing_subscriber::fmt::init(); + 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); - println!("🖨️ Print API запущен на http://0.0.0.0:3000"); + info!("🖨️ Print API запущен на http://0.0.0.0:3000"); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000") .await - .unwrap(); + .expect("Не удалось забиндить порт 3000"); - axum::serve(listener, app).await.unwrap(); + 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 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" { - if let Ok(text) = field.text().await { - if let Ok(settings) = serde_json::from_str::(&text) { - settings_map.insert(settings.filename.clone(), 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(); - if let Ok(data) = field.bytes().await { - files_data.push((file_name, data)); + 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 { - // Клонируем настройки перед использованием - let settings = settings_map.get(&file_name).cloned(); - + info!(file_name = %file_name, "🔄 Начало обработки файла"); + + // Нормализация расширения let ext = std::path::Path::new(&file_name) .extension() .and_then(|e| e.to_str()) - .unwrap_or(""); - + .unwrap_or("") + .to_lowercase(); + + info!(file_name = %file_name, extension = %ext, "Определено расширение файла"); + + // Создание временного файла let temp_file = if ext.is_empty() { NamedTempFile::new() } else { @@ -82,59 +147,162 @@ async fn handle_print(mut multipart: Multipart) -> impl IntoResponse { 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; } - // Теперь settings это Option, а не ссылка + 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(s) = settings { + + 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).output() + + 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() => { - printed_files.push(file_name); + 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); - errors.push(format!("lp ошибка для {}: {}", file_name, 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() + )); } - _ => { - errors.push(format!("Не удалось запустить lp для {}", file_name)); + 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() { - (StatusCode::OK, format!("Напечатано {} файлов", printed_files.len())) + info!(files = ?printed_files, "🎉 Все файлы успешно напечатаны"); + ( + StatusCode::OK, + format!("Напечатано {} файлов", printed_files.len()), + ) } else if printed_files.is_empty() { - (StatusCode::BAD_REQUEST, format!("Ошибки: {:?}", errors)) + error!(errors = ?errors, "💥 Все файлы не удалось напечатать"); + ( + StatusCode::BAD_REQUEST, + format!("Ошибки: {:?}", errors), + ) } else { + warn!( + printed = ?printed_files, + errors = ?errors, + "⚠️ Частичная печать: некоторые файлы не напечатаны" + ); ( StatusCode::MULTI_STATUS, format!("Частично: {:?}, Ошибки: {:?}", printed_files, errors),