first commit

This commit is contained in:
eraden 2024-06-14 11:18:13 +02:00
commit b66b2cee57
10 changed files with 1936 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
Mods

1394
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

3
Cargo.toml Normal file
View File

@ -0,0 +1,3 @@
[workspace]
members = ["daemon", "mod_list", "server"]
resolver = "2"

0
README.md Normal file
View File

15
daemon/Cargo.toml Normal file
View File

@ -0,0 +1,15 @@
[package]
name = "daemon"
version = "0.1.0"
edition = "2021"
[dependencies]
directories = "5.0.1"
postcard = { version = "1.0.8", features = ["alloc", "use-std"] }
serde = "1.0.203"
tracing = "0.1.40"
tracing-subscriber = "0.3.18"
urlencoded = "0.6.0"
mod_list = { path = "../mod_list" }
tokio = { version = "1.38.0", features = ["full"] }
urlencoding = "2.1.3"

140
daemon/src/main.rs Normal file
View File

@ -0,0 +1,140 @@
use std::time::Duration;
use tokio::io::*;
use tokio::net::TcpStream;
use tokio::time::sleep;
use mod_list::*;
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
let server_address = if cfg!(debug_assertions) {
"0.0.0.0:50002"
} else {
"https://mods.7dtd.ita-prog.pl"
};
tracing::debug!("Server address: {server_address}");
let home = directories::UserDirs::new()
.expect("Failed to build user home directory resolver")
.home_dir()
.to_owned();
let game_dir = home.join(".local/share/Steam/steamapps/common/7 Days To Die");
let mods = game_dir.join("Mods");
let local_mod_list = ModList::new(vec![]);
std::fs::create_dir_all(&mods).expect("Failed to create mods directory");
let mut buffer = Vec::new();
loop {
tracing::info!("Checking for updates");
buffer.clear();
if let Err(e) = mod_list::refresh_mod_list(mods.clone(), local_mod_list.clone()).await {
tracing::error!("Failed to load mod list: {e}");
}
let mut connection = BufReader::new(TcpStream::connect(server_address).await.unwrap());
connection
.write_all(b"GET /sync HTTP/1.1\n")
.await
.expect("Failed to write request");
tracing::debug!("Request send /sync");
tracing::debug!("reading response");
{
let mut buffer = String::new();
connection
.read_line(&mut buffer)
.await
.expect("Response line not found");
connection
.read_line(&mut buffer)
.await
.expect("Server name not found");
connection
.read_line(&mut buffer)
.await
.expect("Empty line after response head not found");
}
let size = connection
.read_to_end(&mut buffer)
.await
.expect("Failed to read from server");
tracing::debug!("Bytes {buffer:?}");
tracing::debug!("Load {} bytes", buffer.len());
// TODO: remove
// std::fs::write("/tmp/c", &buffer).ok();
let remote_mod_list: Vec<ModInfo> =
postcard::from_bytes(&buffer[..size]).expect("Failed to deserialize remote mod list");
connection
.shutdown()
.await
.expect("Failed to close connection");
tracing::info!("Loaded mod list: {remote_mod_list:?}");
let remote_mod_list = ModList::new(remote_mod_list);
let diff = remote_mod_list.outdated_files(local_mod_list.clone()).await;
tracing::info!("Outdated files: {:?}", diff.len());
if !diff.is_empty() {
tracing::info!("Staring sync on files");
for f in diff {
buffer.clear();
let encoded = urlencoding::encode(&f);
let mut connection =
BufReader::new(TcpStream::connect(server_address).await.unwrap());
connection
.write_all(format!("GET /d?{encoded} HTTP/1.1\n").as_bytes())
.await
.expect("Failed to write request");
{
let mut buffer = String::new();
connection
.read_line(&mut buffer)
.await
.expect("Response line not found");
connection
.read_line(&mut buffer)
.await
.expect("Server name not found");
connection
.read_line(&mut buffer)
.await
.expect("Empty line after response head not found");
}
let size = connection
.read_to_end(&mut buffer)
.await
.expect("Failed to read from server");
tracing::debug!("Mod file bytes {buffer:?}");
tracing::debug!("Mod file {size} bytes");
let file_path = mods.join(&f);
tracing::debug!("Writing to: {file_path:?}");
if let Some(dir_path) = file_path.parent() {
tokio::fs::create_dir_all(dir_path)
.await
.expect("Failed to create mod file parent dir");
};
if let Err(e) = tokio::fs::write(file_path, &buffer).await {
tracing::warn!("Unable to save {f:?}: {e}");
break;
}
}
}
sleep(Duration::from_secs(60 * 60)).await;
}
}

10
mod_list/Cargo.toml Normal file
View File

@ -0,0 +1,10 @@
[package]
name = "mod_list"
version = "0.1.0"
edition = "2021"
[dependencies]
derive_more = "0.99.17"
serde = "1.0.203"
tokio = { version = "1.38.0", features = ["full"] }
tracing = "0.1.40"

161
mod_list/src/lib.rs Normal file
View File

@ -0,0 +1,161 @@
use serde::*;
use std::sync::Arc;
use std::{collections::HashMap, path::PathBuf};
use tokio::sync::RwLock;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ModFileInfo {
pub path: String,
pub ts: std::time::SystemTime,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ModInfo {
pub name: String,
pub ts: std::time::SystemTime,
pub files: Vec<ModFileInfo>,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct ModListInner {
pub mods: Vec<ModInfo>,
pub files_count: usize,
}
#[derive(Clone, Debug, Default, derive_more::Deref, derive_more::DerefMut)]
pub struct ModList(Arc<RwLock<ModListInner>>);
impl ModList {
pub fn new(mods: Vec<ModInfo>) -> Self {
Self(Arc::new(RwLock::new(ModListInner {
files_count: Self::count_files(&mods),
mods,
})))
}
fn count_files(mods: &[ModInfo]) -> usize {
mods.iter()
.fold(0, |agg, mod_info| agg + mod_info.files.len())
}
pub async fn exists(&self, file: &str) -> bool {
let l = self.0.read().await;
l.mods
.iter()
.any(|mod_info| mod_info.files.iter().any(|f| f.path == file))
}
pub async fn outdated_files(&self, local_mod_list: ModList) -> Vec<String> {
let remote_lock = self.0.read().await;
let remote_files = remote_lock.mods.iter().fold(
HashMap::with_capacity(remote_lock.files_count),
|mut agg, mod_info| {
mod_info.files.iter().for_each(|file| {
agg.insert(&file.path, file.ts);
});
agg
},
);
let local_lock = local_mod_list.0.read().await;
let local_files = local_lock.mods.iter().fold(
HashMap::with_capacity(local_lock.files_count),
|mut agg, mod_info| {
mod_info.files.iter().for_each(|file| {
agg.insert(&file.path, file.ts);
});
agg
},
);
remote_files
.iter()
.filter_map(|(remote_path, remote_date)| {
let Some(local_date) = local_files.get(remote_path) else {
return Some((*remote_path).clone());
};
if local_date < remote_date {
return Some((*remote_path).clone());
}
None
})
.collect::<Vec<_>>()
}
}
pub fn file_relative(dir: &str, path: PathBuf) -> String {
let path = path.display().to_string();
let (_, end) = path.split_at(dir.len() + 1);
end.to_string()
}
pub async fn refresh_mod_list(dir: PathBuf, mod_list: ModList) -> std::io::Result<()> {
tracing::debug!("refresh mod list: {dir:?}");
let mut list = Vec::with_capacity(1024);
let dir_string = dir.display().to_string();
for f in std::fs::read_dir(&dir)? {
let f = f?;
let meta = f.metadata()?;
if !meta.is_dir() {
continue;
}
let Some(name) = f.file_name().to_str().map(String::from) else {
continue;
};
let date = meta.modified()?;
let mut files = Vec::with_capacity(1024);
let file = ModFileInfo {
path: file_relative(&dir_string, f.path()),
ts: date,
};
files.push(file);
read_rec(&dir_string, f.path(), &mut files)?;
let newest = files
.iter()
.fold(None, |agg, f| match (agg, f.ts) {
(None, ts) => Some(ts),
(Some(prev), current) if prev < current => Some(current),
(prev, _) => prev,
})
.unwrap_or(std::time::UNIX_EPOCH);
list.push(ModInfo {
name,
ts: newest,
files,
});
}
tracing::debug!("Mods: {list:?}");
let mut lock = mod_list.0.write().await;
lock.files_count = ModList::count_files(&list);
lock.mods = list;
Ok(())
}
fn read_rec(dir: &str, path: PathBuf, res: &mut Vec<ModFileInfo>) -> std::io::Result<()> {
tracing::debug!("read rec: {path:?}");
for f in std::fs::read_dir(path)? {
let f = f?;
let meta = f.metadata()?;
if meta.is_dir() {
let date = meta.modified()?;
let file = ModFileInfo {
path: file_relative(dir, f.path()),
ts: date,
};
res.push(file);
read_rec(&dir, f.path(), res)?;
} else if meta.is_file() {
let date = meta.modified()?;
let file = ModFileInfo {
path: file_relative(dir, f.path()),
ts: date,
};
res.push(file);
}
}
Ok(())
}

15
server/Cargo.toml Normal file
View File

@ -0,0 +1,15 @@
[package]
name = "mods-server"
version = "0.1.0"
edition = "2021"
[dependencies]
futures-util = { version = "0.3.30", features = ["io"] }
heapless = { version = "0.8.0", features = ["serde"] }
postcard = { version = "1.0.8", features = ["alloc", "use-std"] }
serde = "1.0.203"
tracing = "0.1.40"
tracing-subscriber = "0.3.18"
urlencoding = "2.1.3"
mod_list = { path = "../mod_list" }
tokio = { version = "1.38.0", features = ["full"] }

196
server/src/main.rs Normal file
View File

@ -0,0 +1,196 @@
use mod_list::*;
use postcard::to_allocvec;
use std::path::PathBuf;
use std::time::Duration;
use tokio::io::*;
use tokio::net::{TcpListener, TcpStream};
use tokio::spawn;
use tokio::time::sleep;
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
let dir = PathBuf::new()
.join(std::env::var("7DTD_MODS_DIR").expect("7DTD_MODS_DIR must be provided"));
tracing::debug!("Mods directory: {dir:?}");
let mod_list = ModList::new(vec![]);
{
let mod_list = mod_list.clone();
let dir = dir.clone();
spawn(async move {
loop {
if let Err(e) = refresh_mod_list(dir.clone(), mod_list.clone()).await {
tracing::error!("read mod list: {e}");
}
sleep(Duration::from_secs(60 * 60)).await;
}
});
}
let listener = TcpListener::bind("127.0.0.1:50002").await.unwrap();
tracing::info!("listening");
loop {
let dir = dir.clone();
let mod_list = mod_list.clone();
let incoming = listener.accept().await;
match incoming {
Ok((stream, addr)) => {
println!("accepted a connection from {}", addr);
spawn(handle_request(dir.clone(), stream, mod_list.clone()));
}
Err(e) => {
println!("accepted connection failed: {}", e);
return;
}
}
}
}
async fn handle_request(dir: PathBuf, stream: TcpStream, mod_list: ModList) {
let mut stream = BufStream::new(stream);
tracing::debug!("Handler spawned");
if let Err(e) = try_request_handler(dir, &mut stream, mod_list).await {
tracing::error!("Failed to handle request: {e}");
}
if let Err(e) = stream.shutdown().await {
tracing::error!("Unable to close connection: {e}");
}
}
async fn try_request_handler(
dir: PathBuf,
stream: &mut BufStream<TcpStream>,
mod_list: ModList,
) -> std::io::Result<()> {
tracing::debug!("Attempt to handle request...");
let mut buf = String::new();
tracing::info!("Reading request body...");
let len = match stream.read_line(&mut buf).await {
Ok(len) => len,
Err(e) => {
tracing::error!("Failed to read from stream: {e}");
return Ok(());
}
};
tracing::info!("Reading body size is: {len}");
if len == 0 {
return Ok(());
}
let payload = buf;
// let Ok(payload) = std::str::from_utf8(&buf) else {
// tracing::info!("Invalid request: Not a string");
// return Ok(());
// };
tracing::debug!("payload: {}", payload);
let mut lines = payload.lines();
let Some(line) = lines.next() else {
tracing::info!("Invalid request: empty string");
return Ok(());
};
let mut parts = line
.split_whitespace()
.take(3)
.collect::<Vec<_>>()
.into_iter();
let Some(_method) = parts.next() else {
tracing::info!("Invalid request: no method");
return Ok(());
};
let Some(http_path) = parts.next() else {
tracing::info!("Invalid request: no http path");
return Ok(());
};
tracing::debug!("received {http_path:?} request");
let res = match http_path {
"/sync" => {
write_status(200, stream).await?;
send_mod_list(mod_list.clone(), stream).await
}
req if req.starts_with("/d?") => send_file(mod_list, dir, stream, req).await,
unknown => {
tracing::info!("Unknown {unknown:?}");
write_status(404, stream).await
}
};
if let Err(e) = res {
tracing::error!("Failed to handle req {http_path:?}: {e}");
}
Ok(())
}
async fn send_file(
mod_list: ModList,
dir: PathBuf,
stream: &mut BufStream<TcpStream>,
req: &str,
) -> std::io::Result<()> {
tracing::info!("Sending file for path {req:?}");
let (_, file) = req.split_once('?').unwrap();
tracing::debug!("File is: {file:?}");
let Ok(file) = urlencoding::decode(file) else {
tracing::debug!("Failed to decode file: {file}");
write_status(404, stream).await?;
return Ok(());
};
if !mod_list.exists(&*file).await {
write_status(404, stream).await?;
return Ok(());
}
let path = dir.join(&*file);
tracing::info!("Uploading file: {path:?}");
match tokio::fs::read(&path).await {
Err(e) => {
tracing::error!("Failed to load file {path:?}: {e}");
write_status(404, stream).await?;
}
Ok(buf) => {
write_status(200, stream).await?;
stream.write_all(&buf).await?;
tracing::info!("File uploaded successfully");
}
};
tracing::info!("File send finished");
Ok(())
}
async fn write_status(status: u16, stream: &mut BufStream<TcpStream>) -> std::io::Result<()> {
tracing::info!("Sending header...");
tracing::info!("Sending status: {status}");
let header = format!("HTTP/2 {status}\n");
// write all
stream.write_all(header.as_bytes()).await?;
stream.write_all(b"server: midnightflare\n").await?;
stream.write_all(b"\n").await?;
tracing::info!("Header finished");
Ok(())
}
async fn send_mod_list(
mod_list: ModList,
stream: &mut BufStream<TcpStream>,
) -> std::io::Result<()> {
tracing::info!("Sending mod list");
let list = mod_list.read().await;
let bytes: Vec<u8> = to_allocvec(&list.mods).unwrap();
tracing::debug!("Sending bytes {bytes:?}");
// TODO debug
std::fs::write("/tmp/s", &bytes).ok();
tracing::debug!("Buffer size is: {}", bytes.len());
// stream.write_u64(bytes.len() as u64).await?;
stream.write_all(&bytes).await?;
tracing::info!("Send successful");
Ok(())
}