use std::fs;
use std::path::PathBuf;
use std::sync::{Mutex, OnceLock, RwLock};
use crate::types::{Session, SessionData, StorageFile};
static STORAGE_ROOT: OnceLock<RwLock<Option<PathBuf>>> = OnceLock::new();
static STORAGE_IO_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
pub(crate) fn storage_io_lock() -> &'static Mutex<()> {
STORAGE_IO_LOCK.get_or_init(|| Mutex::new(()))
}
#[cfg(test)]
pub(crate) fn storage_test_lock() -> &'static Mutex<()> {
static TEST_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
TEST_LOCK.get_or_init(|| Mutex::new(()))
}
pub fn set_storage_root(root: impl Into<PathBuf>) {
let lock = STORAGE_ROOT.get_or_init(|| RwLock::new(None));
if let Ok(mut configured_root) = lock.write() {
*configured_root = Some(root.into());
}
}
pub fn clear_storage_root() {
let lock = STORAGE_ROOT.get_or_init(|| RwLock::new(None));
if let Ok(mut configured_root) = lock.write() {
*configured_root = None;
}
}
pub fn get_storage_path() -> PathBuf {
storage_root()
.join(".otherone")
.join("storage")
.join("otherone-storage.json")
}
fn storage_root() -> PathBuf {
STORAGE_ROOT
.get_or_init(|| RwLock::new(None))
.read()
.ok()
.and_then(|configured_root| configured_root.clone())
.unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")))
}
pub fn read_storage_file() -> Result<StorageFile, crate::error::StorageError> {
let _guard = storage_io_lock().lock().map_err(|_| {
crate::error::StorageError::ConfigError("local storage lock poisoned".to_string())
})?;
read_storage_file_unlocked()
}
pub(crate) fn read_storage_file_unlocked() -> Result<StorageFile, crate::error::StorageError> {
let storage_path = get_storage_path();
if !storage_path.exists() {
if let Some(parent) = storage_path.parent() {
fs::create_dir_all(parent)?;
}
let initial_data = StorageFile {
sessions: Vec::new(),
};
let json = serde_json::to_string_pretty(&initial_data)?;
fs::write(&storage_path, json)?;
return Ok(initial_data);
}
let content = fs::read_to_string(&storage_path)?;
let data: StorageFile = serde_json::from_str(&content)?;
Ok(data)
}
pub fn write_storage_file(data: &StorageFile) -> Result<(), crate::error::StorageError> {
let _guard = storage_io_lock().lock().map_err(|_| {
crate::error::StorageError::ConfigError("local storage lock poisoned".to_string())
})?;
write_storage_file_unlocked(data)
}
pub(crate) fn write_storage_file_unlocked(
data: &StorageFile,
) -> Result<(), crate::error::StorageError> {
let storage_path = get_storage_path();
if let Some(parent) = storage_path.parent() {
fs::create_dir_all(parent)?;
}
let json = serde_json::to_string_pretty(data)?;
fs::write(&storage_path, json)?;
Ok(())
}
pub fn get_all_sessions() -> Result<Vec<Session>, crate::error::StorageError> {
get_all_sessions_with_internal(false)
}
pub fn get_all_sessions_with_internal(
include_internal: bool,
) -> Result<Vec<Session>, crate::error::StorageError> {
let data = read_storage_file()?;
let sessions = data
.sessions
.iter()
.filter(|session| {
include_internal
|| session
.metadata
.get("session_kind")
.and_then(|value| value.as_str())
!= Some("agent_internal")
})
.map(|s| Session {
partition_key: s.partition_key.clone(),
session_id: s.session_id.clone(),
status: s.status,
create_at: s.create_at.clone(),
attributes: s.attributes.clone(),
metadata: s.metadata.clone(),
})
.collect();
Ok(sessions)
}
pub fn read_session_data(session_id: &str) -> Result<SessionData, crate::error::StorageError> {
if session_id.is_empty() {
return Err(crate::error::StorageError::ConfigError(
"session_id is required".to_string(),
));
}
let data = read_storage_file()?;
let session = data
.sessions
.iter()
.find(|s| s.session_id == session_id && s.status == 0);
match session {
None => Ok(SessionData {
session: None,
entries: Vec::new(),
compacted_entries: Vec::new(),
}),
Some(s) => Ok(SessionData {
session: Some(Session {
partition_key: s.partition_key.clone(),
session_id: s.session_id.clone(),
status: s.status,
create_at: s.create_at.clone(),
attributes: s.attributes.clone(),
metadata: s.metadata.clone(),
}),
entries: s.entries.clone(),
compacted_entries: s.compacted_entries.clone(),
}),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_read_session_data_empty_id() {
let result = read_session_data("");
assert!(result.is_err());
}
#[test]
fn storage_root_can_be_configured_without_changing_current_dir() {
let _guard = storage_test_lock().lock().unwrap();
let root = std::env::temp_dir().join(format!(
"otherone-storage-root-{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
set_storage_root(root.clone());
let path = get_storage_path();
assert!(path.starts_with(&root));
let data = read_storage_file().unwrap();
assert!(data.sessions.is_empty());
assert!(path.exists());
clear_storage_root();
}
}