- Исправил печать jpg; Добавил сокращения: сборка cargo br

This commit is contained in:
2026-09-07 13:25:13 +03:00
parent ee199f5486
commit 07b5332599
4 changed files with 233 additions and 35 deletions

5
.cargo/config.toml Normal file
View File

@ -0,0 +1,5 @@
[alias]
b = "build --release"
br = "build --release"
r = "run --release"

28
Cargo.lock generated
View File

@ -72,6 +72,7 @@ dependencies = [
"tempfile", "tempfile",
"tokio", "tokio",
"tower-http", "tower-http",
"tracing",
"tracing-subscriber", "tracing-subscriber",
] ]
@ -471,7 +472,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 3.0.4",
] ]
[[package]] [[package]]
@ -557,6 +558,17 @@ version = "0.9.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" 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]] [[package]]
name = "syn" name = "syn"
version = "3.0.4" version = "3.0.4"
@ -621,7 +633,7 @@ checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 3.0.4",
] ]
[[package]] [[package]]
@ -676,9 +688,21 @@ checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
dependencies = [ dependencies = [
"log", "log",
"pin-project-lite", "pin-project-lite",
"tracing-attributes",
"tracing-core", "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]] [[package]]
name = "tracing-core" name = "tracing-core"
version = "0.1.36" version = "0.1.36"

View File

@ -12,3 +12,4 @@ tower-http = { version = "0.5", features = ["cors"] }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
bytes = "1" bytes = "1"
tracing = "0.1"

View File

@ -1,17 +1,19 @@
use axum::{ use axum::{
extract::Multipart, extract::{DefaultBodyLimit, Multipart},
http::StatusCode, http::StatusCode,
response::IntoResponse, response::IntoResponse,
routing::post, routing::post,
Router, Router,
}; };
use serde::Deserialize; use serde::Deserialize;
use std::process::Command; use std::process::Command;
use tempfile::NamedTempFile; use tempfile::NamedTempFile;
use tokio::fs; use tokio::fs;
use tower_http::cors::CorsLayer; use tower_http::cors::CorsLayer;
use tracing::{error, info, warn};
#[derive(Deserialize, Clone)] // Добавили Clone #[derive(Deserialize, Clone, Debug)]
struct PrintSettings { struct PrintSettings {
filename: String, filename: String,
pages: String, pages: String,
@ -23,56 +25,119 @@ struct PrintSettings {
#[tokio::main] #[tokio::main]
async fn 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 cors = CorsLayer::permissive();
let app = Router::new() let app = Router::new()
.route("/print", post(handle_print)) .route("/print", post(handle_print))
.layer(DefaultBodyLimit::max(50 * 1024 * 1024)) // 50 MB
.layer(cors); .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") let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
.await .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 { async fn handle_print(mut multipart: Multipart) -> impl IntoResponse {
info!("📥 Получен запрос на печать");
let mut printed_files: Vec<String> = Vec::new(); let mut printed_files: Vec<String> = Vec::new();
let mut errors: Vec<String> = Vec::new(); let mut errors: Vec<String> = Vec::new();
let mut settings_map: std::collections::HashMap<String, PrintSettings> = std::collections::HashMap::new(); let mut settings_map: std::collections::HashMap<String, PrintSettings> =
std::collections::HashMap::new();
let mut files_data: Vec<(String, bytes::Bytes)> = Vec::new(); let mut files_data: Vec<(String, bytes::Bytes)> = Vec::new();
// ── Парсинг multipart ──────────────────────────────────────────────
while let Some(field) = multipart.next_field().await.unwrap_or(None) { while let Some(field) = multipart.next_field().await.unwrap_or(None) {
let field_name = field.name().unwrap_or("").to_string(); let field_name = field.name().unwrap_or("").to_string();
if field_name == "settings" { if field_name == "settings" {
if let Ok(text) = field.text().await { match field.text().await {
if let Ok(settings) = serde_json::from_str::<PrintSettings>(&text) { Ok(text) => {
settings_map.insert(settings.filename.clone(), settings); info!(raw_settings = %text, "Получены настройки печати");
match serde_json::from_str::<PrintSettings>(&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" { } else if field_name == "files" {
let file_name = field.file_name().unwrap_or("unknown").to_string(); let file_name = field.file_name().unwrap_or("unknown").to_string();
if let Ok(data) = field.bytes().await { match field.bytes().await {
files_data.push((file_name, data)); 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 { for (file_name, data) in files_data {
// Клонируем настройки перед использованием info!(file_name = %file_name, "🔄 Начало обработки файла");
let settings = settings_map.get(&file_name).cloned();
// Нормализация расширения
let ext = std::path::Path::new(&file_name) let ext = std::path::Path::new(&file_name)
.extension() .extension()
.and_then(|e| e.to_str()) .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() { let temp_file = if ext.is_empty() {
NamedTempFile::new() NamedTempFile::new()
} else { } else {
@ -82,59 +147,162 @@ async fn handle_print(mut multipart: Multipart) -> impl IntoResponse {
let temp_file = match temp_file { let temp_file = match temp_file {
Ok(f) => f, Ok(f) => f,
Err(e) => { Err(e) => {
error!(file_name = %file_name, error = %e, "Не удалось создать temp файл");
errors.push(format!("Не удалось создать temp файл для {}: {}", file_name, e)); errors.push(format!("Не удалось создать temp файл для {}: {}", file_name, e));
continue; continue;
} }
}; };
let path = temp_file.path().to_path_buf(); 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 { 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)); errors.push(format!("Ошибка записи {}: {}", file_name, e));
continue; continue;
} }
// Теперь settings это Option<PrintSettings>, а не ссылка 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 print_result = tokio::task::spawn_blocking(move || {
let mut cmd = Command::new("lp"); 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()); cmd.arg("-n").arg(s.copies.to_string());
if s.pages != "all" && !s.pages.is_empty() { if s.pages != "all" && !s.pages.is_empty() {
cmd.arg("-P").arg(&s.pages); cmd.arg("-P").arg(&s.pages);
} }
if s.color_mode == "bw" { if s.color_mode == "bw" {
cmd.arg("-o").arg("ColorModel=Gray"); cmd.arg("-o").arg("ColorModel=Gray");
} }
cmd.arg("-o").arg(format!("PageSize={}", s.format)); cmd.arg("-o").arg(format!("PageSize={}", s.format));
} }
cmd.arg(&path).output() cmd.arg(&path);
// Логируем итоговую команду
let args: Vec<String> = 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; .await;
// Обработка результата — используем file_name_for_log вместо file_name
match print_result { match print_result {
Ok(Ok(output)) if output.status.success() => { 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)) => { Ok(Ok(output)) => {
let stderr = String::from_utf8_lossy(&output.stderr); 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()
));
} }
_ => { Ok(Err(e)) => {
errors.push(format!("Не удалось запустить lp для {}", file_name)); 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() { 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() { } else if printed_files.is_empty() {
(StatusCode::BAD_REQUEST, format!("Ошибки: {:?}", errors)) error!(errors = ?errors, "💥 Все файлы не удалось напечатать");
(
StatusCode::BAD_REQUEST,
format!("Ошибки: {:?}", errors),
)
} else { } else {
warn!(
printed = ?printed_files,
errors = ?errors,
"⚠️ Частичная печать: некоторые файлы не напечатаны"
);
( (
StatusCode::MULTI_STATUS, StatusCode::MULTI_STATUS,
format!("Частично: {:?}, Ошибки: {:?}", printed_files, errors), format!("Частично: {:?}, Ошибки: {:?}", printed_files, errors),