Adds GET /api/v1/admin/system returning disk (scoped to storage_dir via statvfs), memory, CPU, and a server-side alerts array that fires at >90% disk or memory. Disk uses nix::sys::statvfs directly rather than sysinfo's Disks API to avoid mountpoint-matching gymnastics for the storage_dir. A new `Storage::local_root() -> Option<&Path>` trait method exposes the root; the default returns None so a future S3Storage gets `disk: null` in the response instead of fabricated numbers. CPU is sampled inline (refresh → 250ms sleep → refresh → read) so the endpoint adds 250ms of latency per call. No background-cache yet — admin traffic is low-volume and the moving parts aren't worth it until polling shows up. Alerts are evaluated server-side so the frontend can render them without re-implementing the thresholds.
59 lines
1.9 KiB
Rust
59 lines
1.9 KiB
Rust
//! Pluggable blob storage.
|
|
//!
|
|
//! Handlers depend on the `Storage` trait, never on a concrete backend.
|
|
//! Add new backends (S3, GCS, …) as new impls in this module and wire
|
|
//! them up in `app::build` based on config.
|
|
|
|
mod local;
|
|
|
|
use std::io;
|
|
use std::pin::Pin;
|
|
|
|
use std::path::Path;
|
|
|
|
use async_trait::async_trait;
|
|
use bytes::Bytes;
|
|
use futures_core::Stream;
|
|
|
|
pub use local::LocalStorage;
|
|
|
|
#[derive(thiserror::Error, Debug)]
|
|
pub enum StorageError {
|
|
#[error(transparent)]
|
|
Io(#[from] io::Error),
|
|
#[error("not found")]
|
|
NotFound,
|
|
#[error("invalid storage key")]
|
|
BadKey,
|
|
}
|
|
|
|
/// Boxed byte stream returned by `Storage::get_stream` so the trait stays
|
|
/// object-safe regardless of the concrete reader behind it.
|
|
pub type ByteStream = Pin<Box<dyn Stream<Item = io::Result<Bytes>> + Send>>;
|
|
|
|
pub struct StreamingFile {
|
|
pub stream: ByteStream,
|
|
pub size_bytes: u64,
|
|
}
|
|
|
|
#[async_trait]
|
|
pub trait Storage: Send + Sync {
|
|
async fn put(&self, key: &str, bytes: &[u8]) -> Result<(), StorageError>;
|
|
/// Reads the entire blob into memory. Convenient for small assets
|
|
/// (covers, thumbnails). For pages and other large blobs, use
|
|
/// `get_stream` so axum can pipe bytes straight to the client.
|
|
async fn get(&self, key: &str) -> Result<Vec<u8>, StorageError>;
|
|
async fn get_stream(&self, key: &str) -> Result<StreamingFile, StorageError>;
|
|
async fn delete(&self, key: &str) -> Result<(), StorageError>;
|
|
async fn exists(&self, key: &str) -> Result<bool, StorageError>;
|
|
|
|
/// Filesystem path the backend is rooted at, when introspectable.
|
|
/// Returns `None` for backends that aren't a local filesystem (e.g.
|
|
/// a future `S3Storage`). The admin system endpoint uses this to
|
|
/// statvfs the data dir; backends that return `None` get a `disk:
|
|
/// null` payload instead of fabricated numbers.
|
|
fn local_root(&self) -> Option<&Path> {
|
|
None
|
|
}
|
|
}
|