Skip to main content

lanflare_cli/
commands.rs

1//! CLI command implementations.
2
3use anyhow::Context;
4use lanflare_core::config::AppConfig;
5use lanflare_core::crypto::cert::TlsIdentity;
6use lanflare_core::discovery::interface::{local_interfaces, InterfaceFilter};
7use lanflare_core::discovery::mdns::{start_mdns_engine, MdnsConfig, MdnsDiscoveryEvent, MdnsNodeInfo};
8use lanflare_core::discovery::multicast::{
9    start_multicast_engine, MulticastConfig, MulticastDiscoveryEvent, MulticastNodeInfo,
10};
11use lanflare_core::discovery::registry::{DiscoveredPeer, PeerRegistry};
12use lanflare_proto::discovery::{DeviceType, ProtocolType};
13use std::path::PathBuf;
14use std::time::Duration;
15use tokio::sync::{mpsc, oneshot};
16
17use crate::tui::render_peer_table;
18
19/// Handles the `lanflare status` command.
20///
21/// Prints local node configuration, identity (certificate fingerprint), and available network interfaces.
22pub async fn status(config: &AppConfig, identity: &TlsIdentity) -> anyhow::Result<()> {
23    println!("LanFlare Node Status:");
24    println!("  Alias:       {}", config.alias);
25    println!("  Port:        {}", config.port);
26    println!("  Download Dir: {}", config.download_dir.display());
27    println!("  Auto Accept: {}", config.auto_accept);
28    println!(
29        "  PIN Protect: {}",
30        if config.pin.is_some() { "Enabled" } else { "Disabled" }
31    );
32    println!("  Fingerprint: {}", identity.fingerprint);
33    println!();
34
35    println!("Local Network Interfaces:");
36    let filter = InterfaceFilter::default();
37    match local_interfaces(&filter) {
38        Ok(interfaces) => {
39            if interfaces.v4.is_empty() && interfaces.v6.is_empty() {
40                println!("  (No active non-loopback network interfaces found)");
41            } else {
42                for iface in &interfaces.v4 {
43                    println!("  - IPv4: {} ({})", iface.address, iface.name);
44                }
45                for iface in &interfaces.v6 {
46                    println!("  - IPv6: index {} ({})", iface.index, iface.name);
47                }
48            }
49        }
50        Err(err) => {
51            println!("  Failed to enumerate network interfaces: {err}");
52        }
53    }
54
55    Ok(())
56}
57
58/// Options for the `lanflare scan` command.
59#[derive(Clone, Debug)]
60pub struct ScanOptions {
61    /// Scan timeout in seconds.
62    pub duration_secs: u64,
63    /// Whether to enable UDP Multicast discovery.
64    pub multicast: bool,
65    /// Whether to enable mDNS discovery.
66    pub mdns: bool,
67    /// Force active /24 subnet IP scan.
68    pub subnet: bool,
69    /// Fall back to /24 subnet scan if multicast/mDNS yield 0 peers.
70    pub fallback_subnet: bool,
71}
72
73impl Default for ScanOptions {
74    fn default() -> Self {
75        Self {
76            duration_secs: 3,
77            multicast: true,
78            mdns: true,
79            subnet: false,
80            fallback_subnet: true,
81        }
82    }
83}
84
85/// Handles the `lanflare scan` command.
86///
87/// Spins up dual discovery engines (Multicast UDP and mDNS), listens for announcements,
88/// aggregates them in `PeerRegistry`, and displays a formatted table of active peers.
89pub async fn scan(
90    config: &AppConfig,
91    identity: &TlsIdentity,
92    options: &ScanOptions,
93) -> anyhow::Result<Vec<DiscoveredPeer>> {
94    println!(
95        "Scanning local network for LanFlare / LocalSend peers (timeout: {}s)...",
96        options.duration_secs
97    );
98
99    let registry = PeerRegistry::new();
100
101    // 1. Setup UDP multicast discovery if enabled
102    let (mc_stop_tx, mc_stop_rx) = oneshot::channel();
103    let (mc_event_tx, mut mc_event_rx) = mpsc::channel::<MulticastDiscoveryEvent>(100);
104
105    let mc_handle = if options.multicast {
106        let mc_config = MulticastConfig {
107            port: config.port,
108            node_info: MulticastNodeInfo {
109                alias: config.alias.clone(),
110                version: "2.0".to_string(),
111                device_model: Some("LanFlare Node".to_string()),
112                device_type: Some(DeviceType::Desktop),
113                fingerprint: identity.fingerprint.clone(),
114                port: config.port,
115                protocol: ProtocolType::Https,
116                download: false,
117            },
118            ..MulticastConfig::default()
119        };
120
121        match start_multicast_engine(mc_config, mc_event_tx, mc_stop_rx).await {
122            Ok(handle) => {
123                // Send an announcement burst so peers respond/discover us
124                let h_clone = handle.clone();
125                tokio::spawn(async move {
126                    h_clone.announce_burst().await;
127                });
128                Some(handle)
129            }
130            Err(e) => {
131                tracing::warn!("Failed to start multicast engine: {e}");
132                None
133            }
134        }
135    } else {
136        None
137    };
138
139    // 2. Setup mDNS discovery if enabled
140    let (mdns_stop_tx, mdns_stop_rx) = oneshot::channel();
141    let (mdns_event_tx, mut mdns_event_rx) = mpsc::channel::<MdnsDiscoveryEvent>(100);
142
143    let mdns_handle = if options.mdns {
144        let mdns_config = MdnsConfig {
145            node_info: MdnsNodeInfo {
146                alias: config.alias.clone(),
147                instance_name: String::new(),
148                version: "2.0".to_string(),
149                device_model: Some("LanFlare Node".to_string()),
150                device_type: Some(DeviceType::Desktop),
151                fingerprint: identity.fingerprint.clone(),
152                port: config.port,
153                protocol: ProtocolType::Https,
154                download: false,
155            },
156            enable_responder: true,
157            enable_browser: true,
158            ..MdnsConfig::default()
159        };
160
161        match start_mdns_engine(mdns_config, mdns_event_tx, mdns_stop_rx).await {
162            Ok(handle) => Some(handle),
163            Err(e) => {
164                tracing::warn!("Failed to start mDNS engine: {e}");
165                None
166            }
167        }
168    } else {
169        None
170    };
171
172    // 3. Collector loop for discovery events
173    let registry_clone = registry.clone();
174    let collector_task = tokio::spawn(async move {
175        loop {
176            tokio::select! {
177                Some(event) = mc_event_rx.recv() => {
178                    if let MulticastDiscoveryEvent::PeerDiscovered { remote_ip, scope_id, message } = event {
179                        registry_clone.handle_multicast(remote_ip, scope_id, &message).await;
180                    }
181                }
182                Some(event) = mdns_event_rx.recv() => {
183                    match event {
184                        MdnsDiscoveryEvent::PeerDiscovered(peer) => {
185                            registry_clone.handle_mdns(&peer).await;
186                        }
187                        MdnsDiscoveryEvent::PeerRemoved { fullname, .. } => {
188                            // Extract instance name or remove
189                            tracing::debug!("mDNS service removed: {fullname}");
190                        }
191                    }
192                }
193                else => break,
194            }
195        }
196    });
197
198    // 4. Escalation worker: if subnet scan requested or fallback enabled,
199    // escalate after a brief multicast grace period (500ms, matching official LocalSend)
200    let registry_subnet = registry.clone();
201    let identity_subnet = identity.clone();
202    let port_subnet = config.port;
203    let should_run_subnet = options.subnet || options.fallback_subnet;
204    let immediate_subnet = options.subnet;
205
206    let subnet_task = if should_run_subnet {
207        Some(tokio::spawn(async move {
208            if !immediate_subnet {
209                // Multicast grace delay before escalating to subnet scan
210                tokio::time::sleep(Duration::from_millis(500)).await;
211                // If peers were already found via multicast/mDNS, skip subnet sweep
212                if !registry_subnet.get_all().await.is_empty() {
213                    return;
214                }
215            }
216
217            let filter = InterfaceFilter::default();
218            if let Ok(interfaces) = local_interfaces(&filter) {
219                let valid_ifaces: Vec<_> = interfaces
220                    .v4
221                    .into_iter()
222                    .filter(|iface| is_sweepable_interface(&iface.name, &iface.address))
223                    .collect();
224
225                if !valid_ifaces.is_empty() {
226                    let descs: Vec<String> = valid_ifaces
227                        .iter()
228                        .map(|i| format!("{}/24 via {}", i.address, i.name))
229                        .collect();
230                    println!("Probing subnets in parallel: {}...", descs.join(", "));
231
232                    let scan_tasks = valid_ifaces.into_iter().map(|iface| {
233                        let reg = registry_subnet.clone();
234                        let ident = identity_subnet.clone();
235                        async move {
236                            lanflare_core::discovery::scanner::scan_subnet(
237                                iface.address,
238                                port_subnet,
239                                &ident,
240                                &reg,
241                            )
242                            .await
243                        }
244                    });
245
246                    futures_util::future::join_all(scan_tasks).await;
247                }
248            }
249        }))
250    } else {
251        None
252    };
253
254    // Wait for discovery to complete:
255    // - Exits early as soon as peers are discovered (plus brief 250ms grace window for remaining packets)
256    // - If immediate subnet scan was requested (-s), exits as soon as the subnet scan completes
257    // - Otherwise waits for the configured scan duration
258    let start = std::time::Instant::now();
259    let scan_timeout = Duration::from_secs(options.duration_secs);
260
261    loop {
262        tokio::time::sleep(Duration::from_millis(100)).await;
263
264        if !registry.get_all().await.is_empty() {
265            tokio::time::sleep(Duration::from_millis(250)).await;
266            break;
267        }
268
269        if let Some(ref st) = subnet_task {
270            if st.is_finished() && (immediate_subnet || start.elapsed() >= scan_timeout) {
271                break;
272            }
273        } else if start.elapsed() >= scan_timeout {
274            break;
275        }
276
277        if start.elapsed() >= scan_timeout.max(Duration::from_secs(5)) {
278            break;
279        }
280    }
281
282    if let Some(st) = subnet_task {
283        let _ = st.await;
284    }
285
286    // Shutdown discovery engines
287    let _ = mc_stop_tx.send(());
288    let _ = mdns_stop_tx.send(());
289
290    if let Some(h) = mc_handle {
291        h.stop().await;
292    }
293    if let Some(h) = mdns_handle {
294        h.stop().await;
295    }
296
297    collector_task.abort();
298
299    // 5. Retrieve active peers
300    let peers = registry.get_all().await;
301
302    print!("{}", render_peer_table(&peers));
303
304    Ok(peers)
305}
306
307/// Determines if an interface is an active LAN subnet suitable for a /24 sweep.
308///
309/// Filters out loopback, virtual containers (docker, veth, vboxnet), and point-to-point
310/// VPN / CGNAT ranges (Tailscale `100.64.0.0/10`, wireguard, tun/tap) which do not carry /24 LANs.
311fn is_sweepable_interface(name: &str, ip: &std::net::Ipv4Addr) -> bool {
312    if ip.is_loopback() {
313        return false;
314    }
315
316    // Skip CGNAT / Carrier Grade NAT (100.64.0.0/10) used by Tailscale, VPNs, etc.
317    let octets = ip.octets();
318    if octets[0] == 100 && (octets[1] & 0xC0) == 64 {
319        return false;
320    }
321
322    let lower = name.to_ascii_lowercase();
323    if lower.starts_with("tailscale")
324        || lower.starts_with("docker")
325        || lower.starts_with("vboxnet")
326        || lower.starts_with("veth")
327        || lower.starts_with("br-")
328    {
329        return false;
330    }
331
332    true
333}
334
335use std::io::Write;
336use std::net::IpAddr;
337use lanflare_core::client::transfer::{
338    OutgoingFile, TransferOptions, TransferOrchestrator, TransferProgress,
339};
340use lanflare_core::client::{LsClient, LsClientConfig};
341use lanflare_core::server::{start_server, PrepareUploadDecision, ServerTransferEvent};
342use lanflare_proto::discovery::RegisterDtoV2;
343
344use crate::tui::{shorten_fingerprint, TransferProgressTracker};
345
346/// Prompt user in terminal synchronously or read from reader.
347pub fn prompt_user_input(prompt: &str) -> anyhow::Result<String> {
348    print!("{prompt}");
349    std::io::stdout().flush().context("Failed to flush stdout")?;
350    let mut input = String::new();
351    std::io::stdin()
352        .read_line(&mut input)
353        .context("Failed to read user input from stdin")?;
354    Ok(input.trim().to_string())
355}
356
357/// Resolves target peer from query string (IP, alias, or fingerprint) against registry.
358pub fn match_target_peer<'a>(
359    query: &str,
360    peers: &'a [DiscoveredPeer],
361) -> Option<&'a DiscoveredPeer> {
362    let q = query.trim();
363    if q.is_empty() {
364        return None;
365    }
366
367    // 1. Exact canonical fingerprint match (or prefix)
368    let q_upper = q.to_ascii_uppercase();
369    if let Some(p) = peers.iter().find(|p| p.fingerprint == q_upper || p.fingerprint.starts_with(&q_upper)) {
370        return Some(p);
371    }
372
373    // 2. Exact IP match
374    if let Ok(ip) = q.parse::<IpAddr>()
375        && let Some(p) = peers.iter().find(|p| p.endpoints.iter().any(|ep| ep.ip == ip))
376    {
377        return Some(p);
378    }
379
380    // 3. Exact alias match (case-insensitive)
381    if let Some(p) = peers.iter().find(|p| p.alias.eq_ignore_ascii_case(q)) {
382        return Some(p);
383    }
384
385    // 4. Substring alias match
386    peers.iter().find(|p| p.alias.to_ascii_lowercase().contains(&q.to_ascii_lowercase()))
387}
388
389/// Discovers peers and presents an interactive selection if multiple are found.
390pub async fn discover_or_select_target(
391    config: &AppConfig,
392    identity: &TlsIdentity,
393    query: Option<&str>,
394) -> anyhow::Result<DiscoveredPeer> {
395    if let Some(q) = query {
396        // Fast-path: Check if user provided direct IP address (e.g. 192.168.1.50)
397        let trimmed = q.trim();
398        if let Ok(ip) = trimmed.parse::<IpAddr>() {
399            println!("Probing direct IP address {ip}...");
400            let probe_config = LsClientConfig {
401                timeout: Duration::from_secs(3),
402                connect_timeout: Duration::from_secs(2),
403                tls_identity: Some(identity.clone()),
404                target_fingerprint: None,
405            };
406            if let Ok(probe_client) = LsClient::new(probe_config) {
407                for proto in [ProtocolType::Https, ProtocolType::Http] {
408                    let proto_str = match proto {
409                        ProtocolType::Https => "https",
410                        ProtocolType::Http => "http",
411                    };
412                    let base_url = match ip {
413                        IpAddr::V4(v4) => format!("{proto_str}://{v4}:{}", config.port),
414                        IpAddr::V6(v6) => format!("{proto_str}://[{v6}]:{}", config.port),
415                    };
416                    if let Ok(info) = probe_client.get_info(&base_url).await {
417                        let peer_port = info.port.unwrap_or(config.port);
418                        let peer_proto = info.protocol.unwrap_or(proto);
419                        let ep = lanflare_core::discovery::registry::PeerEndpoint::new(
420                            ip,
421                            None,
422                            lanflare_core::discovery::DiscoverySource::ManualScan,
423                            0,
424                        );
425                        return Ok(DiscoveredPeer {
426                            fingerprint: info.fingerprint,
427                            alias: info.alias,
428                            version: info.version,
429                            device_model: info.device_model,
430                            device_type: info.device_type,
431                            port: peer_port,
432                            protocol: peer_proto,
433                            download: info.download,
434                            endpoints: vec![ep],
435                            discovery_sources: std::collections::HashSet::new(),
436                            first_seen_ms: 0,
437                            last_seen_ms: 0,
438                            metadata: std::collections::HashMap::new(),
439                        });
440                    }
441                }
442            }
443
444            // Fallback unprobed direct peer
445            let ep = lanflare_core::discovery::registry::PeerEndpoint::new(
446                ip,
447                None,
448                lanflare_core::discovery::DiscoverySource::ManualScan,
449                0,
450            );
451            return Ok(DiscoveredPeer {
452                fingerprint: String::new(),
453                alias: format!("Direct Peer ({ip})"),
454                version: "2.0".to_string(),
455                device_model: None,
456                device_type: None,
457                port: config.port,
458                protocol: ProtocolType::Https,
459                download: false,
460                endpoints: vec![ep],
461                discovery_sources: std::collections::HashSet::new(),
462                first_seen_ms: 0,
463                last_seen_ms: 0,
464                metadata: std::collections::HashMap::new(),
465            });
466        }
467    }
468
469    // Run network scan
470    println!("Scanning network to find target peer...");
471    let scan_opts = ScanOptions {
472        duration_secs: 2,
473        multicast: true,
474        mdns: true,
475        subnet: false,
476        fallback_subnet: true,
477    };
478    let peers = scan(config, identity, &scan_opts).await?;
479
480    if peers.is_empty() {
481        if let Some(q) = query {
482            // If query is an IP or host, fall back to direct attempt
483            anyhow::bail!("No peers found matching query '{q}'. Use explicit IP if known.");
484        } else {
485            anyhow::bail!("No peers discovered on local network.");
486        }
487    }
488
489    if let Some(q) = query {
490        if let Some(matched) = match_target_peer(q, &peers) {
491            return Ok(matched.clone());
492        }
493        anyhow::bail!("No discovered peer matched query '{q}'.");
494    }
495
496    // Interactive picker when query is None
497    if peers.len() == 1 {
498        let p = &peers[0];
499        println!("Single peer found: {} ({})", p.alias, p.primary_ip().map(|ip| ip.to_string()).unwrap_or_default());
500        return Ok(p.clone());
501    }
502
503    println!("\nSelect a peer to send files to:");
504    for (idx, p) in peers.iter().enumerate() {
505        let ip_str = p.primary_ip().map(|ip| ip.to_string()).unwrap_or_else(|| "-".to_string());
506        let fp_str = shorten_fingerprint(&p.fingerprint);
507        println!("  [{}] {} | IP: {} | Port: {} | FP: {}", idx + 1, p.alias, ip_str, p.port, fp_str);
508    }
509
510    loop {
511        let choice_str = prompt_user_input(&format!("Enter peer number (1-{}): ", peers.len()))?;
512        if let Ok(choice) = choice_str.parse::<usize>()
513            && (1..=peers.len()).contains(&choice)
514        {
515            return Ok(peers[choice - 1].clone());
516        }
517        println!("Invalid choice. Please enter a number between 1 and {}.", peers.len());
518    }
519}
520
521/// Options for the `lanflare send` command.
522#[derive(Clone, Debug)]
523pub struct SendOptions {
524    pub files: Vec<PathBuf>,
525    pub to: Option<String>,
526    pub pin: Option<String>,
527}
528
529/// Handles the `lanflare send` command.
530///
531/// Discovers or resolves target peer, calculates file sizes & SHA-256 checksums,
532/// establishes transfer session with `TransferOrchestrator`, and displays real-time
533/// multi-progress bars.
534pub async fn send(
535    config: &AppConfig,
536    identity: &TlsIdentity,
537    options: &SendOptions,
538) -> anyhow::Result<()> {
539    if options.files.is_empty() {
540        anyhow::bail!("No files specified to send");
541    }
542
543    // 1. Verify and prepare all files
544    let mut outgoing_files = Vec::with_capacity(options.files.len());
545    let mut total_bytes = 0u64;
546
547    for f in &options.files {
548        if !f.exists() {
549            anyhow::bail!("File not found: {}", f.display());
550        }
551        println!("Preparing file: {}...", f.display());
552        let outgoing = OutgoingFile::from_path(f)
553            .await
554            .with_context(|| format!("Failed to read file metadata for: {}", f.display()))?
555            .with_computed_sha256()
556            .await
557            .with_context(|| format!("Failed to compute SHA-256 for: {}", f.display()))?;
558
559        total_bytes += outgoing.size;
560        outgoing_files.push(outgoing);
561    }
562
563    println!(
564        "Prepared {} file(s) ({:.2} MB total).",
565        outgoing_files.len(),
566        total_bytes as f64 / (1024.0 * 1024.0)
567    );
568
569    // 2. Resolve destination peer
570    let target = discover_or_select_target(config, identity, options.to.as_deref()).await?;
571    let base_url = target
572        .primary_base_url()
573        .context("Target peer does not have a valid IP endpoint")?;
574
575    println!("Target resolved: {} at {}", target.alias, base_url);
576
577    // 3. Configure HTTP client
578    let target_fp = if target.fingerprint.is_empty() {
579        None
580    } else {
581        Some(target.fingerprint.clone())
582    };
583
584    let client_config = LsClientConfig {
585        timeout: Duration::from_secs(60),
586        connect_timeout: Duration::from_secs(5),
587        tls_identity: Some(identity.clone()),
588        target_fingerprint: target_fp,
589    };
590    let client = LsClient::new(client_config)
591        .context("Failed to initialize LocalSend client with TLS identity")?;
592
593    let sender_info = RegisterDtoV2 {
594        alias: config.alias.clone(),
595        version: "2.0".to_string(),
596        device_model: Some("LanFlare CLI".to_string()),
597        device_type: Some(DeviceType::Desktop),
598        fingerprint: identity.fingerprint.clone(),
599        port: config.port,
600        protocol: ProtocolType::Https,
601        download: false,
602    };
603
604    let orchestrator = TransferOrchestrator::new(client, sender_info);
605
606    // 4. Setup progress channel and progress tracker
607    let (progress_tx, mut progress_rx) = mpsc::channel::<TransferProgress>(100);
608    let mut tracker = TransferProgressTracker::new(true);
609
610    let transfer_opts = TransferOptions {
611        max_concurrency: 4,
612        pin: options.pin.clone().or_else(|| config.pin.clone()),
613        cancel_token: None,
614        progress_interval: Duration::from_millis(50),
615        ..Default::default()
616    };
617
618    // Spawn orchestrator task
619    let orchestrator_task = tokio::spawn(async move {
620        orchestrator
621            .transfer(&base_url, outgoing_files, transfer_opts, Some(progress_tx))
622            .await
623    });
624
625    // Event pump for progress bars
626    while let Some(ev) = progress_rx.recv().await {
627        tracker.handle_event(&ev);
628    }
629
630    match orchestrator_task.await {
631        Ok(Ok(())) => {
632            println!("All files transferred successfully!");
633            Ok(())
634        }
635        Ok(Err(err)) => {
636            anyhow::bail!("Transfer failed: {err}");
637        }
638        Err(join_err) => {
639            anyhow::bail!("Transfer task panicked or aborted: {join_err}");
640        }
641    }
642}
643
644/// Options for the `lanflare receive` command.
645#[derive(Clone, Debug)]
646pub struct ReceiveOptions {
647    pub dir: Option<PathBuf>,
648    pub port: Option<u16>,
649    pub auto_accept: bool,
650    pub pin: Option<String>,
651}
652
653/// Handles the `lanflare receive` command.
654///
655/// Starts Hyper HTTP/HTTPS server daemon with optional PIN protection,
656/// receives file upload sessions, supports auto-accept or interactive approval,
657/// and saves incoming files directly to the download directory.
658pub async fn receive(
659    config: &AppConfig,
660    identity: &TlsIdentity,
661    options: &ReceiveOptions,
662) -> anyhow::Result<()> {
663    let dest_dir = options
664        .dir
665        .as_ref()
666        .unwrap_or(&config.download_dir);
667
668    tokio::fs::create_dir_all(dest_dir)
669        .await
670        .with_context(|| format!("Failed to create download directory: {}", dest_dir.display()))?;
671
672    let port = options.port.unwrap_or(config.port);
673    let auto_accept = options.auto_accept || config.auto_accept;
674    let pin = options.pin.clone().or_else(|| config.pin.clone());
675
676    let server_config = AppConfig {
677        alias: config.alias.clone(),
678        port,
679        download_dir: dest_dir.clone(),
680        auto_accept,
681        pin: pin.clone(),
682    };
683
684    let registry = PeerRegistry::new();
685    let (event_tx, mut event_rx) = mpsc::channel::<ServerTransferEvent>(100);
686
687    let server_handle = start_server(
688        server_config,
689        Some(identity.clone()),
690        registry.clone(),
691        Some(event_tx),
692    )
693    .await
694    .with_context(|| format!("Failed to start receive server on port {port}"))?;
695
696    let bound_port = server_handle.bound_port();
697    println!("Receiver active and listening on port {bound_port}");
698    println!("Saving files to: {}", dest_dir.display());
699    println!("Auto-accept transfers: {}", if auto_accept { "Enabled" } else { "Disabled (interactive prompt)" });
700    if let Some(ref p) = pin {
701        println!("PIN protection enabled: required PIN is '{p}'");
702    }
703    println!("Certificate Fingerprint: {}", identity.fingerprint);
704    println!("Press Ctrl+C to terminate receiver daemon.\n");
705
706    // Event loop handling transfer events
707    while let Some(event) = event_rx.recv().await {
708        match event {
709            ServerTransferEvent::DeviceRegistered { ip, info } => {
710                println!("Device registered: {} ({}) at {}", info.alias, info.version, ip);
711            }
712            ServerTransferEvent::PrepareUpload {
713                session_id,
714                sender_ip,
715                sender_info,
716                files,
717                decision_tx,
718                ..
719            } => {
720                println!("\n[Incoming Transfer Request]");
721                println!("  From:       {} ({})", sender_info.alias, sender_ip);
722                println!("  Session ID: {session_id}");
723                println!("  Files ({}) :", files.len());
724
725                let mut total_size = 0u64;
726                for (id, file) in &files {
727                    total_size += file.size;
728                    println!(
729                        "    - {} ({:.2} MB) [ID: {}]",
730                        file.file_name,
731                        file.size as f64 / (1024.0 * 1024.0),
732                        id
733                    );
734                }
735                println!("  Total Size: {:.2} MB", total_size as f64 / (1024.0 * 1024.0));
736
737                if auto_accept {
738                    println!("Auto-accepting incoming transfer...");
739                    let all_ids = files.keys().cloned().collect();
740                    let _ = decision_tx.send(PrepareUploadDecision::Accept(all_ids));
741                } else {
742                    let ans = prompt_user_input("Accept this transfer? [Y/n]: ")
743                        .unwrap_or_else(|_| "n".to_string());
744                    if ans.is_empty() || ans.eq_ignore_ascii_case("y") || ans.eq_ignore_ascii_case("yes") {
745                        let all_ids = files.keys().cloned().collect();
746                        let _ = decision_tx.send(PrepareUploadDecision::Accept(all_ids));
747                        println!("Accepted transfer.");
748                    } else {
749                        let _ = decision_tx.send(PrepareUploadDecision::Decline);
750                        println!("Declined transfer.");
751                    }
752                }
753            }
754            ServerTransferEvent::FileStarted { file_name, total_bytes, .. } => {
755                println!(
756                    "Receiving file: {} ({:.2} MB)...",
757                    file_name,
758                    total_bytes as f64 / (1024.0 * 1024.0)
759                );
760            }
761            ServerTransferEvent::FileCompleted { result, .. } => {
762                println!(
763                    "File saved successfully: {} (SHA-256: {}) ✓",
764                    result.target_path.display(),
765                    result.sha256
766                );
767            }
768            ServerTransferEvent::FileFailed { file_id, error, .. } => {
769                eprintln!("File transfer failed [ID: {file_id}]: {error}");
770            }
771            ServerTransferEvent::SessionEnded { session_id, reason } => {
772                println!("Transfer session ended [ID: {session_id}]: {reason:?}\n");
773            }
774        }
775    }
776
777    server_handle.wait().await;
778    Ok(())
779}