From adf6f9d7574a337405d60321cd2da352386501da Mon Sep 17 00:00:00 2001 From: ddidderr Date: Sun, 2 Mar 2025 13:06:18 +0100 Subject: [PATCH] [code] improve structure (focus: server) --- crates/lanspread-client/src/lib.rs | 10 +- crates/lanspread-compat/Cargo.toml | 3 + crates/lanspread-compat/src/eti.rs | 19 + crates/lanspread-db/src/db.rs | 13 +- crates/lanspread-server/src/assets.rs | 24 -- crates/lanspread-server/src/cli.rs | 23 ++ crates/lanspread-server/src/main.rs | 378 +++--------------- crates/lanspread-server/src/quic.rs | 152 +++++++ crates/lanspread-server/src/req.rs | 129 ++++++ .../src-tauri/src/lib.rs | 6 +- crates/lanspread-utils/src/macros.rs | 4 +- 11 files changed, 393 insertions(+), 368 deletions(-) delete mode 100644 crates/lanspread-server/src/assets.rs create mode 100644 crates/lanspread-server/src/cli.rs create mode 100644 crates/lanspread-server/src/quic.rs create mode 100644 crates/lanspread-server/src/req.rs diff --git a/crates/lanspread-client/src/lib.rs b/crates/lanspread-client/src/lib.rs index 54bfdc8..25d2a0b 100644 --- a/crates/lanspread-client/src/lib.rs +++ b/crates/lanspread-client/src/lib.rs @@ -6,12 +6,12 @@ use bytes::BytesMut; use lanspread_db::db::{Game, GameFileDescription}; use lanspread_proto::{Message as _, Request, Response}; use lanspread_utils::maybe_addr; -use s2n_quic::{client::Connect, provider::limits::Limits, Client as QuicClient, Connection}; +use s2n_quic::{Client as QuicClient, Connection, client::Connect, provider::limits::Limits}; use tokio::{ io::AsyncWriteExt, sync::{ - mpsc::{UnboundedReceiver, UnboundedSender}, Mutex, + mpsc::{UnboundedReceiver, UnboundedSender}, }, }; @@ -72,9 +72,7 @@ async fn download_game_files( server_addr: SocketAddr, tx_notify_ui: UnboundedSender, ) -> eyre::Result<()> { - let limits = Limits::default() - .with_max_handshake_duration(Duration::from_secs(3))? - .with_max_idle_timeout(Duration::from_secs(1))?; + let limits = Limits::default().with_max_handshake_duration(Duration::from_secs(3))?; let client = QuicClient::builder() .with_tls(CERT_PEM)? @@ -173,7 +171,7 @@ pub async fn run( loop { let limits = Limits::default() .with_max_handshake_duration(Duration::from_secs(3))? - .with_max_idle_timeout(Duration::from_secs(1))?; + .with_max_idle_timeout(Duration::ZERO)?; let client = QuicClient::builder() .with_tls(CERT_PEM)? diff --git a/crates/lanspread-compat/Cargo.toml b/crates/lanspread-compat/Cargo.toml index 88437d3..07aa835 100644 --- a/crates/lanspread-compat/Cargo.toml +++ b/crates/lanspread-compat/Cargo.toml @@ -4,6 +4,9 @@ version = "0.1.0" edition = "2024" [dependencies] +# local +lanspread-db = { path = "../lanspread-db" } + eyre = { workspace = true } sqlx = { workspace = true } serde = { workspace = true } diff --git a/crates/lanspread-compat/src/eti.rs b/crates/lanspread-compat/src/eti.rs index 74fb41a..8fef027 100644 --- a/crates/lanspread-compat/src/eti.rs +++ b/crates/lanspread-compat/src/eti.rs @@ -1,5 +1,6 @@ use std::path::Path; +use lanspread_db::db::Game; use serde::{Deserialize, Serialize}; use sqlx::sqlite::SqlitePool; @@ -45,3 +46,21 @@ pub async fn get_games(db: &Path) -> eyre::Result> { Ok(games) } + +impl From for Game { + fn from(eti_game: EtiGame) -> Self { + Self { + id: eti_game.game_id, + name: eti_game.game_title, + description: eti_game.game_readme_de, + release_year: eti_game.game_release, + publisher: eti_game.game_publisher, + max_players: eti_game.game_maxplayers, + version: eti_game.game_version, + genre: eti_game.genre_de, + size: (eti_game.game_size * 1024.0 * 1024.0 * 1024.0) as u64, + thumbnail: None, + installed: false, + } + } +} diff --git a/crates/lanspread-db/src/db.rs b/crates/lanspread-db/src/db.rs index 696d8a5..cac0dbc 100644 --- a/crates/lanspread-db/src/db.rs +++ b/crates/lanspread-db/src/db.rs @@ -1,7 +1,7 @@ #![allow(clippy::missing_errors_doc)] #![allow(clippy::doc_markdown)] -use std::{collections::HashMap, fmt}; +use std::{collections::HashMap, fmt, path::Path}; use bytes::Bytes; use serde::{Deserialize, Serialize}; @@ -93,6 +93,17 @@ impl GameDB { db } + pub fn add_thumbnails(&mut self, thumbs_dir: &Path) { + for game in self.games.values_mut() { + let asset = thumbs_dir.join(format!("{}.jpg", game.id)); + if let Ok(data) = std::fs::read(&asset) { + game.thumbnail = Some(Bytes::from(data)); + } else { + tracing::warn!("Thumbnail missing: {}", game.id); + } + } + } + #[must_use] pub fn get_game_by_id(&self, id: S) -> Option<&Game> where diff --git a/crates/lanspread-server/src/assets.rs b/crates/lanspread-server/src/assets.rs deleted file mode 100644 index fb1d007..0000000 --- a/crates/lanspread-server/src/assets.rs +++ /dev/null @@ -1,24 +0,0 @@ -use std::path::PathBuf; - -use bytes::Bytes; - -pub(crate) struct Thumbnails { - folder: PathBuf, -} - -impl Thumbnails { - pub(crate) fn new(folder: PathBuf) -> Thumbnails { - Thumbnails { folder } - } - - pub(crate) fn get(&self, path: &str) -> Option { - let asset = self.folder.join(format!("{path}.jpg")); - - if let Ok(data) = std::fs::read(asset) { - return Some(Bytes::from(data)); - } - - tracing::warn!("Thumbnail not found: {path}"); - None - } -} diff --git a/crates/lanspread-server/src/cli.rs b/crates/lanspread-server/src/cli.rs new file mode 100644 index 0000000..210d2fb --- /dev/null +++ b/crates/lanspread-server/src/cli.rs @@ -0,0 +1,23 @@ +use std::{net::IpAddr, path::PathBuf}; + +use clap::Parser; + +#[allow(clippy::doc_markdown)] +#[derive(Debug, Parser)] +pub(crate) struct Cli { + /// IP address to bind to. + #[clap(long)] + pub(crate) ip: IpAddr, + /// Listen port. + #[clap(long)] + pub(crate) port: u16, + /// Game database path (SQLite). + #[clap(long)] + pub(crate) db: PathBuf, + /// Games folder. + #[clap(long)] + pub(crate) game_dir: PathBuf, + /// Thumbnails folder. + #[clap(long)] + pub(crate) thumbs_dir: PathBuf, +} diff --git a/crates/lanspread-server/src/main.rs b/crates/lanspread-server/src/main.rs index 8dab420..c802729 100644 --- a/crates/lanspread-server/src/main.rs +++ b/crates/lanspread-server/src/main.rs @@ -1,304 +1,54 @@ -#![allow(clippy::doc_markdown)] +mod cli; +mod quic; +mod req; -use std::{ - fs::File, - io::Read as _, - net::{IpAddr, SocketAddr}, - path::{Path, PathBuf}, - sync::Arc, -}; +use std::{convert::Into, net::SocketAddr}; -use assets::Thumbnails; -use bytes::Bytes; -use clap::Parser; -use lanspread_compat::eti::{self, EtiGame}; -use lanspread_db::db::{Game, GameDB, GameFileDescription}; +use clap::Parser as _; +use cli::Cli; +use lanspread_compat::eti; +use lanspread_db::db::{Game, GameDB}; use lanspread_mdns::{ - DaemonEvent, - MdnsAdvertiser, - LANSPREAD_INSTANCE_NAME, - LANSPREAD_SERVICE_TYPE, + DaemonEvent, LANSPREAD_INSTANCE_NAME, LANSPREAD_SERVICE_TYPE, MdnsAdvertiser, }; -use lanspread_proto::{Message as _, Request, Response}; -use lanspread_utils::maybe_addr; -use s2n_quic::Server as QuicServer; -use tokio::{io::AsyncWriteExt, sync::Mutex}; +use quic::run_server; use tracing_subscriber::EnvFilter; -use walkdir::WalkDir; -mod assets; +fn spawn_mdns_task(server_addr: SocketAddr) -> eyre::Result<()> { + let mdns = MdnsAdvertiser::new(LANSPREAD_SERVICE_TYPE, LANSPREAD_INSTANCE_NAME, server_addr)?; -static KEY_PEM: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../key.pem")); -static CERT_PEM: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../cert.pem")); - -#[derive(Clone, Debug)] -struct ServerCtx { - handler: RequestHandler, - games_folder: PathBuf, -} - -#[derive(Clone, Debug)] -struct ConnectionCtx { - server_ctx: Arc, - remote_addr: String, -} - -async fn run(addr: SocketAddr, db: GameDB, games_folder: PathBuf) -> eyre::Result<()> { - let mut server = QuicServer::builder() - .with_tls((CERT_PEM, KEY_PEM))? - .with_io(addr)? - .start()?; - - let server_ctx = Arc::new(ServerCtx { - handler: RequestHandler::new(db), - games_folder, - }); - - while let Some(mut connection) = server.accept().await { - let conn_ctx = Arc::new(ConnectionCtx { - server_ctx: server_ctx.clone(), - remote_addr: maybe_addr!(connection.remote_addr()), - }); - // spawn a new task for the connection - tokio::spawn(async move { - tracing::info!("{} connected", conn_ctx.remote_addr); - - while let Ok(Some(stream)) = connection.accept_bidirectional_stream().await { - let (mut rx, mut tx) = stream.split(); - - let conn_ctx = conn_ctx.clone(); - // spawn a new task for the stream - tokio::spawn(async move { - tracing::trace!("{} stream opened", conn_ctx.remote_addr); - - // handle streams - while let Ok(Some(data)) = rx.receive().await { - tracing::trace!( - "{} msg: (raw): {}", - conn_ctx.remote_addr, - String::from_utf8_lossy(&data) - ); - - let request = Request::decode(data); - tracing::debug!("{} msg: {:?}", conn_ctx.remote_addr, request); - - if let Request::GetGameFileData(game_file_desc) = &request { - tracing::debug!( - "{} client requested game file data: {:?}", - conn_ctx.remote_addr, - game_file_desc - ); - - // deliver file data to client - let path = conn_ctx - .server_ctx - .games_folder - .join(&game_file_desc.relative_path); - - if let Ok(mut file) = File::open(&path) { - let mut buf = vec![0; 64 * 1024]; - - while let Ok(n) = file.read(&mut buf) { - if n == 0 { - break; - } - - if let Err(e) = tx.write_all(&buf[..n]).await { - tracing::error!( - "{} failed to send file data: {}", - conn_ctx.remote_addr, - e - ); - } - } - - // if let Err(e) = tokio::io::copy(&mut file, &mut tx).await { - // tracing::error!( - // "{} failed to send file data: {}", - // conn_ctx.remote_addr, - // e - // ); - // } - } else { - tracing::error!( - "{} failed to open file: {}", - conn_ctx.remote_addr, - path.display() - ); - } - - if let Err(e) = tx.close().await { - tracing::error!( - "{} failed to close stream: {}", - conn_ctx.remote_addr, - e - ); - } - - continue; - } - - let response = conn_ctx - .server_ctx - .handler - .handle_request(request, &conn_ctx) - .await; - - tracing::trace!("{} server response: {:?}", conn_ctx.remote_addr, response); - let raw_response = response.encode(); - tracing::trace!( - "{} server response (raw): {}", - conn_ctx.remote_addr, - String::from_utf8_lossy(&raw_response) - ); - - // write response back to client - if let Err(e) = tx.write_all(&raw_response).await { - tracing::error!(?e); - } - - // close the stream - if let Err(e) = tx.close().await { - tracing::error!(?e); - } - } - }); + tokio::spawn(async move { + while let Ok(event) = mdns.monitor.recv() { + tracing::info!("mDNS: {:?}", &event); + if let DaemonEvent::Error(e) = event { + tracing::error!("mDNS: {e}"); + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + continue; } - }); - } + } + }); Ok(()) } -fn get_relative_path(base: &Path, deep_path: &Path) -> std::io::Result { - let base_canonical = base.canonicalize()?; - let full_canonical = deep_path.canonicalize()?; +async fn prepare_game_db(cli: &Cli) -> eyre::Result { + // build games from ETI database + let mut games: Vec = eti::get_games(&cli.db) + .await? + .into_iter() + .map(Into::into) + .collect(); - full_canonical - .strip_prefix(&base_canonical) - .map(std::path::Path::to_path_buf) - .map_err(|_| { - std::io::Error::new( - std::io::ErrorKind::Other, - "Path is not within base directory", - ) - }) -} + // filter out games that the server does not have in game_dir + games.retain(|game| cli.game_dir.join(&game.id).is_dir()); -#[derive(Clone, Debug)] -struct RequestHandler { - db: Arc>, -} + let mut game_db = GameDB::from(games); -impl RequestHandler { - fn new(games: GameDB) -> RequestHandler { - RequestHandler { - db: Arc::new(Mutex::new(games)), - } - } + game_db.add_thumbnails(&cli.thumbs_dir); - async fn handle_request(&self, request: Request, conn_ctx: &ConnectionCtx) -> Response { - match request { - Request::Ping => Response::Pong, - Request::ListGames => { - let db = self.db.lock().await; - Response::Games(db.all_games().into_iter().cloned().collect()) - } - Request::GetGame { id } => { - if self.db.lock().await.get_game_by_id(&id).is_none() { - tracing::error!("Game not found in DB: {id}"); - return Response::GameNotFound(id); - } + tracing::info!("Prepared game database with {} games", game_db.games.len()); - let games_folder = &conn_ctx.server_ctx.games_folder; - - let game_dir = games_folder.join(&id); - if !game_dir.exists() { - tracing::error!("Game folder does not exist: {}", game_dir.display()); - return Response::GameNotFound(id); - } - - let mut game_files_descs: Vec = vec![]; - - for entry in WalkDir::new(&game_dir) - .into_iter() - .filter_map(std::result::Result::ok) - { - match get_relative_path(games_folder, entry.path()) { - Ok(relative_path) => match relative_path.to_str() { - Some(relative_path) => { - let game_file_description = GameFileDescription { - game_id: id.clone(), - relative_path: relative_path.to_string(), - is_dir: entry.file_type().is_dir(), - }; - - tracing::debug!("Found game file: {:?}", game_file_description); - - game_files_descs.push(game_file_description); - } - None => { - tracing::error!("Failed to get relative path: {relative_path:?}",); - } - }, - Err(e) => { - tracing::error!("Failed to get relative path: {e}"); - } - } - } - - Response::GetGame(game_files_descs) - } - Request::GetGameFileData(_) => { - Response::InvalidRequest(Bytes::new(), "Not implemented".to_string()) - } - Request::Invalid(data, err_msg) => { - tracing::error!( - "got invalid request from client (error: {}): {}", - err_msg, - String::from_utf8_lossy(&data) - ); - Response::InvalidRequest(data, err_msg) - } - } - } -} - -#[derive(Debug, Parser)] -struct Cli { - /// IP address to bind to. - #[clap(long)] - ip: IpAddr, - /// Listen port. - #[clap(long)] - port: u16, - /// Game database path (SQLite). - #[clap(long)] - db: PathBuf, - /// Games folder. - #[clap(long)] - folder: PathBuf, - /// Thumbnails folder. - #[clap(long)] - thumbnails: PathBuf, -} - -fn eti_game_to_game(eti_game: EtiGame) -> Game { - #[allow(clippy::cast_possible_truncation)] - #[allow(clippy::cast_sign_loss)] - Game { - id: eti_game.game_id, - name: eti_game.game_title, - description: eti_game.game_readme_de, - release_year: eti_game.game_release, - publisher: eti_game.game_publisher, - max_players: eti_game.game_maxplayers, - version: eti_game.game_version, - genre: eti_game.genre_de, - size: (eti_game.game_size * 1024.0 * 1024.0 * 1024.0) as u64, - thumbnail: None, - installed: false, - } + Ok(game_db) } #[tokio::main] @@ -309,57 +59,19 @@ async fn main() -> eyre::Result<()> { let cli = Cli::parse(); - #[allow(clippy::manual_assert)] - if !cli.folder.exists() { - panic!( - "Games folder does not exist: {}", - cli.folder.to_str().expect("Invalid path") - ); - } + assert!( + cli.game_dir.exists(), + "Games folder does not exist: {}", + cli.game_dir.to_str().expect("Invalid path") + ); - let eti_games = eti::get_games(&cli.db).await?; - let mut games: Vec = eti_games.into_iter().map(eti_game_to_game).collect(); + let server_addr = SocketAddr::from((cli.ip, cli.port)); - // Filter games based on existing folders - games.retain(|game| { - let game_folder = cli.folder.join(&game.id); - let exists = game_folder.exists() && game_folder.is_dir(); - if !exists { - tracing::debug!("Skipping game {}: folder not found", game.id); - } - exists - }); + // spawn mDNS listener task + spawn_mdns_task(server_addr)?; - let thumbnails = Thumbnails::new(cli.thumbnails); + let game_db = prepare_game_db(&cli).await?; - // add thumbnails to games - for game in &mut games { - if let Some(thumbnail) = thumbnails.get(&game.id) { - game.thumbnail = Some(thumbnail); - } else { - tracing::warn!("No thumbnail found: {}", game.id); - } - } - - let game_db = GameDB::from(games); - - let mdns = MdnsAdvertiser::new( - LANSPREAD_SERVICE_TYPE, - LANSPREAD_INSTANCE_NAME, - (cli.ip, cli.port).into(), - )?; - - tokio::spawn(async move { - while let Ok(event) = mdns.monitor.recv() { - tracing::info!("mDNS: {:?}", &event); - if let DaemonEvent::Error(e) = event { - tracing::error!("mDNS: {e}"); - break; - } - } - }); - - tracing::info!("Server listening on {}:{}", cli.ip, cli.port); - - run(SocketAddr::from((cli.ip, cli.port)), game_db, cli.folder).await + tracing::info!("Server listening on {server_addr}"); + run_server(server_addr, game_db, cli.game_dir).await } diff --git a/crates/lanspread-server/src/quic.rs b/crates/lanspread-server/src/quic.rs new file mode 100644 index 0000000..89dfa4f --- /dev/null +++ b/crates/lanspread-server/src/quic.rs @@ -0,0 +1,152 @@ +use std::{net::SocketAddr, path::PathBuf, sync::Arc, time::Duration}; + +use lanspread_db::db::GameDB; +use lanspread_proto::{Message as _, Request}; +use lanspread_utils::maybe_addr; +use s2n_quic::{ + Connection, Server, + provider::limits::Limits, + stream::{ReceiveStream, SendStream}, +}; +use tokio::io::AsyncWriteExt as _; + +use crate::req::{RequestHandler, send_game_file_data}; + +static KEY_PEM: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../key.pem")); +static CERT_PEM: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../cert.pem")); + +#[derive(Clone, Debug)] +struct ServerCtx { + handler: RequestHandler, + games_folder: PathBuf, +} + +#[derive(Clone, Debug)] +struct ConnectionCtx { + server_ctx: Arc, +} + +#[derive(Clone, Debug)] +struct StreamCtx { + conn_ctx: Arc, +} + +async fn handle_bidi_stream( + mut rx: ReceiveStream, + mut tx: SendStream, + ctx: Arc, +) -> eyre::Result<()> { + let remote_addr = maybe_addr!(rx.connection().remote_addr()); + tracing::trace!("{remote_addr} stream opened"); + + // handle streams + while let Ok(Some(data)) = rx.receive().await { + tracing::trace!( + "{remote_addr} msg: (raw): {}", + String::from_utf8_lossy(&data) + ); + + let request = Request::decode(data); + tracing::debug!("{remote_addr} msg: {request:?}"); + + // special case for now (send game file data to client) + if let Request::GetGameFileData(game_file_desc) = &request { + send_game_file_data( + game_file_desc, + &mut tx, + &ctx.conn_ctx.server_ctx.games_folder, + ) + .await; + continue; + } + + let response = ctx + .conn_ctx + .server_ctx + .handler + .handle_request(request, &ctx.conn_ctx.server_ctx.games_folder) + .await; + + tracing::trace!("{remote_addr} server response: {response:?}"); + let raw_response = response.encode(); + tracing::trace!( + "{remote_addr} server response (raw): {}", + String::from_utf8_lossy(&raw_response) + ); + + // write response back to client + if let Err(e) = tx.write_all(&raw_response).await { + tracing::error!(?e); + } + + // close the stream + if let Err(e) = tx.close().await { + tracing::error!(?e); + } + } + + Ok(()) +} + +async fn handle_connection( + mut connection: Connection, + ctx: Arc, +) -> eyre::Result<()> { + let remote_addr = maybe_addr!(connection.remote_addr()); + tracing::info!("{remote_addr} connected"); + + // handle streams + while let Ok(Some(stream)) = connection.accept_bidirectional_stream().await { + let remote_addr = remote_addr.clone(); + let (rx, tx) = stream.split(); + + let ctx = Arc::new(StreamCtx { + conn_ctx: ctx.clone(), + }); + + // spawn a new task for the stream + tokio::spawn(async move { + if let Err(e) = handle_bidi_stream(rx, tx, ctx).await { + tracing::error!("{remote_addr} stream error: {e}"); + } + }); + } + + Ok(()) +} + +pub(crate) async fn run_server( + addr: SocketAddr, + db: GameDB, + games_folder: PathBuf, +) -> eyre::Result<()> { + let limits = Limits::default() + .with_max_idle_timeout(Duration::ZERO)? + .with_max_handshake_duration(Duration::from_secs(3))?; + + let mut server = Server::builder() + .with_tls((CERT_PEM, KEY_PEM))? + .with_io(addr)? + .with_limits(limits)? + .start()?; + + let server_ctx = Arc::new(ServerCtx { + handler: RequestHandler::new(db), + games_folder, + }); + + while let Some(connection) = server.accept().await { + let conn_ctx = Arc::new(ConnectionCtx { + server_ctx: server_ctx.clone(), + }); + + // spawn a new task for the connection + tokio::spawn(async move { + if let Err(e) = handle_connection(connection, conn_ctx).await { + tracing::error!("Connection error: {}", e); + } + }); + } + + Ok(()) +} diff --git a/crates/lanspread-server/src/req.rs b/crates/lanspread-server/src/req.rs new file mode 100644 index 0000000..1e48379 --- /dev/null +++ b/crates/lanspread-server/src/req.rs @@ -0,0 +1,129 @@ +use std::{ + path::{Path, PathBuf}, + sync::Arc, +}; + +use bytes::Bytes; +use lanspread_db::db::{GameDB, GameFileDescription}; +use lanspread_proto::{Request, Response}; +use lanspread_utils::maybe_addr; +use s2n_quic::stream::SendStream; +use tokio::sync::Mutex; +use walkdir::WalkDir; + +#[derive(Clone, Debug)] +pub(crate) struct RequestHandler { + db: Arc>, +} + +impl RequestHandler { + pub(crate) fn new(games: GameDB) -> RequestHandler { + RequestHandler { + db: Arc::new(Mutex::new(games)), + } + } + + pub(crate) async fn handle_request(&self, request: Request, games_folder: &Path) -> Response { + match request { + Request::Ping => Response::Pong, + Request::ListGames => { + let db = self.db.lock().await; + Response::Games(db.all_games().into_iter().cloned().collect()) + } + Request::GetGame { id } => { + if self.db.lock().await.get_game_by_id(&id).is_none() { + tracing::error!("Game not found in DB: {id}"); + return Response::GameNotFound(id); + } + + let game_dir = games_folder.join(&id); + if !game_dir.exists() { + tracing::error!("Game folder does not exist: {}", game_dir.display()); + return Response::GameNotFound(id); + } + + let mut game_files_descs: Vec = vec![]; + + for entry in WalkDir::new(&game_dir) + .into_iter() + .filter_map(std::result::Result::ok) + { + match get_relative_path(games_folder, entry.path()) { + Ok(relative_path) => match relative_path.to_str() { + Some(relative_path) => { + let game_file_description = GameFileDescription { + game_id: id.clone(), + relative_path: relative_path.to_string(), + is_dir: entry.file_type().is_dir(), + }; + + tracing::debug!("Found game file: {:?}", game_file_description); + + game_files_descs.push(game_file_description); + } + None => { + tracing::error!("Failed to get relative path: {relative_path:?}",); + } + }, + Err(e) => { + tracing::error!("Failed to get relative path: {e}"); + } + } + } + + Response::GetGame(game_files_descs) + } + Request::GetGameFileData(_) => { + Response::InvalidRequest(Bytes::new(), "Not implemented".to_string()) + } + Request::Invalid(data, err_msg) => { + tracing::error!( + "got invalid request from client (error: {}): {}", + err_msg, + String::from_utf8_lossy(&data) + ); + Response::InvalidRequest(data, err_msg) + } + } + } +} + +pub(crate) async fn send_game_file_data( + game_file_desc: &GameFileDescription, + tx: &mut SendStream, + game_dir: &Path, +) { + let remote_addr = maybe_addr!(tx.connection().remote_addr()); + + tracing::debug!("{remote_addr} client requested game file data: {game_file_desc:?}",); + + // deliver file data to client + let path = game_dir.join(&game_file_desc.relative_path); + + if let Ok(mut file) = tokio::fs::File::open(&path).await { + if let Err(e) = tokio::io::copy(&mut file, tx).await { + tracing::error!("{remote_addr} failed to send file data: {e}",); + } + } else { + tracing::error!("{remote_addr} failed to open file: {}", path.display()); + } + + if let Err(e) = tx.close().await { + tracing::error!("{remote_addr} failed to close stream: {e}"); + } +} + +fn get_relative_path(base: &Path, deep_path: &Path) -> std::io::Result { + let base_canonical = base.canonicalize()?; + let full_canonical = deep_path.canonicalize()?; + + full_canonical + .strip_prefix(&base_canonical) + .map(std::path::Path::to_path_buf) + .map_err(|_| { + std::io::Error::new( + std::io::ErrorKind::Other, + "Path is not within base directory", + ) + }) +} diff --git a/crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs b/crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs index af6643f..a31ecb3 100644 --- a/crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs +++ b/crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs @@ -9,10 +9,10 @@ use std::{ use eyre::bail; use lanspread_client::{ClientCommand, ClientEvent}; use lanspread_db::db::{Game, GameDB}; -use lanspread_mdns::{discover_service, LANSPREAD_INSTANCE_NAME, LANSPREAD_SERVICE_TYPE}; +use lanspread_mdns::{LANSPREAD_INSTANCE_NAME, LANSPREAD_SERVICE_TYPE, discover_service}; use tauri::{AppHandle, Emitter as _, Manager}; -use tauri_plugin_shell::{process::Command, ShellExt}; -use tokio::sync::{mpsc::UnboundedSender, Mutex}; +use tauri_plugin_shell::{ShellExt, process::Command}; +use tokio::sync::{Mutex, mpsc::UnboundedSender}; // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ diff --git a/crates/lanspread-utils/src/macros.rs b/crates/lanspread-utils/src/macros.rs index 691ecaf..e651165 100644 --- a/crates/lanspread-utils/src/macros.rs +++ b/crates/lanspread-utils/src/macros.rs @@ -1,6 +1,8 @@ #[macro_export] macro_rules! maybe_addr { ($addr:expr) => { - $addr.map_or("".to_string(), |addr| addr.to_string()) + $addr.map_or(Arc::new("".to_string()), |addr| { + Arc::new(addr.to_string()) + }) }; }