use std::path::PathBuf;
use crate::permission::config::{PermissionConfig, SerializablePermissionConfig};
use bamboo_infrastructure::Config;
#[derive(Debug, Clone)]
pub struct PermissionStorage {
config_dir: PathBuf,
filename: String,
}
impl PermissionStorage {
pub const DEFAULT_FILENAME: &str = "permissions.json";
pub fn new(config_dir: impl Into<PathBuf>) -> Self {
Self {
config_dir: config_dir.into(),
filename: Self::DEFAULT_FILENAME.to_string(),
}
}
pub fn with_filename(config_dir: impl Into<PathBuf>, filename: impl Into<String>) -> Self {
Self {
config_dir: config_dir.into(),
filename: filename.into(),
}
}
pub fn config_path(&self) -> PathBuf {
self.config_dir.join("config.json")
}
fn legacy_permissions_path(&self) -> PathBuf {
self.config_dir.join(&self.filename)
}
pub async fn load(&self) -> Result<Option<PermissionConfig>, PermissionStorageError> {
const KEY: &str = "permissions";
let config = Config::from_data_dir(Some(self.config_dir.clone()));
if let Some(value) = config.extra.get(KEY).cloned() {
let serializable: SerializablePermissionConfig = serde_json::from_value(value)
.map_err(|e| PermissionStorageError::ParseError {
path: self.config_path(),
source: e,
})?;
return Ok(Some(PermissionConfig::from_serializable(serializable)));
}
let legacy = self.legacy_permissions_path();
if !legacy.exists() {
return Ok(None);
}
let content = tokio::fs::read_to_string(&legacy).await.map_err(|e| {
PermissionStorageError::ReadError {
path: legacy.clone(),
source: e,
}
})?;
if content.trim().is_empty() {
return Ok(None);
}
let serializable: SerializablePermissionConfig =
serde_json::from_str(&content).map_err(|e| PermissionStorageError::ParseError {
path: legacy.clone(),
source: e,
})?;
let mut config = config;
config.extra.insert(
KEY.to_string(),
serde_json::to_value(&serializable).map_err(|e| {
PermissionStorageError::SerializationError {
path: self.config_path(),
source: e,
}
})?,
);
let data_dir = self.config_dir.clone();
tokio::task::spawn_blocking(move || config.save_to_dir(data_dir))
.await
.map_err(|e| PermissionStorageError::WriteError {
path: self.config_path(),
source: std::io::Error::new(std::io::ErrorKind::Other, e.to_string()),
})?
.map_err(|e| PermissionStorageError::WriteError {
path: self.config_path(),
source: std::io::Error::new(std::io::ErrorKind::Other, e.to_string()),
})?;
if let Some(name) = legacy.file_name().and_then(|s| s.to_str()) {
let backup = legacy.with_file_name(format!("{name}.migrated.bak"));
let _ = tokio::fs::rename(&legacy, backup).await;
}
Ok(Some(PermissionConfig::from_serializable(serializable)))
}
pub async fn load_or_default(&self) -> Result<PermissionConfig, PermissionStorageError> {
match self.load().await {
Ok(Some(config)) => Ok(config),
Ok(None) => Ok(PermissionConfig::new()),
Err(e) => Err(e),
}
}
pub async fn save(&self, config: &PermissionConfig) -> Result<(), PermissionStorageError> {
const KEY: &str = "permissions";
if !self.config_dir.exists() {
tokio::fs::create_dir_all(&self.config_dir)
.await
.map_err(|e| PermissionStorageError::WriteError {
path: self.config_path(),
source: e,
})?;
}
let serializable = config.to_serializable();
let mut root = Config::from_data_dir(Some(self.config_dir.clone()));
root.extra.insert(
KEY.to_string(),
serde_json::to_value(&serializable).map_err(|e| {
PermissionStorageError::SerializationError {
path: self.config_path(),
source: e,
}
})?,
);
let data_dir = self.config_dir.clone();
tokio::task::spawn_blocking(move || root.save_to_dir(data_dir))
.await
.map_err(|e| PermissionStorageError::WriteError {
path: self.config_path(),
source: std::io::Error::new(std::io::ErrorKind::Other, e.to_string()),
})?
.map_err(|e| PermissionStorageError::WriteError {
path: self.config_path(),
source: std::io::Error::new(std::io::ErrorKind::Other, e.to_string()),
})?;
Ok(())
}
pub fn exists(&self) -> bool {
self.config_path().exists()
}
pub async fn load_with_project(
&self,
project_dir: &std::path::Path,
) -> Result<Option<PermissionConfig>, PermissionStorageError> {
let user_config = self.load().await.unwrap_or(None);
let project_storage = PermissionStorage::new(project_dir.join(".bamboo"));
let project_config = project_storage.load().await.unwrap_or(None);
let local_storage = PermissionStorage::new(project_dir.join(".bamboo"));
let local_config = local_storage.load().await.unwrap_or(None);
let has_any = user_config.is_some() || project_config.is_some() || local_config.is_some();
let mut result: PermissionConfig = user_config.unwrap_or_default();
if let Some(proj) = project_config {
result = proj.merge(&result);
}
if let Some(loc) = local_config {
result = loc.merge(&result);
}
if !has_any {
Ok(None)
} else {
Ok(Some(result))
}
}
pub async fn delete(&self) -> Result<(), PermissionStorageError> {
let path = self.config_path();
if path.exists() {
tokio::fs::remove_file(&path).await.map_err(|e| {
PermissionStorageError::WriteError {
path: path.clone(),
source: e,
}
})?;
}
Ok(())
}
}
#[derive(Debug, thiserror::Error)]
pub enum PermissionStorageError {
#[error("Failed to read permission config from {path}: {source}")]
ReadError {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("Failed to write permission config to {path}: {source}")]
WriteError {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("Failed to parse permission config from {path}: {source}")]
ParseError {
path: PathBuf,
#[source]
source: serde_json::Error,
},
#[error("Failed to serialize permission config for {path}: {source}")]
SerializationError {
path: PathBuf,
#[source]
source: serde_json::Error,
},
}
pub fn default_storage() -> Option<PermissionStorage> {
Some(PermissionStorage::new(
bamboo_infrastructure::paths::bamboo_dir(),
))
}
pub fn app_storage(app_name: &str) -> Option<PermissionStorage> {
Some(PermissionStorage::new(
bamboo_infrastructure::paths::bamboo_dir().join(app_name),
))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::permission::config::{PermissionRule, PermissionType};
#[tokio::test]
async fn test_save_and_load() {
let temp_dir = std::env::temp_dir().join("bamboo_permission_test");
let _ = tokio::fs::remove_dir_all(&temp_dir).await;
let storage = PermissionStorage::new(&temp_dir);
let config = PermissionConfig::new();
config.add_rule(PermissionRule::new(PermissionType::WriteFile, "*.rs", true));
config.add_rule(PermissionRule::new(
PermissionType::ExecuteCommand,
"cargo *",
true,
));
storage.save(&config).await.unwrap();
let loaded = storage.load().await.unwrap().unwrap();
let rules = loaded.get_rules();
assert_eq!(rules.len(), 2);
let _ = tokio::fs::remove_dir_all(&temp_dir).await;
}
#[tokio::test]
async fn test_load_nonexistent() {
let temp_dir = std::env::temp_dir().join("bamboo_permission_test_nonexistent");
let _ = tokio::fs::remove_dir_all(&temp_dir).await;
let storage = PermissionStorage::new(&temp_dir);
let result = storage.load().await.unwrap();
assert!(result.is_none());
}
#[tokio::test]
async fn test_load_or_default() {
let temp_dir = std::env::temp_dir().join("bamboo_permission_test_default");
let _ = tokio::fs::remove_dir_all(&temp_dir).await;
let storage = PermissionStorage::new(&temp_dir);
let config = storage.load_or_default().await.unwrap();
assert!(config.is_enabled());
let _ = tokio::fs::remove_dir_all(&temp_dir).await;
}
}