Compare commits

..

No commits in common. "adf6f9d7574a337405d60321cd2da352386501da" and "d1eb185498e55315e682addaa96d0c110cb5b2b6" have entirely different histories.

20 changed files with 1176 additions and 1414 deletions

1805
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -15,9 +15,9 @@ resolver = "2"
bytes = { version = "1.9", features = ["serde"] } bytes = { version = "1.9", features = ["serde"] }
clap = { version = "4.5", features = ["derive"] } clap = { version = "4.5", features = ["derive"] }
eyre = "0.6" eyre = "0.6"
itertools = "0.14" itertools = "0.13"
log = "0.4" log = "0.4"
mdns-sd = "0.13" mdns-sd = "0.12"
s2n-quic = { version = "1.51", features = ["provider-event-tracing"] } s2n-quic = { version = "1.51", features = ["provider-event-tracing"] }
semver = "1.0" semver = "1.0"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }

View File

@ -1,7 +1,7 @@
[package] [package]
name = "lanspread-client" name = "lanspread-client"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2021"
[lints.rust] [lints.rust]
unsafe_code = "forbid" unsafe_code = "forbid"

View File

@ -6,12 +6,12 @@ use bytes::BytesMut;
use lanspread_db::db::{Game, GameFileDescription}; use lanspread_db::db::{Game, GameFileDescription};
use lanspread_proto::{Message as _, Request, Response}; use lanspread_proto::{Message as _, Request, Response};
use lanspread_utils::maybe_addr; use lanspread_utils::maybe_addr;
use s2n_quic::{Client as QuicClient, Connection, client::Connect, provider::limits::Limits}; use s2n_quic::{client::Connect, provider::limits::Limits, Client as QuicClient, Connection};
use tokio::{ use tokio::{
io::AsyncWriteExt, io::AsyncWriteExt,
sync::{ sync::{
Mutex,
mpsc::{UnboundedReceiver, UnboundedSender}, mpsc::{UnboundedReceiver, UnboundedSender},
Mutex,
}, },
}; };
@ -72,7 +72,9 @@ async fn download_game_files(
server_addr: SocketAddr, server_addr: SocketAddr,
tx_notify_ui: UnboundedSender<ClientEvent>, tx_notify_ui: UnboundedSender<ClientEvent>,
) -> eyre::Result<()> { ) -> eyre::Result<()> {
let limits = Limits::default().with_max_handshake_duration(Duration::from_secs(3))?; let limits = Limits::default()
.with_max_handshake_duration(Duration::from_secs(3))?
.with_max_idle_timeout(Duration::from_secs(1))?;
let client = QuicClient::builder() let client = QuicClient::builder()
.with_tls(CERT_PEM)? .with_tls(CERT_PEM)?
@ -171,7 +173,7 @@ pub async fn run(
loop { loop {
let limits = Limits::default() let limits = Limits::default()
.with_max_handshake_duration(Duration::from_secs(3))? .with_max_handshake_duration(Duration::from_secs(3))?
.with_max_idle_timeout(Duration::ZERO)?; .with_max_idle_timeout(Duration::from_secs(1))?;
let client = QuicClient::builder() let client = QuicClient::builder()
.with_tls(CERT_PEM)? .with_tls(CERT_PEM)?

View File

@ -1,12 +1,9 @@
[package] [package]
name = "lanspread-compat" name = "lanspread-compat"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2021"
[dependencies] [dependencies]
# local
lanspread-db = { path = "../lanspread-db" }
eyre = { workspace = true } eyre = { workspace = true }
sqlx = { workspace = true } sqlx = { workspace = true }
serde = { workspace = true } serde = { workspace = true }

View File

@ -1,6 +1,5 @@
use std::path::Path; use std::path::Path;
use lanspread_db::db::Game;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::sqlite::SqlitePool; use sqlx::sqlite::SqlitePool;
@ -46,21 +45,3 @@ pub async fn get_games(db: &Path) -> eyre::Result<Vec<EtiGame>> {
Ok(games) Ok(games)
} }
impl From<EtiGame> 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,
}
}
}

View File

@ -1,7 +1,7 @@
[package] [package]
name = "lanspread-db" name = "lanspread-db"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2021"
[lints.rust] [lints.rust]
unsafe_code = "forbid" unsafe_code = "forbid"

View File

@ -1,7 +1,7 @@
#![allow(clippy::missing_errors_doc)] #![allow(clippy::missing_errors_doc)]
#![allow(clippy::doc_markdown)] #![allow(clippy::doc_markdown)]
use std::{collections::HashMap, fmt, path::Path}; use std::{collections::HashMap, fmt};
use bytes::Bytes; use bytes::Bytes;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -93,17 +93,6 @@ impl GameDB {
db 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] #[must_use]
pub fn get_game_by_id<S>(&self, id: S) -> Option<&Game> pub fn get_game_by_id<S>(&self, id: S) -> Option<&Game>
where where

View File

@ -1,7 +1,7 @@
[package] [package]
name = "lanspread-mdns" name = "lanspread-mdns"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2021"
[dependencies] [dependencies]
mdns-sd = { workspace = true } mdns-sd = { workspace = true }

View File

@ -1,7 +1,7 @@
[package] [package]
name = "lanspread-proto" name = "lanspread-proto"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2021"
[lints.rust] [lints.rust]
unsafe_code = "forbid" unsafe_code = "forbid"

View File

@ -1,7 +1,7 @@
[package] [package]
name = "lanspread-server" name = "lanspread-server"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2021"
[lints.rust] [lints.rust]
unsafe_code = "forbid" unsafe_code = "forbid"

View File

@ -0,0 +1,24 @@
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<Bytes> {
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
}
}

View File

@ -1,23 +0,0 @@
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,
}

View File

@ -1,54 +1,304 @@
mod cli; #![allow(clippy::doc_markdown)]
mod quic;
mod req;
use std::{convert::Into, net::SocketAddr}; use std::{
fs::File,
use clap::Parser as _; io::Read as _,
use cli::Cli; net::{IpAddr, SocketAddr},
use lanspread_compat::eti; path::{Path, PathBuf},
use lanspread_db::db::{Game, GameDB}; sync::Arc,
use lanspread_mdns::{
DaemonEvent, LANSPREAD_INSTANCE_NAME, LANSPREAD_SERVICE_TYPE, MdnsAdvertiser,
}; };
use quic::run_server;
use assets::Thumbnails;
use bytes::Bytes;
use clap::Parser;
use lanspread_compat::eti::{self, EtiGame};
use lanspread_db::db::{Game, GameDB, GameFileDescription};
use lanspread_mdns::{
DaemonEvent,
MdnsAdvertiser,
LANSPREAD_INSTANCE_NAME,
LANSPREAD_SERVICE_TYPE,
};
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 tracing_subscriber::EnvFilter; use tracing_subscriber::EnvFilter;
use walkdir::WalkDir;
fn spawn_mdns_task(server_addr: SocketAddr) -> eyre::Result<()> { mod assets;
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<ServerCtx>,
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 { tokio::spawn(async move {
while let Ok(event) = mdns.monitor.recv() { tracing::info!("{} connected", conn_ctx.remote_addr);
tracing::info!("mDNS: {:?}", &event);
if let DaemonEvent::Error(e) = event { while let Ok(Some(stream)) = connection.accept_bidirectional_stream().await {
tracing::error!("mDNS: {e}"); let (mut rx, mut tx) = stream.split();
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
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; 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);
}
} }
}); });
}
});
}
Ok(()) Ok(())
} }
async fn prepare_game_db(cli: &Cli) -> eyre::Result<GameDB> { fn get_relative_path(base: &Path, deep_path: &Path) -> std::io::Result<PathBuf> {
// build games from ETI database let base_canonical = base.canonicalize()?;
let mut games: Vec<Game> = eti::get_games(&cli.db) let full_canonical = deep_path.canonicalize()?;
.await?
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",
)
})
}
#[derive(Clone, Debug)]
struct RequestHandler {
db: Arc<Mutex<GameDB>>,
}
impl RequestHandler {
fn new(games: GameDB) -> RequestHandler {
RequestHandler {
db: Arc::new(Mutex::new(games)),
}
}
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);
}
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<GameFileDescription> = vec![];
for entry in WalkDir::new(&game_dir)
.into_iter() .into_iter()
.map(Into::into) .filter_map(std::result::Result::ok)
.collect(); {
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(),
};
// filter out games that the server does not have in game_dir tracing::debug!("Found game file: {:?}", game_file_description);
games.retain(|game| cli.game_dir.join(&game.id).is_dir());
let mut game_db = GameDB::from(games); 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}");
}
}
}
game_db.add_thumbnails(&cli.thumbs_dir); 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)
}
}
}
}
tracing::info!("Prepared game database with {} games", game_db.games.len()); #[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,
}
Ok(game_db) 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,
}
} }
#[tokio::main] #[tokio::main]
@ -59,19 +309,57 @@ async fn main() -> eyre::Result<()> {
let cli = Cli::parse(); let cli = Cli::parse();
assert!( #[allow(clippy::manual_assert)]
cli.game_dir.exists(), if !cli.folder.exists() {
panic!(
"Games folder does not exist: {}", "Games folder does not exist: {}",
cli.game_dir.to_str().expect("Invalid path") cli.folder.to_str().expect("Invalid path")
); );
}
let server_addr = SocketAddr::from((cli.ip, cli.port)); let eti_games = eti::get_games(&cli.db).await?;
let mut games: Vec<Game> = eti_games.into_iter().map(eti_game_to_game).collect();
// spawn mDNS listener task // Filter games based on existing folders
spawn_mdns_task(server_addr)?; 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
});
let game_db = prepare_game_db(&cli).await?; let thumbnails = Thumbnails::new(cli.thumbnails);
tracing::info!("Server listening on {server_addr}"); // add thumbnails to games
run_server(server_addr, game_db, cli.game_dir).await 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
} }

View File

@ -1,152 +0,0 @@
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<ServerCtx>,
}
#[derive(Clone, Debug)]
struct StreamCtx {
conn_ctx: Arc<ConnectionCtx>,
}
async fn handle_bidi_stream(
mut rx: ReceiveStream,
mut tx: SendStream,
ctx: Arc<StreamCtx>,
) -> 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<ConnectionCtx>,
) -> 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(())
}

View File

@ -1,129 +0,0 @@
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<Mutex<GameDB>>,
}
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<GameFileDescription> = 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<PathBuf> {
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",
)
})
}

View File

@ -3,7 +3,7 @@ name = "lanspread-tauri-deno-ts"
version = "0.1.0" version = "0.1.0"
description = "A Tauri App" description = "A Tauri App"
authors = ["you"] authors = ["you"]
edition = "2024" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@ -9,10 +9,10 @@ use std::{
use eyre::bail; use eyre::bail;
use lanspread_client::{ClientCommand, ClientEvent}; use lanspread_client::{ClientCommand, ClientEvent};
use lanspread_db::db::{Game, GameDB}; use lanspread_db::db::{Game, GameDB};
use lanspread_mdns::{LANSPREAD_INSTANCE_NAME, LANSPREAD_SERVICE_TYPE, discover_service}; use lanspread_mdns::{discover_service, LANSPREAD_INSTANCE_NAME, LANSPREAD_SERVICE_TYPE};
use tauri::{AppHandle, Emitter as _, Manager}; use tauri::{AppHandle, Emitter as _, Manager};
use tauri_plugin_shell::{ShellExt, process::Command}; use tauri_plugin_shell::{process::Command, ShellExt};
use tokio::sync::{Mutex, mpsc::UnboundedSender}; use tokio::sync::{mpsc::UnboundedSender, Mutex};
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/

View File

@ -1,7 +1,7 @@
[package] [package]
name = "lanspread-utils" name = "lanspread-utils"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2021"
[lints.rust] [lints.rust]
unsafe_code = "forbid" unsafe_code = "forbid"

View File

@ -1,8 +1,6 @@
#[macro_export] #[macro_export]
macro_rules! maybe_addr { macro_rules! maybe_addr {
($addr:expr) => { ($addr:expr) => {
$addr.map_or(Arc::new("<unknown>".to_string()), |addr| { $addr.map_or("<unknown>".to_string(), |addr| addr.to_string())
Arc::new(addr.to_string())
})
}; };
} }