pub mod discovery; pub mod error; pub mod http_server; pub mod models; pub mod transfer; use std::{ collections::HashMap, net::{Ipv4Addr, SocketAddr, SocketAddrV4}, sync::{Arc, OnceLock}, }; use julid::Julid; use log::error; use models::{Device, FileMetadata}; use serde::{Deserialize, Serialize}; use tokio::{ net::UdpSocket, sync::{ Mutex, mpsc::{self, UnboundedSender}, }, task::JoinSet, }; use transfer::Session; pub const DEFAULT_PORT: u16 = 53317; pub const MULTICAST_IP: Ipv4Addr = Ipv4Addr::new(224, 0, 0, 167); pub const LISTENING_SOCKET_ADDR: SocketAddrV4 = SocketAddrV4::new(Ipv4Addr::from_bits(0), DEFAULT_PORT); pub type ShutdownSender = mpsc::Sender<()>; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Listeners { Udp, Http, Multicast, } #[derive(Debug, Clone, PartialEq, Eq)] pub enum UploadDialog { UploadDeny, UploadConfirm, } #[derive(Debug, Clone)] pub enum TransferEvent { Received(Julid), Cancelled(Julid), UploadRequest { id: Julid, request: JoecalUploadRequest, }, } #[derive(Clone, Debug)] pub struct JoecalUploadRequest { pub alias: String, pub files: HashMap, pub tx: UnboundedSender, } /// Contains the main network and backend state for an application session. #[derive(Clone)] pub struct JoecalService { pub device: Device, pub peers: Arc>>, pub sessions: Arc>>, // Session ID to Session pub running_state: Arc>, pub socket: Arc, pub client: reqwest::Client, shutdown_sender: OnceLock, // the receiving end will be held by the application so it can update the UI based on backend // events transfer_event_tx: UnboundedSender, } impl JoecalService { pub async fn new( device: Device, transfer_event_tx: UnboundedSender, ) -> crate::error::Result { let socket = UdpSocket::bind(LISTENING_SOCKET_ADDR).await?; socket.set_multicast_loop_v4(true)?; socket.set_multicast_ttl_v4(2)?; // one hop out from localnet socket.join_multicast_v4(MULTICAST_IP, Ipv4Addr::from_bits(0))?; Ok(Self { device, client: reqwest::Client::new(), socket: socket.into(), transfer_event_tx, peers: Default::default(), sessions: Default::default(), running_state: Default::default(), shutdown_sender: Default::default(), }) } pub async fn start(&self, config: &Config, handles: &mut JoinSet) { let state = self.clone(); let konfig = config.clone(); handles.spawn({ let (tx, shutdown_rx) = mpsc::channel(1); let _ = self.shutdown_sender.set(tx); async move { if let Err(e) = state.start_http_server(shutdown_rx, &konfig).await { error!("HTTP server error: {e}"); } Listeners::Http } }); let state = self.clone(); let konfig = config.clone(); handles.spawn({ async move { if let Err(e) = state.listen_multicast(&konfig).await { error!("UDP listener error: {e}"); } Listeners::Multicast } }); let state = self.clone(); let config = config.clone(); handles.spawn({ async move { loop { let rstate = state.running_state.lock().await; if *rstate == RunningState::Stopping { break; } if let Err(e) = state.announce(None, &config).await { error!("Announcement error: {e}"); } tokio::time::sleep(std::time::Duration::from_secs(5)).await; } Listeners::Udp } }); } pub async fn stop(&self) { let mut rstate = self.running_state.lock().await; *rstate = RunningState::Stopping; let _ = self .shutdown_sender .get() .expect("Could not get stop signal transmitter") .send(()) .await; } pub async fn refresh_peers(&self) { let mut peers = self.peers.lock().await; peers.clear(); } pub fn send_event(&self, event: TransferEvent) { if let Err(e) = self.transfer_event_tx.send(event.clone()) { error!("got error sending transfer event '{event:?}': {e:?}"); } } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum RunningState { Running, Stopping, } impl Default for RunningState { fn default() -> Self { Self::Running } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Config { pub multicast_addr: SocketAddrV4, pub port: u16, pub download_dir: String, } impl Default for Config { fn default() -> Self { let home = std::env::home_dir().unwrap_or("/tmp".into()); let dd = home.join("joecalsend-downloads"); Self { multicast_addr: SocketAddrV4::new(MULTICAST_IP, DEFAULT_PORT), port: DEFAULT_PORT, download_dir: dd.to_string_lossy().into(), } } }