a.0.0.6
This commit is contained in:
1 parent
5d01dfb63a
commit
b0d63c9f0f
20 files changed
+1857
-1417
No files matched your search
@@ -1,6 +1,6 @@
|
||||
use crate::{models::*, net, storage};
|
||||
use crate::{models::*, net, storage, transfer};
|
||||
use axum::{
|
||||
body::{Body, Bytes},
|
||||
body::Body,
|
||||
extract::{Path, Request, State},
|
||||
http::{header, HeaderMap, HeaderName, HeaderValue, Method, StatusCode},
|
||||
middleware::Next,
|
||||
@@ -8,13 +8,7 @@ use axum::{
|
||||
Json,
|
||||
};
|
||||
use futures_util::StreamExt;
|
||||
use std::{
|
||||
io,
|
||||
net::IpAddr,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tokio_stream::wrappers::ReceiverStream;
|
||||
use std::{io, net::IpAddr, sync::Arc};
|
||||
use tokio_util::io::ReaderStream;
|
||||
|
||||
const INDEX_HTML: &str = include_str!("static/index.html");
|
||||
@@ -126,6 +120,9 @@ pub async fn ui_js() -> impl IntoResponse {
|
||||
}
|
||||
|
||||
pub async fn ui_get_state(State(state): State<Arc<AppState>>) -> Json<serde_json::Value> {
|
||||
if let Some(ip) = net::detect_lan_ip() {
|
||||
state.update_my_ip(ip);
|
||||
}
|
||||
let mut peers: Vec<PeerDevice> = state.peers.read().values().cloned().collect();
|
||||
peers.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
let texts: Vec<TextItem> = state.ram_texts.read().iter().cloned().collect();
|
||||
@@ -135,11 +132,14 @@ pub async fn ui_get_state(State(state): State<Arc<AppState>>) -> Json<serde_json
|
||||
|
||||
Json(serde_json::json!({
|
||||
"device_name": state.device_name,
|
||||
"my_ip": state.my_ip,
|
||||
"my_ip": state.current_ip(),
|
||||
"p2p_port": state.p2p_port,
|
||||
"my_fingerprint": state.my_fingerprint,
|
||||
"my_pin": short_id(&state.my_fingerprint),
|
||||
"media_dir": state.media_dir.display().to_string(),
|
||||
"ping_interval_sec": state.ping_interval_sec.load(std::sync::atomic::Ordering::Relaxed),
|
||||
"ping_min": MIN_PING_SEC,
|
||||
"ping_max": MAX_PING_SEC,
|
||||
"peers": peers,
|
||||
"texts": texts,
|
||||
"media": media,
|
||||
@@ -173,9 +173,13 @@ pub async fn ui_scan_range(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<UiScanRangeRequest>,
|
||||
) -> Result<StatusCode, AppError> {
|
||||
if let Some(ip) = net::detect_lan_ip() {
|
||||
state.update_my_ip(ip);
|
||||
}
|
||||
let port = req.port.filter(|p| *p != 0).unwrap_or(state.p2p_port);
|
||||
let my_ip = state.current_ip();
|
||||
let (label, ips) =
|
||||
parse_ip_range(&req.range, &state.my_ip).map_err(|e| AppError::new(StatusCode::BAD_REQUEST, e))?;
|
||||
parse_ip_range(&req.range, &my_ip).map_err(|e| AppError::new(StatusCode::BAD_REQUEST, e))?;
|
||||
let total = ips.len();
|
||||
|
||||
{
|
||||
@@ -231,6 +235,19 @@ pub async fn ui_trust_peer(
|
||||
};
|
||||
state.persist_trust();
|
||||
state.add_log(msg);
|
||||
// сообщаем собеседнику сразу: он увидит запрос на сверку (или отзыв) и должен подтвердить у себя
|
||||
let (st, id) = (state.clone(), req.peer_id.clone());
|
||||
tokio::spawn(async move { net::push_trust(&st, &id).await });
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
|
||||
pub async fn ui_set_settings(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<UiSettingsRequest>,
|
||||
) -> Result<StatusCode, AppError> {
|
||||
state
|
||||
.set_ping_interval(req.ping_interval_sec)
|
||||
.map_err(|e| AppError::new(StatusCode::BAD_REQUEST, e))?;
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
|
||||
@@ -240,78 +257,16 @@ pub async fn ui_clear_ram(State(state): State<Arc<AppState>>) -> StatusCode {
|
||||
StatusCode::OK
|
||||
}
|
||||
|
||||
// ───────────────────────── Отправка ─────────────────────────
|
||||
fn resolve_target(state: &AppState, id: &str) -> Result<(Vec<IpAddr>, u16, String), AppError> {
|
||||
let peers = state.peers.read();
|
||||
let p = peers
|
||||
.get(id)
|
||||
.ok_or_else(|| AppError::new(StatusCode::NOT_FOUND, "Получатель не найден"))?;
|
||||
if !p.verified {
|
||||
return Err(AppError::new(
|
||||
StatusCode::FORBIDDEN,
|
||||
format!("Сначала заверьте устройство '{}' (код сверки {})", p.name, p.pin_code),
|
||||
));
|
||||
}
|
||||
let ips: Vec<IpAddr> = p.ips.iter().filter_map(|s| s.parse().ok()).collect();
|
||||
if ips.is_empty() {
|
||||
return Err(AppError::new(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Адрес получателя неизвестен. Выполните опрос или сканирование.",
|
||||
));
|
||||
}
|
||||
Ok((ips, p.port, p.name.clone()))
|
||||
}
|
||||
|
||||
// ───────────────────────── Отправка (логика — в transfer.rs, общая с CLI) ─────────────────────────
|
||||
pub async fn ui_send_text(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<UiSendTextRequest>,
|
||||
) -> Result<StatusCode, AppError> {
|
||||
if req.text.trim().is_empty() {
|
||||
return Err(AppError::new(StatusCode::BAD_REQUEST, "Пустой текст"));
|
||||
}
|
||||
if req.text.len() > MAX_TEXT_BYTES {
|
||||
return Err(AppError::new(StatusCode::PAYLOAD_TOO_LARGE, "Текст больше 64 КБ"));
|
||||
}
|
||||
let (ips, port, peer_name) = resolve_target(&state, &req.peer_id)?;
|
||||
let client = state.client_for_peer(&req.peer_id, false)?;
|
||||
let body = serde_json::to_vec(&IncomingTextPayload { text: req.text })
|
||||
.map_err(|e| AppError::internal("json", e))?;
|
||||
|
||||
for ip in ips {
|
||||
let url = format!("https://{}:{}/p2p/text", ip, port);
|
||||
match client
|
||||
.post(&url)
|
||||
.header(header::CONTENT_TYPE, "application/json")
|
||||
.body(body.clone())
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(r) if r.status().is_success() => {
|
||||
state.add_log(format!("Текст доставлен в ОЗУ устройства '{}'", peer_name));
|
||||
return Ok(StatusCode::OK);
|
||||
}
|
||||
Ok(r) if r.status() == StatusCode::FORBIDDEN => {
|
||||
return Err(AppError::new(
|
||||
StatusCode::FORBIDDEN,
|
||||
"Получатель ещё не заверил ваше устройство (доверие должно быть взаимным)",
|
||||
))
|
||||
}
|
||||
Ok(r) => {
|
||||
return Err(AppError::new(
|
||||
StatusCode::BAD_GATEWAY,
|
||||
format!("Получатель отклонил запрос ({})", r.status()),
|
||||
))
|
||||
}
|
||||
Err(_) => continue, // пробуем следующий адрес
|
||||
}
|
||||
}
|
||||
Err(AppError::new(
|
||||
StatusCode::BAD_GATEWAY,
|
||||
"Не удалось установить защищённое соединение (узел недоступен или сертификат не совпал)",
|
||||
))
|
||||
transfer::send_text(&state, &req.peer_id, req.text).await?;
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
|
||||
/// Потоковая отправка: браузер → (канал с backpressure) → TLS → получатель. Память O(1).
|
||||
/// Потоковая отправка: браузер → канал с backpressure → TLS → получатель. Память O(1).
|
||||
pub async fn ui_send_media(
|
||||
State(state): State<Arc<AppState>>,
|
||||
headers: HeaderMap,
|
||||
@@ -320,107 +275,15 @@ pub async fn ui_send_media(
|
||||
let peer_id = hdr(&headers, "x-target-id")
|
||||
.ok_or_else(|| AppError::new(StatusCode::BAD_REQUEST, "Не указан получатель"))?
|
||||
.to_string();
|
||||
let display_name = sanitize_filename(&pct_decode(hdr(&headers, "x-filename").unwrap_or("file.bin")));
|
||||
let name = pct_decode(hdr(&headers, "x-filename").unwrap_or("file.bin"));
|
||||
let total: u64 = hdr(&headers, "content-length")
|
||||
.and_then(|v| v.parse().ok())
|
||||
.ok_or_else(|| AppError::new(StatusCode::LENGTH_REQUIRED, "Не указан размер файла"))?;
|
||||
if total == 0 {
|
||||
return Err(AppError::new(StatusCode::BAD_REQUEST, "Файл пустой"));
|
||||
}
|
||||
if total > MAX_FILE_SIZE {
|
||||
return Err(AppError::new(StatusCode::PAYLOAD_TOO_LARGE, "Файл больше 2 ГБ"));
|
||||
}
|
||||
|
||||
let (ips, port, peer_name) = resolve_target(&state, &peer_id)?;
|
||||
let client = state.client_for_peer(&peer_id, true)?;
|
||||
let url = format!("https://{}:{}/p2p/media", ips[0], port);
|
||||
|
||||
let tid = format!("tx_{}", random_hex(6));
|
||||
state.add_transfer(TransferProgress {
|
||||
id: tid.clone(),
|
||||
filename: display_name.clone(),
|
||||
peer_name: peer_name.clone(),
|
||||
direction: Direction::Out,
|
||||
transferred_bytes: 0,
|
||||
total_bytes: total,
|
||||
percent: 0,
|
||||
done: false,
|
||||
error: None,
|
||||
updated_at: now_sec(),
|
||||
});
|
||||
|
||||
let (tx, rx) = tokio::sync::mpsc::channel::<Result<Bytes, io::Error>>(4);
|
||||
let fwd_state = state.clone();
|
||||
let fwd_id = tid.clone();
|
||||
let fwd = tokio::spawn(async move {
|
||||
let mut stream = std::pin::pin!(body.into_data_stream());
|
||||
let mut sent: u64 = 0;
|
||||
let mut last = Instant::now();
|
||||
while let Some(item) = stream.next().await {
|
||||
let chunk = match item {
|
||||
Ok(c) => c,
|
||||
Err(_) => {
|
||||
let _ = tx.send(Err(io::Error::new(io::ErrorKind::Other, "client aborted"))).await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
sent += chunk.len() as u64;
|
||||
if sent > total {
|
||||
let _ = tx.send(Err(io::Error::new(io::ErrorKind::Other, "body too long"))).await;
|
||||
return;
|
||||
}
|
||||
// если получатель не читает 30 с — закрываем поток, запрос завершится ошибкой
|
||||
match tokio::time::timeout(Duration::from_secs(30), tx.send(Ok(chunk))).await {
|
||||
Ok(Ok(())) => {}
|
||||
_ => return,
|
||||
}
|
||||
if last.elapsed() >= Duration::from_millis(100) {
|
||||
fwd_state.update_transfer_bytes(&fwd_id, sent);
|
||||
last = Instant::now();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let result = client
|
||||
.post(&url)
|
||||
.header("x-filename", pct_encode(&display_name))
|
||||
.header("x-total-size", total.to_string())
|
||||
.header(header::CONTENT_TYPE, "application/octet-stream")
|
||||
.header(header::CONTENT_LENGTH, total.to_string())
|
||||
.body(reqwest::Body::wrap_stream(ReceiverStream::new(rx)))
|
||||
.send()
|
||||
.await;
|
||||
fwd.abort();
|
||||
|
||||
let fail = |msg: &str, status: StatusCode| {
|
||||
state.fail_transfer(&tid, msg.to_string());
|
||||
remove_transfer_later(&state, tid.clone(), 10);
|
||||
AppError::new(status, msg)
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(r) if r.status().is_success() => {
|
||||
state.finish_transfer(&tid);
|
||||
remove_transfer_later(&state, tid.clone(), 4);
|
||||
state.add_log(format!(
|
||||
"Файл '{}' ({} байт) доставлен на '{}'",
|
||||
display_name, total, peer_name
|
||||
));
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
Ok(r) if r.status() == StatusCode::FORBIDDEN => Err(fail(
|
||||
"Получатель ещё не заверил ваше устройство",
|
||||
StatusCode::FORBIDDEN,
|
||||
)),
|
||||
Ok(r) => Err(fail(
|
||||
&format!("Получатель отклонил передачу (код {})", r.status()),
|
||||
StatusCode::BAD_GATEWAY,
|
||||
)),
|
||||
Err(_) => Err(fail(
|
||||
"Ошибка сети/TLS при передаче (узел недоступен, сертификат не совпал или обрыв)",
|
||||
StatusCode::BAD_GATEWAY,
|
||||
)),
|
||||
}
|
||||
let stream = body
|
||||
.into_data_stream()
|
||||
.map(|r| r.map_err(|_| io::Error::new(io::ErrorKind::Other, "client aborted")));
|
||||
transfer::send_stream(&state, &peer_id, &name, total, stream).await?;
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
|
||||
// ───────────────────────── Раздача принятых файлов ─────────────────────────
|
||||
|
||||
Reference in new issue
Block a user