- Исправил печать jpg; Добавил сокращения: сборка cargo br
This commit is contained in:
5
.cargo/config.toml
Normal file
5
.cargo/config.toml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
|
||||||
|
[alias]
|
||||||
|
b = "build --release"
|
||||||
|
br = "build --release"
|
||||||
|
r = "run --release"
|
||||||
28
Cargo.lock
generated
28
Cargo.lock
generated
@ -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"
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
212
src/main.rs
212
src/main.rs
@ -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) => {
|
||||||
|
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);
|
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 {
|
||||||
|
Ok(data) => {
|
||||||
|
info!(
|
||||||
|
file_name = %file_name,
|
||||||
|
size_bytes = data.len(),
|
||||||
|
"📎 Получен файл"
|
||||||
|
);
|
||||||
files_data.push((file_name, data));
|
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,23 +147,55 @@ 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() {
|
||||||
@ -112,29 +209,100 @@ async fn handle_print(mut multipart: Multipart) -> impl IntoResponse {
|
|||||||
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),
|
||||||
|
|||||||
Reference in New Issue
Block a user