use std::collections::BTreeMap;
use std::io::Read;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use super::types::{MarketplaceCatalog, MarketplaceCatalogId};
use crate::plugins::registry::{
ensure_private_plugin_state_directory, harden_plugin_state_file, open_existing_regular_file,
open_state_lock, path_entry_exists, save_state_with_hardener, state_lock_path,
validate_existing_plugin_state_parent,
};
const MARKETPLACE_SCHEMA_VERSION: u32 = 1;
const MARKETPLACE_STATE_FILE: &str = "marketplaces.json";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StoredMarketplaceCatalog {
pub added_at: String,
pub source_path: String,
pub catalog: MarketplaceCatalog,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MarketplaceState {
schema_version: u32,
#[serde(default)]
catalogs: BTreeMap<String, StoredMarketplaceCatalog>,
}
impl Default for MarketplaceState {
fn default() -> Self {
Self {
schema_version: MARKETPLACE_SCHEMA_VERSION,
catalogs: BTreeMap::new(),
}
}
}
impl MarketplaceState {
#[must_use]
pub fn catalogs(&self) -> &BTreeMap<String, StoredMarketplaceCatalog> {
&self.catalogs
}
pub fn get(&self, name: &str) -> Option<&StoredMarketplaceCatalog> {
self.catalogs.get(name)
}
}
pub struct MarketplaceStore {
path: PathBuf,
}
impl MarketplaceStore {
#[must_use]
pub fn open(state_path: Option<&Path>) -> Option<Self> {
let parent = state_path?.parent()?.to_path_buf();
Some(Self {
path: parent.join(MARKETPLACE_STATE_FILE),
})
}
#[must_use]
pub fn path(&self) -> &Path {
&self.path
}
pub fn load(&self) -> Result<MarketplaceState, String> {
validate_existing_plugin_state_parent(&self.path)?;
let lock_path = state_lock_path(&self.path);
if path_entry_exists(&lock_path)? {
let lock_file = open_state_lock(&lock_path, false)?;
let lock = fd_lock::RwLock::new(lock_file);
let _guard = lock
.read()
.map_err(|e| format!("failed to read-lock marketplace state: {e}"))?;
return self.load_unlocked();
}
self.load_unlocked()
}
fn load_unlocked(&self) -> Result<MarketplaceState, String> {
let Some(mut file) = open_existing_regular_file(&self.path, false)? else {
return Ok(MarketplaceState::default());
};
let mut raw = String::new();
file.read_to_string(&mut raw)
.map_err(|e| format!("failed to read {}: {e}", self.path.display()))?;
let state: MarketplaceState = serde_json::from_str(&raw)
.map_err(|e| format!("failed to parse {}: {e}", self.path.display()))?;
if state.schema_version != MARKETPLACE_SCHEMA_VERSION {
return Err(format!(
"unsupported marketplace state schema {}; expected {MARKETPLACE_SCHEMA_VERSION} at {}",
state.schema_version,
self.path.display()
));
}
Ok(state)
}
pub fn add(
&self,
id: &MarketplaceCatalogId,
entry: StoredMarketplaceCatalog,
) -> Result<(), String> {
self.mutate(|state| {
if state.catalogs.contains_key(id.as_str()) {
return Err(format!(
"a marketplace named `{}` already exists; /plugin marketplace remove {} first",
id.as_str(),
id.as_str()
));
}
state.catalogs.insert(id.as_str().to_string(), entry);
Ok(())
})
}
pub fn remove(&self, name: &str) -> Result<bool, String> {
self.mutate(|state| Ok(state.catalogs.remove(name).is_some()))
}
fn mutate<R>(
&self,
mutate: impl FnOnce(&mut MarketplaceState) -> Result<R, String>,
) -> Result<R, String> {
let lock_path = state_lock_path(&self.path);
if let Some(parent) = lock_path.parent() {
ensure_private_plugin_state_directory(parent)?;
}
let lock_file = open_state_lock(&lock_path, true)?;
let mut lock = fd_lock::RwLock::new(lock_file);
let _guard = lock
.write()
.map_err(|e| format!("failed to lock marketplace state for update: {e}"))?;
let mut next = self.load_unlocked()?;
let result = mutate(&mut next)?;
save_state_with_hardener(&self.path, &next, harden_plugin_state_file)?;
Ok(result)
}
}