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"] }
clap = { version = "4.5", features = ["derive"] }
eyre = "0.6"
itertools = "0.13"
itertools = "0.14"
log = "0.4"
mdns-sd = "0.12"
mdns-sd = "0.13"
s2n-quic = { version = "1.51", features = ["provider-event-tracing"] }
semver = "1.0"
serde = { version = "1.0", features = ["derive"] }

View File

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

View File

@ -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<ClientEvent>,
) -> 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)?

View File

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

View File

@ -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<Vec<EtiGame>> {
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]
name = "lanspread-db"
version = "0.1.0"
edition = "2021"
edition = "2024"
[lints.rust]
unsafe_code = "forbid"

View File

@ -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<S>(&self, id: S) -> Option<&Game>
where

View File

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

View File

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

View File

@ -1,7 +1,7 @@
[package]
name = "lanspread-server"
version = "0.1.0"
edition = "2021"
edition = "2024"
[lints.rust]
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::{
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<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 {
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
);
}
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;
}
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(())
}
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",
)
})
}
#[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)
async fn prepare_game_db(cli: &Cli) -> eyre::Result<GameDB> {
// build games from ETI database
let mut games: Vec<Game> = eti::get_games(&cli.db)
.await?
.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(),
};
.map(Into::into)
.collect();
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);
}
None => {
tracing::error!("Failed to get relative path: {relative_path:?}",);
}
},
Err(e) => {
tracing::error!("Failed to get relative path: {e}");
}
}
}
let mut game_db = GameDB::from(games);
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)
}
}
}
}
game_db.add_thumbnails(&cli.thumbs_dir);
#[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,
}
tracing::info!("Prepared game database with {} games", game_db.games.len());
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!(
assert!(
cli.game_dir.exists(),
"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 mut games: Vec<Game> = eti_games.into_iter().map(eti_game_to_game).collect();
// 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
});
let thumbnails = Thumbnails::new(cli.thumbnails);
// 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
let server_addr = SocketAddr::from((cli.ip, cli.port));
// spawn mDNS listener task
spawn_mdns_task(server_addr)?;
let game_db = prepare_game_db(&cli).await?;
tracing::info!("Server listening on {server_addr}");
run_server(server_addr, game_db, cli.game_dir).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"
description = "A Tauri App"
authors = ["you"]
edition = "2021"
edition = "2024"
# 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 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/

View File

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

View File

@ -1,6 +1,8 @@
#[macro_export]
macro_rules! maybe_addr {
($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())
})
};
}