diff --git a/README.md b/README.md index 6187960..1d57e34 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Server Manager - Next-Gen Media Server Orchestrator 🚀 -![Server Manager Banner](https://img.shields.io/badge/Status-Tested-brightgreen) ![Version](https://img.shields.io/badge/Version-1.0.7-blue) ![Rust](https://img.shields.io/badge/Built%20With-Rust-orange) ![Docker](https://img.shields.io/badge/Powered%20By-Docker-blue) +![Server Manager Banner](https://img.shields.io/badge/Status-Tested-brightgreen) ![Version](https://img.shields.io/badge/Version-1.0.8-blue) ![Rust](https://img.shields.io/badge/Built%20With-Rust-orange) ![Docker](https://img.shields.io/badge/Powered%20By-Docker-blue) **Server Manager** is a powerful and intelligent tool written in Rust to deploy, manage, and optimize a complete personal media and cloud server stack. It detects your hardware and automatically configures 28 Docker services for optimal performance. diff --git a/server_manager/Cargo.toml b/server_manager/Cargo.toml index 475f80e..59a6547 100644 --- a/server_manager/Cargo.toml +++ b/server_manager/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "server_manager" -version = "1.0.7" +version = "1.0.8" edition = "2021" [dependencies] diff --git a/server_manager/src/core/config.rs b/server_manager/src/core/config.rs index c57c2a7..562040e 100644 --- a/server_manager/src/core/config.rs +++ b/server_manager/src/core/config.rs @@ -3,10 +3,10 @@ use log::info; use serde::{Deserialize, Serialize}; use std::collections::HashSet; use std::fs; -use std::path::Path; use std::sync::OnceLock; use std::time::SystemTime; use tokio::sync::RwLock; +use crate::core::paths; #[derive(Debug, Clone)] struct CachedConfig { @@ -24,7 +24,7 @@ pub struct Config { impl Config { pub fn load() -> Result { - let path = Path::new("config.yaml"); + let path = paths::get_load_path("config.yaml"); if path.exists() { let content = fs::read_to_string(path).context("Failed to read config.yaml")?; // If empty file, return default @@ -45,12 +45,14 @@ impl Config { }) }); + let path = paths::get_load_path("config.yaml"); + // Fast path: Optimistic read { let guard = cache.read().await; if let Some(cached_mtime) = guard.last_mtime { // Check if file still matches - if let Ok(metadata) = tokio::fs::metadata("config.yaml").await { + if let Ok(metadata) = tokio::fs::metadata(&path).await { if let Ok(modified) = metadata.modified() { if modified == cached_mtime { return Ok(guard.config.clone()); @@ -64,7 +66,7 @@ impl Config { let mut guard = cache.write().await; // Check metadata again (double-checked locking pattern) - let metadata_res = tokio::fs::metadata("config.yaml").await; + let metadata_res = tokio::fs::metadata(&path).await; match metadata_res { Ok(metadata) => { @@ -77,7 +79,7 @@ impl Config { } // Load file - match tokio::fs::read_to_string("config.yaml").await { + match tokio::fs::read_to_string(&path).await { Ok(content) => { let config = if content.trim().is_empty() { Config::default() @@ -105,7 +107,8 @@ impl Config { pub fn save(&self) -> Result<()> { let content = serde_yaml_ng::to_string(self)?; - fs::write("config.yaml", content).context("Failed to write config.yaml")?; + let path = paths::get_save_path("config.yaml"); + fs::write(path, content).context("Failed to write config.yaml")?; Ok(()) } diff --git a/server_manager/src/core/hardware.rs b/server_manager/src/core/hardware.rs index 09f3ff2..9acbf31 100644 --- a/server_manager/src/core/hardware.rs +++ b/server_manager/src/core/hardware.rs @@ -31,6 +31,7 @@ impl HardwareInfo { sys.refresh_memory(); sys.refresh_cpu(); sys.refresh_disks_list(); + sys.refresh_disks(); let total_memory = sys.total_memory(); // Bytes let ram_gb = total_memory / 1024 / 1024 / 1024; diff --git a/server_manager/src/core/mod.rs b/server_manager/src/core/mod.rs index 9aa751b..8581869 100644 --- a/server_manager/src/core/mod.rs +++ b/server_manager/src/core/mod.rs @@ -6,3 +6,4 @@ pub mod hardware; pub mod secrets; pub mod system; pub mod users; +pub mod paths; diff --git a/server_manager/src/core/paths.rs b/server_manager/src/core/paths.rs new file mode 100644 index 0000000..c9d9282 --- /dev/null +++ b/server_manager/src/core/paths.rs @@ -0,0 +1,41 @@ +use std::path::{Path, PathBuf}; + +// This module handles standard paths for configuration, secrets, and users. +// It prioritizes /opt/server_manager if available, falling back to CWD for development. + +pub const OPT_DIR: &str = "/opt/server_manager"; + +/// Returns the path to load a file from. +/// Priority: /opt/server_manager/ > ./ +/// If /opt version exists, returns it. Otherwise returns ./ (even if it doesn't exist yet). +pub fn get_load_path(filename: &str) -> PathBuf { + let opt_path = Path::new(OPT_DIR).join(filename); + if opt_path.exists() { + opt_path + } else { + PathBuf::from(filename) + } +} + +/// Returns the path to save a file to. +/// Priority: /opt/server_manager/ (if /opt/server_manager dir exists) > ./ +pub fn get_save_path(filename: &str) -> PathBuf { + let opt_dir = Path::new(OPT_DIR); + if opt_dir.exists() && opt_dir.is_dir() { + opt_dir.join(filename) + } else { + PathBuf::from(filename) + } +} + +pub fn get_config_path() -> PathBuf { + get_load_path("config.yaml") +} + +pub fn get_users_path() -> PathBuf { + get_load_path("users.yaml") +} + +pub fn get_secrets_path() -> PathBuf { + get_load_path("secrets.yaml") +} diff --git a/server_manager/src/core/secrets.rs b/server_manager/src/core/secrets.rs index 5908693..defe765 100644 --- a/server_manager/src/core/secrets.rs +++ b/server_manager/src/core/secrets.rs @@ -3,7 +3,7 @@ use log::info; use serde::{Deserialize, Serialize}; use rand::RngExt; use std::fs; -use std::path::Path; +use crate::core::paths; #[derive(Serialize, Deserialize, Default, Clone)] pub struct Secrets { @@ -21,9 +21,9 @@ pub struct Secrets { impl Secrets { pub fn load_or_create() -> Result { - let path = Path::new("secrets.yaml"); + let path = paths::get_secrets_path(); let mut secrets: Secrets = if path.exists() { - let content = fs::read_to_string(path).context("Failed to read secrets.yaml")?; + let content = fs::read_to_string(&path).context("Failed to read secrets.yaml")?; serde_yaml_ng::from_str(&content).context("Failed to parse secrets.yaml")? } else { Secrets::default() @@ -74,7 +74,8 @@ impl Secrets { if changed { info!("Generated new secrets."); let content = serde_yaml_ng::to_string(&secrets)?; - fs::write(path, content).context("Failed to write secrets.yaml")?; + let save_path = paths::get_save_path("secrets.yaml"); + fs::write(save_path, content).context("Failed to write secrets.yaml")?; } Ok(secrets) diff --git a/server_manager/src/core/users.rs b/server_manager/src/core/users.rs index 41870d0..44d4ba9 100644 --- a/server_manager/src/core/users.rs +++ b/server_manager/src/core/users.rs @@ -6,7 +6,7 @@ use nix::unistd::Uid; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::fs; -use std::path::Path; +use crate::core::paths; #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub enum Role { @@ -34,22 +34,10 @@ impl UserManager { } pub fn load() -> Result { - // Try CWD or /opt/server_manager - let path = Path::new("users.yaml"); - let fallback_path = Path::new("/opt/server_manager/users.yaml"); - - // Priority: /opt/server_manager/users.yaml > ./users.yaml - // This aligns with save() behavior which prefers /opt if available. - let load_path = if fallback_path.exists() { - Some(fallback_path) - } else if path.exists() { - Some(path) - } else { - None - }; + let path = paths::get_users_path(); - let mut manager = if let Some(p) = load_path { - let content = fs::read_to_string(p).context("Failed to read users.yaml")?; + let mut manager = if path.exists() { + let content = fs::read_to_string(path).context("Failed to read users.yaml")?; if content.trim().is_empty() { UserManager::default() } else { @@ -88,15 +76,8 @@ impl UserManager { } pub fn save(&self) -> Result<()> { - // Prefer saving to /opt/server_manager if it exists/is writable, else CWD - let target = if Path::new("/opt/server_manager").exists() { - Path::new("/opt/server_manager/users.yaml") - } else { - Path::new("users.yaml") - }; - let content = serde_yaml_ng::to_string(self)?; - fs::write(target, content).context("Failed to write users.yaml")?; + fs::write(paths::get_save_path("users.yaml"), content).context("Failed to write users.yaml")?; Ok(()) } diff --git a/server_manager/src/interface/web.rs b/server_manager/src/interface/web.rs index 1f677c3..9369f0a 100644 --- a/server_manager/src/interface/web.rs +++ b/server_manager/src/interface/web.rs @@ -1,4 +1,5 @@ use crate::core::config::Config; +use crate::core::paths; use crate::core::users::{Role, UserManager}; use crate::services; use axum::{ @@ -49,15 +50,17 @@ type SharedState = Arc; impl AppState { async fn get_config(&self) -> Config { + let path = paths::get_config_path(); + // Fast path: check metadata - let current_mtime = tokio::fs::metadata("config.yaml") + let current_mtime = tokio::fs::metadata(&path) .await .and_then(|m| m.modified()) .ok(); { let cache = self.config_cache.read().await; - if cache.last_modified == current_mtime { + if cache.last_modified == current_mtime && current_mtime.is_some() { return cache.config.clone(); } } @@ -65,13 +68,13 @@ impl AppState { // Slow path: reload let mut cache = self.config_cache.write().await; - // Re-check mtime under write lock to avoid race - let current_mtime_2 = tokio::fs::metadata("config.yaml") + // Re-check mtime under write lock + let current_mtime_2 = tokio::fs::metadata(&path) .await .and_then(|m| m.modified()) .ok(); - if cache.last_modified == current_mtime_2 { + if cache.last_modified == current_mtime_2 && current_mtime_2.is_some() { return cache.config.clone(); } @@ -84,20 +87,17 @@ impl AppState { } async fn get_users(&self) -> UserManager { - // Determine path logic (matches UserManager::load) - let path = std::path::Path::new("users.yaml"); - let fallback_path = std::path::Path::new("/opt/server_manager/users.yaml"); - let file_path = if path.exists() { path } else { fallback_path }; + let path = paths::get_users_path(); // Fast path: check metadata - let current_mtime = tokio::fs::metadata(file_path).await + let current_mtime = tokio::fs::metadata(&path).await .and_then(|m| m.modified()) .ok(); { let cache = self.users_cache.read().await; // If mtime matches (or both None), return cached - if cache.last_modified == current_mtime { + if cache.last_modified == current_mtime && current_mtime.is_some() { return cache.manager.clone(); } } @@ -106,11 +106,11 @@ impl AppState { let mut cache = self.users_cache.write().await; // Re-check mtime under write lock - let current_mtime_2 = tokio::fs::metadata(file_path).await + let current_mtime_2 = tokio::fs::metadata(&path).await .and_then(|m| m.modified()) .ok(); - if cache.last_modified == current_mtime_2 { + if cache.last_modified == current_mtime_2 && current_mtime_2.is_some() { return cache.manager.clone(); } @@ -135,19 +135,14 @@ pub async fn start_server(port: u16) -> anyhow::Result<()> { sys.refresh_all(); let initial_config = Config::load().unwrap_or_default(); - let initial_config_mtime = std::fs::metadata("config.yaml") + let initial_config_mtime = std::fs::metadata(paths::get_config_path()) .ok() .and_then(|m| m.modified().ok()); let initial_users = UserManager::load().unwrap_or_default(); - let initial_users_mtime = std::fs::metadata("users.yaml") + let initial_users_mtime = std::fs::metadata(paths::get_users_path()) .ok() - .and_then(|m| m.modified().ok()) - .or_else(|| { - std::fs::metadata("/opt/server_manager/users.yaml") - .ok() - .and_then(|m| m.modified().ok()) - }); + .and_then(|m| m.modified().ok()); let app_state = Arc::new(AppState { system: Mutex::new(sys), @@ -637,10 +632,8 @@ async fn add_user_handler(State(state): State, session: Session, Fo } else { info!("User {} added via Web UI by {}", payload.username, session_user.username); // Update mtime to prevent unnecessary reload - let path = std::path::Path::new("users.yaml"); - let fallback_path = std::path::Path::new("/opt/server_manager/users.yaml"); - let file_path = if path.exists() { path } else { fallback_path }; - if let Ok(m) = std::fs::metadata(file_path) { + let path = paths::get_save_path("users.yaml"); + if let Ok(m) = tokio::fs::metadata(path).await { cache.last_modified = m.modified().ok(); } } @@ -668,10 +661,8 @@ async fn delete_user_handler(State(state): State, session: Session, } else { info!("User {} deleted via Web UI by {}", username, session_user.username); // Update mtime to prevent unnecessary reload - let path = std::path::Path::new("users.yaml"); - let fallback_path = std::path::Path::new("/opt/server_manager/users.yaml"); - let file_path = if path.exists() { path } else { fallback_path }; - if let Ok(m) = std::fs::metadata(file_path) { + let path = paths::get_save_path("users.yaml"); + if let Ok(m) = tokio::fs::metadata(path).await { cache.last_modified = m.modified().ok(); } } diff --git a/server_manager/src/lib.rs b/server_manager/src/lib.rs index 7734e8d..2eeb2ca 100644 --- a/server_manager/src/lib.rs +++ b/server_manager/src/lib.rs @@ -135,7 +135,7 @@ pub fn build_compose_structure( let service = Service { image: service_impl.image().to_string(), - container_name: service_impl.name().to_string(), + container_name: name.clone(), restart: "unless-stopped".to_string(), ports, environment,