This commit is contained in:
netadmin committed 2026-10-23 14:51:33 +03:00
1 parent 5723bfc532
commit e88dc07696
5 files changed
+613 -104

No files matched your search

+228 -30
View File
@@ -1,6 +1,7 @@
use crate::models::{
sanitize_filename, AppState, IncomingTextPayload, MediaItem, PeerDevice, TextItem,
UiManualPeerRequest, UiSendTextRequest, UiTrustRequest, MEDIA_DIR,
now_sec, parse_ip_range, sanitize_filename, url_decode, AppState, IncomingTextPayload,
MediaItem, PeerDevice, ScanProgress, TextItem, TransferProgress, UiManualPeerRequest,
UiScanRangeRequest, UiSendTextRequest, UiTrustRequest, MEDIA_DIR,
};
use axum::{
body::Bytes,
@@ -9,7 +10,8 @@ use axum::{
response::{Html, IntoResponse},
Json,
};
use std::{path::PathBuf, sync::Arc};
use std::{net::SocketAddr, path::PathBuf, sync::Arc, time::Duration};
use tokio::sync::Semaphore;
const INDEX_HTML: &str = include_str!("static/index.html");
@@ -18,17 +20,14 @@ pub async fn ui_index() -> Html<&'static str> {
}
pub async fn ui_get_state(State(state): State<Arc<AppState>>) -> Json<serde_json::Value> {
let mut peers_list: Vec<PeerDevice> = state
.peers
.read()
.unwrap()
.values()
.cloned()
.collect();
let mut peers_list: Vec<PeerDevice> =
state.peers.read().unwrap().values().cloned().collect();
peers_list.sort_by(|a, b| a.name.cmp(&b.name));
let texts: Vec<TextItem> = state.ram_texts.read().unwrap().iter().cloned().collect();
let media: Vec<MediaItem> = state.disk_media.read().unwrap().iter().cloned().collect();
let transfers: Vec<TransferProgress> = state.transfers.read().unwrap().clone();
let scan = state.scan_status.read().unwrap().clone();
let logs: Vec<String> = state.logs.read().unwrap().iter().cloned().collect();
Json(serde_json::json!({
@@ -40,6 +39,8 @@ pub async fn ui_get_state(State(state): State<Arc<AppState>>) -> Json<serde_json
"peers": peers_list,
"texts": texts,
"media": media,
"transfers": transfers,
"scan": scan,
"logs": logs
}))
}
@@ -84,6 +85,114 @@ pub async fn ui_add_manual_peer(
Ok(StatusCode::OK)
}
/// Асинхронное сканирование всего или части диапазона IP-адресов
pub async fn ui_scan_range(
State(state): State<Arc<AppState>>,
Json(req): Json<UiScanRangeRequest>,
) -> Result<StatusCode, (StatusCode, String)> {
{
let scan = state.scan_status.read().unwrap();
if scan.active {
return Err((StatusCode::CONFLICT, "Сканирование уже выполняется".into()));
}
}
let port = req.port.unwrap_or(state.p2p_port);
let (range_label, ips) =
parse_ip_range(&req.range, &state.my_ip).map_err(|e| (StatusCode::BAD_REQUEST, e))?;
let total = ips.len();
{
let mut scan = state.scan_status.write().unwrap();
*scan = ScanProgress {
active: true,
range: range_label.clone(),
scanned: 0,
total,
found: 0,
};
}
state.add_log(format!(
"Запущено сканирование диапазона {} ({} адресов, порт {})",
range_label, total, port
));
let state_bg = state.clone();
tokio::spawn(async move {
let semaphore = Arc::new(Semaphore::new(48));
let mut handles = Vec::with_capacity(total);
for ip in ips {
let sem = semaphore.clone();
let st = state_bg.clone();
handles.push(tokio::spawn(async move {
let _permit = sem.acquire().await.unwrap();
// Быстрая проверка открытости TCP-порта (350 мс) перед HTTPS-рукопожатием
let sock_addr: Option<SocketAddr> = format!("{}:{}", ip, port).parse().ok();
let mut found_node = false;
if let Some(addr) = sock_addr {
if tokio::time::timeout(
Duration::from_millis(350),
tokio::net::TcpStream::connect(addr),
)
.await
.map(|r| r.is_ok())
.unwrap_or(false)
{
let url = format!("https://{}:{}/p2p/info", ip, port);
if let Ok(resp) = st
.probe_client
.get(&url)
.header("X-Node-Fp", &st.my_fingerprint)
.header("X-Node-Name", &st.device_name)
.header("X-Node-Ip", &st.my_ip)
.header("X-Node-Port", st.p2p_port.to_string())
.send()
.await
{
if let Ok(info) = resp.json::<serde_json::Value>().await {
let name = info["name"].as_str().unwrap_or("Node").to_string();
let fp = info["fingerprint"].as_str().unwrap_or("").to_string();
let r_port = info["port"].as_u64().unwrap_or(port as u64) as u16;
if fp.len() >= 8 {
st.upsert_peer(name, ip, r_port, fp);
found_node = true;
}
}
}
}
}
let mut scan = st.scan_status.write().unwrap();
scan.scanned += 1;
if found_node {
scan.found += 1;
}
}));
}
for h in handles {
let _ = h.await;
}
let found_total = {
let mut scan = state_bg.scan_status.write().unwrap();
scan.active = false;
scan.found
};
state_bg.add_log(format!(
"Сканирование {} завершено. Найдено устройств: {}",
range_label, found_total
));
});
Ok(StatusCode::OK)
}
pub async fn ui_ping_peers(State(state): State<Arc<AppState>>) -> StatusCode {
let state_clone = state.clone();
tokio::spawn(async move {
@@ -131,7 +240,10 @@ pub async fn ui_send_text(
if !peer.verified {
return Err((
StatusCode::FORBIDDEN,
format!("Сначала заверьте сертификат устройства '{}' [PIN: {}]!", peer.name, peer.pin_code),
format!(
"Сначала заверьте сертификат устройства '{}' [PIN: {}]!",
peer.name, peer.pin_code
),
));
}
let ip = peer
@@ -165,6 +277,7 @@ pub async fn ui_send_text(
}
}
/// Почанковая отправка медиафайла с реальным отслеживанием прогресса по сети
pub async fn ui_send_media(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
@@ -175,15 +288,19 @@ pub async fn ui_send_media(
.and_then(|v| v.to_str().ok())
.ok_or((StatusCode::BAD_REQUEST, "Не указан ID получателя".to_string()))?;
let filename = headers
let raw_filename = headers
.get("X-Filename")
.and_then(|v| v.to_str().ok())
.unwrap_or("file.bin");
.unwrap_or("file.bin")
.to_string();
let display_name = sanitize_filename(&url_decode(&raw_filename));
let mime = headers
.get(header::CONTENT_TYPE)
.and_then(|v| v.to_str().ok())
.unwrap_or("application/octet-stream");
.unwrap_or("application/octet-stream")
.to_string();
let (target_ip, port, peer_name) = {
let peers = state.peers.read().unwrap();
@@ -193,7 +310,10 @@ pub async fn ui_send_media(
if !peer.verified {
return Err((
StatusCode::FORBIDDEN,
format!("Сначала заверьте сертификат устройства '{}' [PIN: {}]!", peer.name, peer.pin_code),
format!(
"Сначала заверьте сертификат устройства '{}' [PIN: {}]!",
peer.name, peer.pin_code
),
));
}
let ip = peer
@@ -204,25 +324,103 @@ pub async fn ui_send_media(
(ip, peer.port, peer.name.clone())
};
let total_size = body.len();
let transfer_id = format!("tx_{}_{}", now_sec(), total_size);
let url = format!("https://{}:{}/p2p/media", target_ip, port);
let resp = state
.http_client
.post(&url)
.header("X-Sender", &state.device_name)
.header("X-Filename", filename)
.header(header::CONTENT_TYPE, mime)
.body(body)
.send()
.await
.map_err(|e| (StatusCode::BAD_GATEWAY, format!("Ошибка TLS/Сети: {}", e)))?;
if resp.status().is_success() {
state.add_log(format!("Медиафайл отправлен на диск устройства '{}'", peer_name));
Ok(StatusCode::OK)
// Адаптивный размер чанка для плавного обновления прогресс-бара на любых файлах
let chunk_size = if total_size < 1024 * 1024 {
32 * 1024 // 32 КБ для файлов до 1 МБ
} else if total_size < 15 * 1024 * 1024 {
128 * 1024 // 128 КБ для файлов до 15 МБ
} else {
Err((resp.status(), "Удаленный узел отклонил медиафайл".to_string()))
512 * 1024 // 512 КБ для крупных файлов
};
state.upsert_transfer(TransferProgress {
id: transfer_id.clone(),
filename: display_name.clone(),
peer_name: peer_name.clone(),
direction: "out".to_string(),
transferred_bytes: 0,
total_bytes: total_size,
percent: 0,
done: false,
error: None,
updated_at: now_sec(),
});
let mut offset = 0usize;
loop {
let end = (offset + chunk_size).min(total_size);
let chunk = body.slice(offset..end);
let send_res = state
.http_client
.post(&url)
.header("X-Sender", &state.device_name)
.header("X-Filename", &raw_filename)
.header("X-Transfer-Id", &transfer_id)
.header("X-Offset", offset.to_string())
.header("X-Total-Size", total_size.to_string())
.header(header::CONTENT_TYPE, &mime)
.body(chunk)
.send()
.await;
match send_res {
Ok(resp) if resp.status().is_success() => {
offset = end;
let is_done = offset >= total_size;
let percent = if total_size == 0 {
100
} else {
((offset as f64 / total_size as f64) * 100.0).min(100.0) as u8
};
state.upsert_transfer(TransferProgress {
id: transfer_id.clone(),
filename: display_name.clone(),
peer_name: peer_name.clone(),
direction: "out".to_string(),
transferred_bytes: offset,
total_bytes: total_size,
percent,
done: is_done,
error: None,
updated_at: now_sec(),
});
if is_done {
break;
}
}
Ok(resp) => {
let err_msg = format!("Узел отклонил чанк (код {})", resp.status());
state.remove_transfer(&transfer_id);
return Err((resp.status(), err_msg));
}
Err(e) => {
let err_msg = format!("Ошибка сети при передаче файла: {}", e);
state.remove_transfer(&transfer_id);
return Err((StatusCode::BAD_GATEWAY, err_msg));
}
}
}
state.add_log(format!(
"Медиафайл '{}' ({} байт) успешно доставлен на '{}'",
display_name, total_size, peer_name
));
let st_cleanup = state.clone();
let tid_cleanup = transfer_id.clone();
tokio::spawn(async move {
tokio::time::sleep(Duration::from_secs(4)).await;
st_cleanup.remove_transfer(&tid_cleanup);
});
Ok(StatusCode::OK)
}
pub async fn ui_serve_saved_file(Path(filename): Path<String>) -> impl IntoResponse {