Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down
2 changes: 1 addition & 1 deletion server_manager/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "server_manager"
version = "1.0.7"
version = "1.0.8"
edition = "2021"

[dependencies]
Expand Down
15 changes: 9 additions & 6 deletions server_manager/src/core/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -24,7 +24,7 @@ pub struct Config {

impl Config {
pub fn load() -> Result<Self> {
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
Expand All @@ -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());
Expand All @@ -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) => {
Expand All @@ -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()
Expand Down Expand Up @@ -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(())
}

Expand Down
1 change: 1 addition & 0 deletions server_manager/src/core/hardware.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions server_manager/src/core/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ pub mod hardware;
pub mod secrets;
pub mod system;
pub mod users;
pub mod paths;
41 changes: 41 additions & 0 deletions server_manager/src/core/paths.rs
Original file line number Diff line number Diff line change
@@ -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/<filename> > ./<filename>
/// If /opt version exists, returns it. Otherwise returns ./<filename> (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/<filename> (if /opt/server_manager dir exists) > ./<filename>
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")
}
9 changes: 5 additions & 4 deletions server_manager/src/core/secrets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -21,9 +21,9 @@ pub struct Secrets {

impl Secrets {
pub fn load_or_create() -> Result<Self> {
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()
Expand Down Expand Up @@ -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)
Expand Down
29 changes: 5 additions & 24 deletions server_manager/src/core/users.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -34,22 +34,10 @@ impl UserManager {
}

pub fn load() -> Result<Self> {
// 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 {
Expand Down Expand Up @@ -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(())
}

Expand Down
49 changes: 20 additions & 29 deletions server_manager/src/interface/web.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::core::config::Config;
use crate::core::paths;
use crate::core::users::{Role, UserManager};
use crate::services;
use axum::{
Expand Down Expand Up @@ -49,29 +50,31 @@ type SharedState = Arc<AppState>;

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();
}
}

// 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();
}

Expand All @@ -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();
}
}
Expand All @@ -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();
}

Expand All @@ -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),
Expand Down Expand Up @@ -637,10 +632,8 @@ async fn add_user_handler(State(state): State<SharedState>, 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();
}
}
Expand Down Expand Up @@ -668,10 +661,8 @@ async fn delete_user_handler(State(state): State<SharedState>, 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();
}
}
Expand Down
2 changes: 1 addition & 1 deletion server_manager/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down