Compare commits

...

3 Commits

Author SHA1 Message Date
adf6f9d757
[code] improve structure (focus: server) 2025-03-02 13:06:18 +01:00
bcf9ad68ad
[deps] cargo update 2025-03-02 13:05:35 +01:00
b21091c247
[code] edition 2024 2025-03-02 13:05:01 +01:00
20 changed files with 1415 additions and 1177 deletions

1811
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.13" itertools = "0.14"
log = "0.4" log = "0.4"
mdns-sd = "0.12" mdns-sd = "0.13"
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 = "2021" edition = "2024"
[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::Connect, provider::limits::Limits, Client as QuicClient, Connection}; use s2n_quic::{Client as QuicClient, Connection, client::Connect, provider::limits::Limits};
use tokio::{ use tokio::{
io::AsyncWriteExt, io::AsyncWriteExt,
sync::{ sync::{
mpsc::{UnboundedReceiver, UnboundedSender},
Mutex, Mutex,
mpsc::{UnboundedReceiver, UnboundedSender},
}, },
}; };
@ -72,9 +72,7 @@ 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() 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::from_secs(1))?;
let client = QuicClient::builder() let client = QuicClient::builder()
.with_tls(CERT_PEM)? .with_tls(CERT_PEM)?
@ -173,7 +171,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::from_secs(1))?; .with_max_idle_timeout(Duration::ZERO)?;
let client = QuicClient::builder() let client = QuicClient::builder()
.with_tls(CERT_PEM)? .with_tls(CERT_PEM)?

View File

@ -1,9 +1,12 @@
[package] [package]
name = "lanspread-compat" name = "lanspread-compat"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2024"
[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,5 +1,6 @@
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;
@ -45,3 +46,21 @@ 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 = "2021" edition = "2024"
[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}; use std::{collections::HashMap, fmt, path::Path};
use bytes::Bytes; use bytes::Bytes;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -93,6 +93,17 @@ 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 = "2021" edition = "2024"
[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 = "2021" edition = "2024"
[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 = "2021" edition = "2024"
[lints.rust] [lints.rust]
unsafe_code = "forbid" unsafe_code = "forbid"

View File

@ -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<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

@ -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,
}

View File

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

@ -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<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

@ -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<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 = "2021" edition = "2024"
# 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::{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::{AppHandle, Emitter as _, Manager};
use tauri_plugin_shell::{process::Command, ShellExt}; use tauri_plugin_shell::{ShellExt, process::Command};
use tokio::sync::{mpsc::UnboundedSender, Mutex}; use tokio::sync::{Mutex, mpsc::UnboundedSender};
// 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 = "2021" edition = "2024"
[lints.rust] [lints.rust]
unsafe_code = "forbid" unsafe_code = "forbid"

View File

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