first commit
This commit is contained in:
commit
b66b2cee57
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/target
|
||||
Mods
|
1394
Cargo.lock
generated
Normal file
1394
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
3
Cargo.toml
Normal file
3
Cargo.toml
Normal file
@ -0,0 +1,3 @@
|
||||
[workspace]
|
||||
members = ["daemon", "mod_list", "server"]
|
||||
resolver = "2"
|
15
daemon/Cargo.toml
Normal file
15
daemon/Cargo.toml
Normal 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
140
daemon/src/main.rs
Normal 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
10
mod_list/Cargo.toml
Normal 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
161
mod_list/src/lib.rs
Normal 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
15
server/Cargo.toml
Normal 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
196
server/src/main.rs
Normal 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(())
|
||||
}
|
Loading…
Reference in New Issue
Block a user