use crate::{Error, FileEntry, FileMetadata, MemoryMetadata, Result};
use async_trait::async_trait;
use chrono::Utc;
use std::path::{Path, PathBuf};
use tokio::fs;
use super::uri::UriParser;
#[async_trait]
pub trait FilesystemOperations: Send + Sync {
async fn list(&self, uri: &str) -> Result<Vec<FileEntry>>;
async fn read(&self, uri: &str) -> Result<String>;
async fn write(&self, uri: &str, content: &str) -> Result<()>;
async fn delete(&self, uri: &str) -> Result<()>;
async fn exists(&self, uri: &str) -> Result<bool>;
async fn metadata(&self, uri: &str) -> Result<FileMetadata>;
}
pub struct CortexFilesystem {
root: PathBuf,
tenant_id: Option<String>,
}
impl CortexFilesystem {
pub fn new(root: impl AsRef<Path>) -> Self {
Self {
root: root.as_ref().to_path_buf(),
tenant_id: None,
}
}
pub fn with_tenant(root: impl AsRef<Path>, tenant_id: impl Into<String>) -> Self {
Self {
root: root.as_ref().to_path_buf(),
tenant_id: Some(tenant_id.into()),
}
}
pub fn root_path(&self) -> &Path {
&self.root
}
pub fn tenant_id(&self) -> Option<&str> {
self.tenant_id.as_deref()
}
pub fn set_tenant(&mut self, tenant_id: Option<impl Into<String>>) {
self.tenant_id = tenant_id.map(|id| id.into());
}
pub async fn initialize(&self) -> Result<()> {
let base_dir = if let Some(tenant_id) = &self.tenant_id {
self.root.join("tenants").join(tenant_id)
} else {
self.root.clone()
};
fs::create_dir_all(&base_dir).await?;
if self.tenant_id.is_some() {
for dimension in &["resources", "user", "agent", "session"] {
let dir = base_dir.join(dimension);
fs::create_dir_all(dir).await?;
}
}
Ok(())
}
fn uri_to_path(&self, uri: &str) -> Result<PathBuf> {
let parsed_uri = UriParser::parse(uri)?;
let path = if let Some(tenant_id) = &self.tenant_id {
let tenant_base = self.root.join("tenants").join(tenant_id);
parsed_uri.to_file_path(&tenant_base)
} else {
parsed_uri.to_file_path(&self.root)
};
Ok(path)
}
#[allow(dead_code)]
async fn load_metadata(&self, dir_path: &Path) -> Result<Option<MemoryMetadata>> {
let metadata_path = dir_path.join(".metadata.json");
if !metadata_path.try_exists()? {
return Ok(None);
}
let content = fs::read_to_string(metadata_path).await?;
let metadata: MemoryMetadata = serde_json::from_str(&content)?;
Ok(Some(metadata))
}
#[allow(dead_code)]
async fn save_metadata(&self, dir_path: &Path, metadata: &MemoryMetadata) -> Result<()> {
let metadata_path = dir_path.join(".metadata.json");
let content = serde_json::to_string_pretty(metadata)?;
fs::write(metadata_path, content).await?;
Ok(())
}
}
#[async_trait]
impl FilesystemOperations for CortexFilesystem {
async fn list(&self, uri: &str) -> Result<Vec<FileEntry>> {
let path = self.uri_to_path(uri)?;
if !path.try_exists()? {
return Err(Error::NotFound {
uri: uri.to_string(),
});
}
let mut entries = Vec::new();
let mut read_dir = fs::read_dir(&path).await?;
while let Some(entry) = read_dir.next_entry().await? {
let metadata = entry.metadata().await?;
let name = entry.file_name().to_string_lossy().to_string();
if name.starts_with('.') && name != ".abstract.md" && name != ".overview.md" {
continue;
}
let entry_uri = format!("{}/{}", uri.trim_end_matches('/'), name);
entries.push(FileEntry {
uri: entry_uri,
name,
is_directory: metadata.is_dir(),
size: metadata.len(),
modified: metadata
.modified()
.map(|t| t.into())
.unwrap_or_else(|_| Utc::now()),
});
}
Ok(entries)
}
async fn read(&self, uri: &str) -> Result<String> {
let path = self.uri_to_path(uri)?;
if !path.try_exists()? {
return Err(Error::NotFound {
uri: uri.to_string(),
});
}
let content = fs::read_to_string(&path).await?;
Ok(content)
}
async fn write(&self, uri: &str, content: &str) -> Result<()> {
let path = self.uri_to_path(uri)?;
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).await?;
}
fs::write(&path, content).await?;
Ok(())
}
async fn delete(&self, uri: &str) -> Result<()> {
let path = self.uri_to_path(uri)?;
if !path.try_exists()? {
return Err(Error::NotFound {
uri: uri.to_string(),
});
}
if path.is_dir() {
fs::remove_dir_all(&path).await?;
} else {
fs::remove_file(&path).await?;
}
Ok(())
}
async fn exists(&self, uri: &str) -> Result<bool> {
let path = self.uri_to_path(uri)?;
Ok(path.try_exists().unwrap_or(false))
}
async fn metadata(&self, uri: &str) -> Result<FileMetadata> {
let path = self.uri_to_path(uri)?;
if !path.try_exists()? {
return Err(Error::NotFound {
uri: uri.to_string(),
});
}
let metadata = fs::metadata(&path).await?;
Ok(FileMetadata {
created_at: metadata
.created()
.map(|t| t.into())
.unwrap_or_else(|_| Utc::now()),
updated_at: metadata
.modified()
.map(|t| t.into())
.unwrap_or_else(|_| Utc::now()),
size: metadata.len(),
is_directory: metadata.is_dir(),
})
}
}