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
4 changes: 3 additions & 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 Expand Up @@ -90,6 +90,7 @@ The tool provides several subcommands:
* `server_manager status`: Displays detected hardware statistics and the profile (Low/Standard/High).
* `server_manager enable <service>`: Enable a service (e.g., `server_manager enable nextcloud`).
* `server_manager disable <service>`: Disable a service.
* `server_manager apply`: Apply current configuration (re-generate and deploy).
* `server_manager web`: Starts the Web Administration Interface (Default: http://0.0.0.0:8099).
* `server_manager user add <username> --quota <GB>`: Create a new user (Role: Admin/Observer) and set storage quota.
* `server_manager user delete <username>`: Delete a user and their data.
Expand Down Expand Up @@ -244,6 +245,7 @@ L'outil dispose de plusieurs sous-commandes :
* `server_manager status` : Affiche les statistiques matérielles détectées et le profil (Low/Standard/High).
* `server_manager enable <service>` : Active un service (ex: `server_manager enable nextcloud`).
* `server_manager disable <service>` : Désactive un service.
* `server_manager apply` : Applique la configuration actuelle (re-génère et déploie).
* `server_manager web` : Démarre l'Interface d'Administration Web (Défaut : http://0.0.0.0:8099).
* `server_manager user add <username> --quota <GB>` : Crée un nouvel utilisateur (Rôle : Admin/Observer) et définit un quota de stockage.
* `server_manager user delete <username>` : Supprime un utilisateur et ses données.
Expand Down
4 changes: 3 additions & 1 deletion server_manager/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions 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 All @@ -26,7 +26,7 @@ time = "0.3"
rand = "0.10.0"

[dev-dependencies]
criterion = "0.5"
criterion = { version = "0.5", features = ["async_tokio"] }

[[bench]]
name = "service_benchmark"
Expand Down
70 changes: 70 additions & 0 deletions server_manager/src/core/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,4 +124,74 @@ impl Config {
info!("Disabled service: {}", service_name);
}
}

pub async fn enable_service_async(service_name: &str) -> Result<()> {
Self::update_service_async(service_name, true).await
}

pub async fn disable_service_async(service_name: &str) -> Result<()> {
Self::update_service_async(service_name, false).await
}

async fn update_service_async(service_name: &str, enable: bool) -> Result<()> {
let cache = CONFIG_CACHE.get_or_init(|| {
RwLock::new(CachedConfig {
config: Config::default(),
last_mtime: None,
})
});

let mut guard = cache.write().await;

// Ensure we have the latest config before modifying
let metadata_res = tokio::fs::metadata("config.yaml").await;
match metadata_res {
Ok(metadata) => {
let modified = metadata.modified().unwrap_or(SystemTime::now());
if guard.last_mtime != Some(modified) {
// Reload
match tokio::fs::read_to_string("config.yaml").await {
Ok(content) => {
if !content.trim().is_empty() {
guard.config = serde_yaml_ng::from_str(&content)
.context("Failed to parse config.yaml")?;
}
guard.last_mtime = Some(modified);
}
Err(_) => {
// If read fails, maybe use current? Or fail?
// Proceeding might overwrite file. Best to fail or assume current.
// Given we verified metadata exists, read failure is rare.
}
}
}
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
// File missing, use default (which is empty/default config)
// But we want to preserve what we have if we have something?
// If file deleted, we probably should start fresh or recreating it is fine.
}
Err(_) => {}
}

// Modify
if enable {
guard.config.enable_service(service_name);
} else {
guard.config.disable_service(service_name);
}

// Save
let content = serde_yaml_ng::to_string(&guard.config)?;
tokio::fs::write("config.yaml", &content)
.await
.context("Failed to write config.yaml")?;

// Update mtime
if let Ok(metadata) = tokio::fs::metadata("config.yaml").await {
guard.last_mtime = metadata.modified().ok();
}

Ok(())
}
}
175 changes: 174 additions & 1 deletion server_manager/src/core/users.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@ use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::Path;
use std::sync::OnceLock;
use std::time::SystemTime;
use tokio::sync::RwLock;

#[derive(Clone)]
struct CachedUsers {
manager: UserManager,
last_mtime: Option<SystemTime>,
}

static USERS_CACHE: OnceLock<RwLock<CachedUsers>> = OnceLock::new();

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub enum Role {
Expand All @@ -30,7 +41,61 @@ pub struct UserManager {

impl UserManager {
pub async fn load_async() -> Result<Self> {
tokio::task::spawn_blocking(Self::load).await?
let cache = USERS_CACHE.get_or_init(|| {
RwLock::new(CachedUsers {
manager: UserManager::default(),
last_mtime: None,
})
});

// Determine path asynchronously
let path = Path::new("users.yaml");
let fallback_path = Path::new("/opt/server_manager/users.yaml");
let target_path = if tokio::fs::try_exists(fallback_path).await.unwrap_or(false) {
fallback_path
} else {
path
};

// Fast path
{
let guard = cache.read().await;
if let Some(cached_mtime) = guard.last_mtime {
if let Ok(metadata) = tokio::fs::metadata(target_path).await {
if let Ok(modified) = metadata.modified() {
if modified == cached_mtime {
return Ok(guard.manager.clone());
}
}
}
}
}

// Slow path
let mut guard = cache.write().await;
// Re-check mtime
if let Ok(metadata) = tokio::fs::metadata(target_path).await {
let modified = metadata.modified().unwrap_or(SystemTime::now());
if guard.last_mtime == Some(modified) {
return Ok(guard.manager.clone());
}

// Reload via spawn_blocking because load() might do synchronous writes (save default admin)
// or reads.
let manager = tokio::task::spawn_blocking(Self::load).await??;
guard.manager = manager.clone();
guard.last_mtime = Some(modified);
Ok(manager)
} else {
// File might not exist, try loading anyway (it handles defaults)
let manager = tokio::task::spawn_blocking(Self::load).await??;
guard.manager = manager.clone();
// Try to get mtime again if created
if let Ok(metadata) = tokio::fs::metadata(target_path).await {
guard.last_mtime = metadata.modified().ok();
}
Ok(manager)
}
}

pub fn load() -> Result<Self> {
Expand Down Expand Up @@ -212,6 +277,114 @@ impl UserManager {
pub fn list_users(&self) -> Vec<&User> {
self.users.values().collect()
}

pub async fn add_user_async(
username: String,
password: String,
role: Role,
quota_gb: Option<u64>,
) -> Result<()> {
let cache = USERS_CACHE.get_or_init(|| {
RwLock::new(CachedUsers {
manager: UserManager::default(),
last_mtime: None,
})
});

let mut guard = cache.write().await;

// Ensure fresh before modifying (simple reload check)
// We reuse load_async logic partially or just trust the next load will refresh?
// Better to refresh now to avoid overwriting changes made by others.
// But since we have write lock, others are blocked.
// We just need to check if file changed since we last loaded.

// Simpler: Just rely on load()'s path logic and reload if needed?
// Or duplicate the "ensure fresh" logic.
// Given complexity, let's just reload strictly if file exists.

let path = Path::new("users.yaml");
let fallback_path = Path::new("/opt/server_manager/users.yaml");
let target_path = if tokio::fs::try_exists(fallback_path).await.unwrap_or(false) {
fallback_path
} else {
path
};

if let Ok(metadata) = tokio::fs::metadata(target_path).await {
let modified = metadata.modified().unwrap_or(SystemTime::now());
if guard.last_mtime != Some(modified) {
if let Ok(m) = tokio::task::spawn_blocking(Self::load).await? {
guard.manager = m;
guard.last_mtime = Some(modified);
}
}
}

// Now modify
let mut manager = guard.manager.clone();

let manager = tokio::task::spawn_blocking(move || {
manager.add_user(&username, &password, role, quota_gb)?;
Ok::<_, anyhow::Error>(manager)
})
.await??;

guard.manager = manager;

// Update mtime
if let Ok(metadata) = tokio::fs::metadata(target_path).await {
guard.last_mtime = metadata.modified().ok();
}

Ok(())
}

pub async fn delete_user_async(username: String) -> Result<()> {
let cache = USERS_CACHE.get_or_init(|| {
RwLock::new(CachedUsers {
manager: UserManager::default(),
last_mtime: None,
})
});

let mut guard = cache.write().await;

// Reload check (same as add)
let path = Path::new("users.yaml");
let fallback_path = Path::new("/opt/server_manager/users.yaml");
let target_path = if tokio::fs::try_exists(fallback_path).await.unwrap_or(false) {
fallback_path
} else {
path
};

if let Ok(metadata) = tokio::fs::metadata(target_path).await {
let modified = metadata.modified().unwrap_or(SystemTime::now());
if guard.last_mtime != Some(modified) {
if let Ok(m) = tokio::task::spawn_blocking(Self::load).await? {
guard.manager = m;
guard.last_mtime = Some(modified);
}
}
}

let mut manager = guard.manager.clone();

let manager = tokio::task::spawn_blocking(move || {
manager.delete_user(&username)?;
Ok::<_, anyhow::Error>(manager)
})
.await??;

guard.manager = manager;

if let Ok(metadata) = tokio::fs::metadata(target_path).await {
guard.last_mtime = metadata.modified().ok();
}

Ok(())
}
}

#[cfg(test)]
Expand Down
Loading