fix: replace sqlx macros with non-macro queries, drop axum_extra dep, fix unused imports
This commit is contained in:
+15
-19
@@ -1,10 +1,8 @@
|
|||||||
use axum::{
|
use axum::{
|
||||||
async_trait,
|
async_trait,
|
||||||
extract::FromRequestParts,
|
extract::FromRequestParts,
|
||||||
http::{request::Parts, HeaderMap},
|
http::request::Parts,
|
||||||
RequestPartsExt,
|
|
||||||
};
|
};
|
||||||
use axum_extra::TypedHeader;
|
|
||||||
use chrono::{Duration, Utc};
|
use chrono::{Duration, Utc};
|
||||||
use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation};
|
use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@@ -16,11 +14,11 @@ use crate::state::AppState;
|
|||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct Claims {
|
pub struct Claims {
|
||||||
pub sub: String, // username
|
pub sub: String, // username
|
||||||
pub user_id: i64,
|
pub user_id: i64,
|
||||||
pub role: String, // "admin" | "user"
|
pub role: String, // "admin" | "user"
|
||||||
pub exp: i64, // unix timestamp
|
pub exp: i64,
|
||||||
pub iat: i64,
|
pub iat: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Claims {
|
impl Claims {
|
||||||
@@ -33,25 +31,24 @@ impl Claims {
|
|||||||
exp: (now + Duration::hours(8)).timestamp(),
|
exp: (now + Duration::hours(8)).timestamp(),
|
||||||
iat: now.timestamp(),
|
iat: now.timestamp(),
|
||||||
};
|
};
|
||||||
let token = encode(
|
Ok(encode(
|
||||||
&Header::default(),
|
&Header::default(),
|
||||||
&claims,
|
&claims,
|
||||||
&EncodingKey::from_secret(secret.as_bytes()),
|
&EncodingKey::from_secret(secret.as_bytes()),
|
||||||
)?;
|
)?)
|
||||||
Ok(token)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn verify(token: &str, secret: &str) -> Result<Claims, jsonwebtoken::errors::Error> {
|
pub fn verify(token: &str, secret: &str) -> Result<Claims, jsonwebtoken::errors::Error> {
|
||||||
let data = decode::<Claims>(
|
Ok(decode::<Claims>(
|
||||||
token,
|
token,
|
||||||
&DecodingKey::from_secret(secret.as_bytes()),
|
&DecodingKey::from_secret(secret.as_bytes()),
|
||||||
&Validation::new(Algorithm::HS256),
|
&Validation::new(Algorithm::HS256),
|
||||||
)?;
|
)?
|
||||||
Ok(data.claims)
|
.claims)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Extractor: pulls Claims from Authorization: Bearer <token> ──────── */
|
/* ── Extractor — pulls Claims from Authorization: Bearer <token> ─────── */
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl FromRequestParts<AppState> for Claims {
|
impl FromRequestParts<AppState> for Claims {
|
||||||
@@ -61,14 +58,13 @@ impl FromRequestParts<AppState> for Claims {
|
|||||||
parts: &mut Parts,
|
parts: &mut Parts,
|
||||||
state: &AppState,
|
state: &AppState,
|
||||||
) -> Result<Self, Self::Rejection> {
|
) -> Result<Self, Self::Rejection> {
|
||||||
let headers: &HeaderMap = &parts.headers;
|
let auth = parts
|
||||||
let auth = headers
|
.headers
|
||||||
.get("authorization")
|
.get("authorization")
|
||||||
.and_then(|v| v.to_str().ok())
|
.and_then(|v| v.to_str().ok())
|
||||||
.and_then(|s| s.strip_prefix("Bearer "))
|
.and_then(|s| s.strip_prefix("Bearer "))
|
||||||
.ok_or(AppError::Unauthorized)?;
|
.ok_or(AppError::Unauthorized)?;
|
||||||
|
|
||||||
Claims::verify(auth, &state.config.jwt_secret)
|
Claims::verify(auth, &state.config.jwt_secret).map_err(|_| AppError::Unauthorized)
|
||||||
.map_err(|_| AppError::Unauthorized)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,10 +23,10 @@ pub enum AppError {
|
|||||||
impl IntoResponse for AppError {
|
impl IntoResponse for AppError {
|
||||||
fn into_response(self) -> Response {
|
fn into_response(self) -> Response {
|
||||||
let (status, message) = match &self {
|
let (status, message) = match &self {
|
||||||
AppError::Unauthorized => (StatusCode::UNAUTHORIZED, self.to_string()),
|
AppError::Unauthorized => (StatusCode::UNAUTHORIZED, self.to_string()),
|
||||||
AppError::NotFound => (StatusCode::NOT_FOUND, self.to_string()),
|
AppError::NotFound => (StatusCode::NOT_FOUND, self.to_string()),
|
||||||
AppError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg.clone()),
|
AppError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg.clone()),
|
||||||
AppError::Internal(_) => {
|
AppError::Internal(_) => {
|
||||||
tracing::error!("{}", self);
|
tracing::error!("{}", self);
|
||||||
(StatusCode::INTERNAL_SERVER_ERROR, "Internal server error".into())
|
(StatusCode::INTERNAL_SERVER_ERROR, "Internal server error".into())
|
||||||
}
|
}
|
||||||
@@ -42,7 +42,7 @@ impl From<sqlx::Error> for AppError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl From<jsonwebtoken::errors::Error> for AppError {
|
impl From<jsonwebtoken::errors::Error> for AppError {
|
||||||
fn from(e: jsonwebtoken::errors::Error) -> Self {
|
fn from(_e: jsonwebtoken::errors::Error) -> Self {
|
||||||
AppError::Unauthorized
|
AppError::Unauthorized
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,15 +6,15 @@ use crate::{auth::Claims, error::AppError, state::AppState};
|
|||||||
|
|
||||||
#[derive(Serialize, sqlx::FromRow)]
|
#[derive(Serialize, sqlx::FromRow)]
|
||||||
pub struct Account {
|
pub struct Account {
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
pub username: String,
|
pub username: String,
|
||||||
pub domain: String,
|
pub domain: String,
|
||||||
pub email: String,
|
pub email: String,
|
||||||
pub package_name: String,
|
pub package_name: String,
|
||||||
pub disk_quota_mb: i64,
|
pub disk_quota_mb: i64,
|
||||||
pub bandwidth_mb: i64,
|
pub bandwidth_mb: i64,
|
||||||
pub status: String,
|
pub status: String,
|
||||||
pub created_at: NaiveDateTime,
|
pub created_at: NaiveDateTime,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
@@ -29,13 +29,12 @@ pub struct CreateAccountRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list_accounts(
|
pub async fn list_accounts(
|
||||||
claims: Claims,
|
_claims: Claims,
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
) -> Result<Json<Vec<Account>>, AppError> {
|
) -> Result<Json<Vec<Account>>, AppError> {
|
||||||
let accounts = sqlx::query_as!(
|
let accounts = sqlx::query_as::<_, Account>(
|
||||||
Account,
|
|
||||||
"SELECT id, username, domain, email, package_name, disk_quota_mb, bandwidth_mb, status, created_at
|
"SELECT id, username, domain, email, package_name, disk_quota_mb, bandwidth_mb, status, created_at
|
||||||
FROM accounts ORDER BY created_at DESC"
|
FROM accounts ORDER BY created_at DESC",
|
||||||
)
|
)
|
||||||
.fetch_all(&state.db)
|
.fetch_all(&state.db)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -43,7 +42,7 @@ pub async fn list_accounts(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn create_account(
|
pub async fn create_account(
|
||||||
claims: Claims,
|
_claims: Claims,
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Json(body): Json<CreateAccountRequest>,
|
Json(body): Json<CreateAccountRequest>,
|
||||||
) -> Result<Json<Account>, AppError> {
|
) -> Result<Json<Account>, AppError> {
|
||||||
@@ -52,61 +51,60 @@ pub async fn create_account(
|
|||||||
Argon2,
|
Argon2,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Validate
|
|
||||||
if body.username.is_empty() || body.domain.is_empty() {
|
if body.username.is_empty() || body.domain.is_empty() {
|
||||||
return Err(AppError::BadRequest("username and domain are required".into()));
|
return Err(AppError::BadRequest("username and domain are required".into()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hash password
|
let salt = SaltString::generate(&mut OsRng);
|
||||||
let salt = SaltString::generate(&mut OsRng);
|
let hash = Argon2::default()
|
||||||
let hash = Argon2::default()
|
|
||||||
.hash_password(body.password.as_bytes(), &salt)
|
.hash_password(body.password.as_bytes(), &salt)
|
||||||
.map_err(|e| AppError::Internal(anyhow::anyhow!(e)))?
|
.map_err(|e| AppError::Internal(anyhow::anyhow!("argon2: {}", e)))?
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
// Create system user (best-effort — panel may not be running as root in dev)
|
// Create system user (best-effort)
|
||||||
let _ = std::process::Command::new("useradd")
|
let _ = std::process::Command::new("useradd")
|
||||||
.args(&[
|
.args(["-m", "-s", "/bin/bash", "-d",
|
||||||
"-m",
|
&format!("/home/{}", body.username),
|
||||||
"-s", "/bin/bash",
|
&body.username])
|
||||||
"-d", &format!("/home/{}", body.username),
|
|
||||||
&body.username,
|
|
||||||
])
|
|
||||||
.output();
|
.output();
|
||||||
|
|
||||||
// Create Apache vhost directory
|
let _ = std::fs::create_dir_all(format!("/home/{}/public_html", body.username));
|
||||||
let vhost_dir = format!("/home/{}/public_html", body.username);
|
|
||||||
let _ = std::fs::create_dir_all(&vhost_dir);
|
|
||||||
|
|
||||||
let package_name = body.package_name.unwrap_or_else(|| "Default".into());
|
let package_name = body.package_name.unwrap_or_else(|| "Default".into());
|
||||||
let disk_quota_mb = body.disk_quota_mb.unwrap_or(10240);
|
let disk_quota_mb = body.disk_quota_mb.unwrap_or(10240);
|
||||||
let bandwidth_mb = body.bandwidth_mb.unwrap_or(0);
|
let bandwidth_mb = body.bandwidth_mb.unwrap_or(0);
|
||||||
|
|
||||||
let id = sqlx::query!(
|
let insert_id: u64 = sqlx::query(
|
||||||
"INSERT INTO accounts (username, domain, email, password_hash, package_name, disk_quota_mb, bandwidth_mb, status)
|
"INSERT INTO accounts (username, domain, email, password_hash, package_name, disk_quota_mb, bandwidth_mb, status)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, 'active')",
|
VALUES (?, ?, ?, ?, ?, ?, ?, 'active')",
|
||||||
body.username, body.domain, body.email, hash,
|
|
||||||
package_name, disk_quota_mb, bandwidth_mb
|
|
||||||
)
|
)
|
||||||
|
.bind(&body.username)
|
||||||
|
.bind(&body.domain)
|
||||||
|
.bind(&body.email)
|
||||||
|
.bind(&hash)
|
||||||
|
.bind(&package_name)
|
||||||
|
.bind(disk_quota_mb)
|
||||||
|
.bind(bandwidth_mb)
|
||||||
.execute(&state.db)
|
.execute(&state.db)
|
||||||
.await?
|
.await?
|
||||||
.last_insert_id() as i64;
|
.last_insert_id();
|
||||||
|
|
||||||
// Also create a panel user so they can log into NixClient
|
// Also create a NixClient panel user
|
||||||
let _ = sqlx::query!(
|
let _ = sqlx::query(
|
||||||
"INSERT INTO users (username, email, password_hash, role) VALUES (?, ?, ?, 'user')
|
"INSERT INTO users (username, email, password_hash, role) VALUES (?, ?, ?, 'user')
|
||||||
ON DUPLICATE KEY UPDATE email = VALUES(email)",
|
ON DUPLICATE KEY UPDATE email = VALUES(email)",
|
||||||
body.username, body.email, hash
|
|
||||||
)
|
)
|
||||||
|
.bind(&body.username)
|
||||||
|
.bind(&body.email)
|
||||||
|
.bind(&hash)
|
||||||
.execute(&state.db)
|
.execute(&state.db)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let account = sqlx::query_as!(
|
let account = sqlx::query_as::<_, Account>(
|
||||||
Account,
|
|
||||||
"SELECT id, username, domain, email, package_name, disk_quota_mb, bandwidth_mb, status, created_at
|
"SELECT id, username, domain, email, package_name, disk_quota_mb, bandwidth_mb, status, created_at
|
||||||
FROM accounts WHERE id = ?",
|
FROM accounts WHERE id = ?",
|
||||||
id
|
|
||||||
)
|
)
|
||||||
|
.bind(insert_id)
|
||||||
.fetch_one(&state.db)
|
.fetch_one(&state.db)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|||||||
@@ -23,29 +23,35 @@ pub struct UserInfo {
|
|||||||
pub role: String,
|
pub role: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Internal row type — non-macro query returns untyped rows, use FromRow
|
||||||
|
#[derive(sqlx::FromRow)]
|
||||||
|
struct UserRow {
|
||||||
|
id: i64,
|
||||||
|
username: String,
|
||||||
|
password_hash: String,
|
||||||
|
role: String,
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn login(
|
pub async fn login(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Json(body): Json<LoginRequest>,
|
Json(body): Json<LoginRequest>,
|
||||||
) -> Result<Json<LoginResponse>, AppError> {
|
) -> Result<Json<LoginResponse>, AppError> {
|
||||||
// Look up user
|
let row: Option<UserRow> = sqlx::query_as::<_, UserRow>(
|
||||||
let row = sqlx::query!(
|
|
||||||
"SELECT id, username, password_hash, role FROM users WHERE username = ? LIMIT 1",
|
"SELECT id, username, password_hash, role FROM users WHERE username = ? LIMIT 1",
|
||||||
body.username
|
|
||||||
)
|
)
|
||||||
|
.bind(&body.username)
|
||||||
.fetch_optional(&state.db)
|
.fetch_optional(&state.db)
|
||||||
.await?
|
.await?;
|
||||||
.ok_or(AppError::Unauthorized)?;
|
|
||||||
|
|
||||||
// Verify password
|
let row = row.ok_or(AppError::Unauthorized)?;
|
||||||
let parsed = PasswordHash::new(&row.password_hash)
|
|
||||||
.map_err(|_| AppError::Unauthorized)?;
|
let parsed = PasswordHash::new(&row.password_hash).map_err(|_| AppError::Unauthorized)?;
|
||||||
Argon2::default()
|
Argon2::default()
|
||||||
.verify_password(body.password.as_bytes(), &parsed)
|
.verify_password(body.password.as_bytes(), &parsed)
|
||||||
.map_err(|_| AppError::Unauthorized)?;
|
.map_err(|_| AppError::Unauthorized)?;
|
||||||
|
|
||||||
// Issue JWT
|
|
||||||
let token = Claims::new(row.id, &row.username, &row.role, &state.config.jwt_secret)
|
let token = Claims::new(row.id, &row.username, &row.role, &state.config.jwt_secret)
|
||||||
.map_err(|e| AppError::Internal(e))?;
|
.map_err(AppError::Internal)?;
|
||||||
|
|
||||||
Ok(Json(LoginResponse {
|
Ok(Json(LoginResponse {
|
||||||
token,
|
token,
|
||||||
|
|||||||
@@ -19,36 +19,30 @@ pub struct DashboardResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_dashboard(
|
pub async fn get_dashboard(
|
||||||
claims: Claims,
|
_claims: Claims,
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
) -> Result<Json<DashboardResponse>, AppError> {
|
) -> Result<Json<DashboardResponse>, AppError> {
|
||||||
// DB counts
|
// Use non-macro query() — tables may not exist at compile time
|
||||||
let accounts = sqlx::query_scalar!("SELECT COUNT(*) FROM accounts")
|
let accounts: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM accounts")
|
||||||
.fetch_one(&state.db).await?.unwrap_or(0);
|
.fetch_one(&state.db).await.unwrap_or(0);
|
||||||
let domains = sqlx::query_scalar!("SELECT COUNT(*) FROM domains")
|
let domains: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM domains")
|
||||||
.fetch_one(&state.db).await?.unwrap_or(0);
|
.fetch_one(&state.db).await.unwrap_or(0);
|
||||||
let databases = sqlx::query_scalar!("SELECT COUNT(*) FROM account_databases")
|
let databases: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM account_databases")
|
||||||
.fetch_one(&state.db).await?.unwrap_or(0);
|
.fetch_one(&state.db).await.unwrap_or(0);
|
||||||
let email_accounts = sqlx::query_scalar!("SELECT COUNT(*) FROM email_accounts")
|
let email_accounts: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM email_accounts")
|
||||||
.fetch_one(&state.db).await?.unwrap_or(0);
|
.fetch_one(&state.db).await.unwrap_or(0);
|
||||||
|
|
||||||
// Section binaries
|
|
||||||
let bin_dir = &state.config.bin_dir;
|
let bin_dir = &state.config.bin_dir;
|
||||||
|
|
||||||
let sysinfo_resp = section::call(bin_dir, "nixpanel-sysinfo", "stats", serde_json::Value::Null)
|
let sysinfo = section::call(bin_dir, "nixpanel-sysinfo", "stats", serde_json::Value::Null)
|
||||||
.unwrap_or_else(|_| nixpanel_common::SectionResponse::err("sysinfo unavailable"));
|
.unwrap_or_else(|_| nixpanel_common::SectionResponse::err("sysinfo unavailable"));
|
||||||
|
|
||||||
let versions_resp = section::call(bin_dir, "nixpanel-sysinfo", "versions", serde_json::Value::Null)
|
let versions = section::call(bin_dir, "nixpanel-sysinfo", "versions", serde_json::Value::Null)
|
||||||
.unwrap_or_else(|_| nixpanel_common::SectionResponse::err("versions unavailable"));
|
.unwrap_or_else(|_| nixpanel_common::SectionResponse::err("versions unavailable"));
|
||||||
|
|
||||||
Ok(Json(DashboardResponse {
|
Ok(Json(DashboardResponse {
|
||||||
stats: DashboardStats {
|
stats: DashboardStats { accounts, domains, databases, email_accounts },
|
||||||
accounts,
|
sysinfo: sysinfo.data.unwrap_or(serde_json::Value::Null),
|
||||||
domains,
|
versions: versions.data.unwrap_or(serde_json::Value::Null),
|
||||||
databases,
|
|
||||||
email_accounts,
|
|
||||||
},
|
|
||||||
sysinfo: sysinfo_resp.data.unwrap_or(serde_json::Value::Null),
|
|
||||||
versions: versions_resp.data.unwrap_or(serde_json::Value::Null),
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,11 @@ use axum::{
|
|||||||
extract::{Path, State},
|
extract::{Path, State},
|
||||||
Json,
|
Json,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use crate::{auth::Claims, error::AppError, section, state::AppState};
|
use crate::{auth::Claims, error::AppError, section, state::AppState};
|
||||||
|
|
||||||
pub async fn list_services(
|
pub async fn list_services(
|
||||||
claims: Claims,
|
_claims: Claims,
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
) -> Result<Json<serde_json::Value>, AppError> {
|
) -> Result<Json<serde_json::Value>, AppError> {
|
||||||
let resp = section::call(
|
let resp = section::call(
|
||||||
@@ -22,12 +21,12 @@ pub async fn list_services(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn service_action(
|
pub async fn service_action(
|
||||||
claims: Claims,
|
_claims: Claims,
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path((name, action)): Path<(String, String)>,
|
Path((name, action)): Path<(String, String)>,
|
||||||
) -> Result<Json<serde_json::Value>, AppError> {
|
) -> Result<Json<serde_json::Value>, AppError> {
|
||||||
let allowed_actions = ["start", "stop", "restart", "reload"];
|
let allowed = ["start", "stop", "restart", "reload"];
|
||||||
if !allowed_actions.contains(&action.as_str()) {
|
if !allowed.contains(&action.as_str()) {
|
||||||
return Err(AppError::BadRequest(format!("Unknown action: {}", action)));
|
return Err(AppError::BadRequest(format!("Unknown action: {}", action)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,11 +39,10 @@ pub async fn service_action(
|
|||||||
.map_err(|e| AppError::Internal(e))?;
|
.map_err(|e| AppError::Internal(e))?;
|
||||||
|
|
||||||
if resp.success {
|
if resp.success {
|
||||||
Ok(Json(serde_json::json!({
|
Ok(Json(serde_json::json!({ "success": true, "message": resp.data })))
|
||||||
"success": true,
|
|
||||||
"message": resp.data
|
|
||||||
})))
|
|
||||||
} else {
|
} else {
|
||||||
Err(AppError::BadRequest(resp.error.unwrap_or_else(|| "Action failed".into())))
|
Err(AppError::BadRequest(
|
||||||
|
resp.error.unwrap_or_else(|| "Action failed".into()),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
use nixpanel_common::{SectionRequest, SectionResponse, ServiceInfo};
|
use nixpanel_common::{SectionRequest, SectionResponse, ServiceInfo};
|
||||||
use std::io::{self, BufRead};
|
use std::io::{self, BufRead};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
|
||||||
|
|
||||||
/// Canonical list of managed services (systemd unit → display name).
|
/// Canonical list of managed services (systemd unit → display name).
|
||||||
const SERVICES: &[(&str, &str)] = &[
|
const SERVICES: &[(&str, &str)] = &[
|
||||||
|
|||||||
@@ -129,7 +129,6 @@ fn get_ram() -> (u64, u64) {
|
|||||||
|
|
||||||
fn get_disk() -> (u64, u64) {
|
fn get_disk() -> (u64, u64) {
|
||||||
// Returns (used_gb, total_gb) for /
|
// Returns (used_gb, total_gb) for /
|
||||||
use std::os::unix::fs::MetadataExt;
|
|
||||||
let stat = nix_statvfs("/");
|
let stat = nix_statvfs("/");
|
||||||
stat.unwrap_or((0, 0))
|
stat.unwrap_or((0, 0))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user