use std::path::Path;
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq, Default)]
#[serde(rename_all = "lowercase")]
pub enum StorageCostProfile {
#[default]
Auto,
Hdd,
Ssd,
Memory,
}
impl StorageCostProfile {
pub fn resolve(&self, data_dir: &Path) -> Self {
match self {
Self::Auto => Self::detect_for_dir(data_dir),
preset => *preset,
}
}
pub fn resolve_for_runtime(&self, runtime: &crate::runtime::RuntimeConfig) -> Self {
if runtime.is_memory() {
return Self::Memory;
}
let dir = runtime.path().map(Path::new).unwrap_or(Path::new("."));
self.resolve(dir)
}
pub fn detect_for_dir(data_dir: &Path) -> Self {
if is_rotational(data_dir).unwrap_or(false) {
Self::Hdd
} else {
Self::Ssd
}
}
}
fn is_rotational(path: &Path) -> Option<bool> {
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
fn dev_major(dev: u64) -> u64 {
((dev >> 8) & 0xfff) | ((dev >> 32) & !0xfff)
}
fn dev_minor(dev: u64) -> u64 {
(dev & 0xff) | ((dev >> 12) & !0xff)
}
let dev = std::fs::metadata(path).ok()?.dev();
let (major, minor) = (dev_major(dev), dev_minor(dev));
let mut current = std::fs::read_link(format!("/sys/dev/block/{major}:{minor}")).ok()?;
for _ in 0..8 {
let candidate = Path::new("/sys").join(¤t).join("queue/rotational");
if let Ok(text) = std::fs::read_to_string(&candidate) {
return Some(text.trim() == "1");
}
if !current.pop() {
break;
}
}
None
}
#[cfg(not(unix))]
{
let _ = path;
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn explicit_presets_pass_through() {
let dir = Path::new("/nonexistent-dir-for-test");
assert_eq!(
StorageCostProfile::Hdd.resolve(dir),
StorageCostProfile::Hdd
);
assert_eq!(
StorageCostProfile::Ssd.resolve(dir),
StorageCostProfile::Ssd
);
assert_eq!(
StorageCostProfile::Memory.resolve(dir),
StorageCostProfile::Memory
);
}
#[test]
fn auto_falls_back_to_ssd_when_undetectable() {
assert_eq!(
StorageCostProfile::Auto.resolve(Path::new("/nonexistent-dir-for-test")),
StorageCostProfile::Ssd
);
}
#[test]
fn rotational_probe_is_positive_only() {
let dir = std::env::temp_dir();
let _ = is_rotational(&dir);
}
#[test]
fn memory_runtime_short_circuits_detection() {
let runtime = crate::runtime::RuntimeConfig::memory();
assert_eq!(
StorageCostProfile::Auto.resolve_for_runtime(&runtime),
StorageCostProfile::Memory
);
}
}