2026-09-19 15:10:01 +03:00
|
|
|
|
use super::models::*;
|
|
|
|
|
|
use axum::{extract::{Multipart, State}, http::StatusCode, Json};
|
|
|
|
|
|
use std::{path::Path, sync::atomic::Ordering};
|
|
|
|
|
|
use tempfile::NamedTempFile;
|
|
|
|
|
|
use tokio::fs;
|
|
|
|
|
|
use tracing::info;
|
|
|
|
|
|
|
|
|
|
|
|
pub async fn create_print(
|
|
|
|
|
|
State(store): State<JobStore>,
|
|
|
|
|
|
mut multipart: Multipart,
|
|
|
|
|
|
) -> Result<Json<JobView>, (StatusCode, String)> {
|
|
|
|
|
|
info!("📥 Получен запрос на печать");
|
2026-09-19 23:10:41 +03:00
|
|
|
|
|
2026-09-19 15:10:01 +03:00
|
|
|
|
let mut settings_list: Vec<Option<PrintSettings>> = Vec::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" {
|
|
|
|
|
|
let parsed = match field.text().await {
|
|
|
|
|
|
Ok(text) => serde_json::from_str::<PrintSettings>(&text).ok(),
|
|
|
|
|
|
Err(_) => None,
|
|
|
|
|
|
};
|
|
|
|
|
|
settings_list.push(parsed);
|
|
|
|
|
|
} else if field_name == "files" {
|
|
|
|
|
|
let file_name = field.file_name().unwrap_or("unknown").to_string();
|
2026-09-19 23:10:41 +03:00
|
|
|
|
if let Ok(data) = field.bytes().await {
|
|
|
|
|
|
files_data.push((file_name, data));
|
|
|
|
|
|
}
|
2026-09-19 15:10:01 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-19 23:10:41 +03:00
|
|
|
|
if files_data.is_empty() {
|
|
|
|
|
|
return Err((StatusCode::BAD_REQUEST, "Нет файлов".into()));
|
|
|
|
|
|
}
|
2026-09-19 15:10:01 +03:00
|
|
|
|
|
|
|
|
|
|
let mut job_files: Vec<JobFile> = Vec::new();
|
|
|
|
|
|
for (i, (file_name, data)) in files_data.into_iter().enumerate() {
|
|
|
|
|
|
let ext = Path::new(&file_name).extension().and_then(|e| e.to_str()).unwrap_or("").to_lowercase();
|
|
|
|
|
|
let temp = if ext.is_empty() { NamedTempFile::new() } else { NamedTempFile::with_suffix(&format!(".{}", ext)) };
|
2026-09-19 23:10:41 +03:00
|
|
|
|
|
2026-09-19 15:10:01 +03:00
|
|
|
|
if let Ok(temp) = temp {
|
|
|
|
|
|
let path = temp.into_temp_path();
|
|
|
|
|
|
if fs::write(&*path, &data).await.is_ok() {
|
|
|
|
|
|
let settings = settings_list.get(i).cloned().flatten();
|
|
|
|
|
|
job_files.push(JobFile {
|
2026-09-19 23:10:41 +03:00
|
|
|
|
name: file_name,
|
|
|
|
|
|
path,
|
|
|
|
|
|
settings,
|
|
|
|
|
|
status: FileStatus::Queued,
|
|
|
|
|
|
error: None,
|
2026-09-19 15:10:01 +03:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-19 23:10:41 +03:00
|
|
|
|
if job_files.is_empty() {
|
|
|
|
|
|
return Err((StatusCode::BAD_REQUEST, "Не удалось сохранить файлы".into()));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ВСЕГДА начинаем с WaitingStart.
|
|
|
|
|
|
// 1. Если первый файл односторонний - он просто поедет печататься.
|
|
|
|
|
|
// 2. Если первый файл двусторонний - лоток и так пуст, очистка не нужна.
|
|
|
|
|
|
// Очистка (AwaitingClearOutput) будет запрошена ТОЛЬКО при переходе
|
|
|
|
|
|
// от предыдущего файла к следующему двустороннему в advance.rs
|
|
|
|
|
|
let initial_phase = JobPhase::WaitingStart;
|
2026-09-19 15:10:01 +03:00
|
|
|
|
|
|
|
|
|
|
let job_id = format!("job-{}", JOB_COUNTER.fetch_add(1, Ordering::SeqCst));
|
|
|
|
|
|
let job = Job {
|
2026-09-19 23:10:41 +03:00
|
|
|
|
id: job_id.clone(),
|
|
|
|
|
|
files: job_files,
|
|
|
|
|
|
current: Some(0),
|
|
|
|
|
|
phase: initial_phase,
|
|
|
|
|
|
cups_id: None,
|
|
|
|
|
|
duplex_first_pass_done: false,
|
2026-09-19 15:10:01 +03:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let view = job.view();
|
|
|
|
|
|
store.lock().unwrap().insert(job_id.clone(), job);
|
|
|
|
|
|
Ok(Json(view))
|
|
|
|
|
|
}
|