use ipfrs_core::{Cid, Error, Result};
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::path::Path;
use std::sync::Arc;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum PinType {
Direct,
Recursive,
}
#[derive(Debug, Clone)]
pub struct PinInfo {
pub cid: Cid,
pub pin_type: PinType,
pub name: Option<String>,
pub created: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct PinInfoSerde {
cid_str: String,
pin_type: PinType,
name: Option<String>,
created: chrono::DateTime<chrono::Utc>,
}
impl From<&PinInfo> for PinInfoSerde {
fn from(pin: &PinInfo) -> Self {
Self {
cid_str: pin.cid.to_string(),
pin_type: pin.pin_type,
name: pin.name.clone(),
created: pin.created,
}
}
}
impl PinInfoSerde {
fn into_pin_info(self) -> Result<PinInfo> {
use std::str::FromStr;
let cid = Cid::from_str(&self.cid_str)
.map_err(|e| Error::Serialization(format!("Invalid CID in stored pin: {}", e)))?;
Ok(PinInfo {
cid,
pin_type: self.pin_type,
name: self.name,
created: self.created,
})
}
}
pub struct PinManager {
pins: Arc<RwLock<HashMap<Cid, PinInfo>>>,
indirect_pins: Arc<RwLock<HashSet<Cid>>>,
recursive_deps: Arc<RwLock<HashMap<Cid, HashSet<Cid>>>>,
}
impl PinManager {
pub fn new() -> Self {
Self {
pins: Arc::new(RwLock::new(HashMap::new())),
indirect_pins: Arc::new(RwLock::new(HashSet::new())),
recursive_deps: Arc::new(RwLock::new(HashMap::new())),
}
}
pub fn pin(&self, cid: Cid, pin_type: PinType, name: Option<String>) -> Result<()> {
let mut pins = self.pins.write();
let pin_info = PinInfo {
cid,
pin_type,
name,
created: chrono::Utc::now(),
};
pins.insert(cid, pin_info);
Ok(())
}
pub fn add_indirect(&self, root_cid: Cid, indirect_cid: Cid) {
let mut indirect = self.indirect_pins.write();
indirect.insert(indirect_cid);
let mut deps = self.recursive_deps.write();
deps.entry(root_cid).or_default().insert(indirect_cid);
}
pub fn add_indirect_many(&self, root_cid: Cid, indirect_cids: &[Cid]) {
let mut indirect = self.indirect_pins.write();
let mut deps = self.recursive_deps.write();
let dep_set = deps.entry(root_cid).or_default();
for &cid in indirect_cids {
indirect.insert(cid);
dep_set.insert(cid);
}
}
pub fn unpin(&self, cid: &Cid, _recursive: bool) -> Result<()> {
let mut pins = self.pins.write();
if let Some(pin_info) = pins.remove(cid) {
if pin_info.pin_type == PinType::Recursive {
let mut deps = self.recursive_deps.write();
if let Some(indirect_cids) = deps.remove(cid) {
let mut indirect = self.indirect_pins.write();
for indirect_cid in indirect_cids {
let still_needed = deps.values().any(|set| set.contains(&indirect_cid));
if !still_needed {
indirect.remove(&indirect_cid);
}
}
}
}
Ok(())
} else {
Err(Error::NotFound(format!("Block not pinned: {}", cid)))
}
}
pub fn is_pinned(&self, cid: &Cid) -> bool {
let pins = self.pins.read();
if pins.contains_key(cid) {
return true;
}
let indirect = self.indirect_pins.read();
indirect.contains(cid)
}
pub fn is_directly_pinned(&self, cid: &Cid) -> bool {
let pins = self.pins.read();
pins.contains_key(cid)
}
pub fn list(&self) -> Vec<PinInfo> {
let pins = self.pins.read();
pins.values().cloned().collect()
}
pub fn get(&self, cid: &Cid) -> Option<PinInfo> {
let pins = self.pins.read();
pins.get(cid).cloned()
}
pub fn count(&self) -> usize {
let pins = self.pins.read();
pins.len()
}
pub fn indirect_count(&self) -> usize {
let indirect = self.indirect_pins.read();
indirect.len()
}
pub async fn save(&self, path: impl AsRef<Path>) -> Result<()> {
let pin_list: Vec<PinInfoSerde> = {
let pins = self.pins.read();
pins.values().map(PinInfoSerde::from).collect()
};
let data = oxicode::serde::encode_to_vec(&pin_list, oxicode::config::standard())
.map_err(|e| Error::Serialization(format!("Failed to serialize pins: {}", e)))?;
tokio::fs::write(path.as_ref(), data).await?;
Ok(())
}
pub async fn load(&self, path: impl AsRef<Path>) -> Result<()> {
let data = tokio::fs::read(path.as_ref()).await?;
let pin_list_serde: Vec<PinInfoSerde> =
oxicode::serde::decode_owned_from_slice(&data, oxicode::config::standard())
.map(|(v, _)| v)
.map_err(|e| Error::Serialization(format!("Failed to deserialize pins: {}", e)))?;
let mut pins = self.pins.write();
pins.clear();
for pin_serde in pin_list_serde {
let pin_info = pin_serde.into_pin_info()?;
pins.insert(pin_info.cid, pin_info);
}
Ok(())
}
pub fn clear(&self) {
let mut pins = self.pins.write();
pins.clear();
let mut indirect = self.indirect_pins.write();
indirect.clear();
let mut deps = self.recursive_deps.write();
deps.clear();
}
}
impl Default for PinManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pin_manager_basic() {
let manager = PinManager::new();
let cid = Cid::default();
manager
.pin(cid, PinType::Direct, Some("test".to_string()))
.expect("test: pin should succeed");
assert!(manager.is_pinned(&cid));
assert!(manager.is_directly_pinned(&cid));
let info = manager.get(&cid).expect("test: get pin should return info");
assert_eq!(info.cid, cid);
assert_eq!(info.pin_type, PinType::Direct);
assert_eq!(info.name, Some("test".to_string()));
let pins = manager.list();
assert_eq!(pins.len(), 1);
manager
.unpin(&cid, false)
.expect("test: unpin should succeed");
assert!(!manager.is_pinned(&cid));
}
#[test]
fn test_pin_manager_indirect() {
use bytes::Bytes;
use ipfrs_core::Block;
let manager = PinManager::new();
let block1 = Block::new(Bytes::from("data1")).expect("test: block creation should succeed");
let block2 = Block::new(Bytes::from("data2")).expect("test: block creation should succeed");
let cid1 = *block1.cid();
let cid2 = *block2.cid();
manager
.pin(cid1, PinType::Recursive, None)
.expect("test: pin should succeed");
manager.add_indirect(cid1, cid2);
assert!(manager.is_directly_pinned(&cid1));
assert!(!manager.is_directly_pinned(&cid2));
assert!(manager.is_pinned(&cid2));
assert_eq!(manager.count(), 1); assert_eq!(manager.indirect_count(), 1); }
#[test]
fn test_pin_manager_recursive_unpin() {
use bytes::Bytes;
use ipfrs_core::Block;
let manager = PinManager::new();
let block1 = Block::new(Bytes::from("data1")).expect("test: block creation should succeed");
let block2 = Block::new(Bytes::from("data2")).expect("test: block creation should succeed");
let block3 = Block::new(Bytes::from("data3")).expect("test: block creation should succeed");
let cid1 = *block1.cid();
let cid2 = *block2.cid();
let cid3 = *block3.cid();
manager
.pin(cid1, PinType::Recursive, None)
.expect("test: pin should succeed");
manager.add_indirect(cid1, cid2);
manager.add_indirect(cid1, cid3);
assert!(manager.is_pinned(&cid1));
assert!(manager.is_pinned(&cid2));
assert!(manager.is_pinned(&cid3));
manager
.unpin(&cid1, true)
.expect("test: unpin should succeed");
assert!(!manager.is_pinned(&cid1));
assert!(!manager.is_pinned(&cid2));
assert!(!manager.is_pinned(&cid3));
assert_eq!(manager.indirect_count(), 0);
}
#[test]
fn test_pin_manager_shared_indirect() {
use bytes::Bytes;
use ipfrs_core::Block;
let manager = PinManager::new();
let block1 = Block::new(Bytes::from("data1")).expect("test: block creation should succeed");
let block2 = Block::new(Bytes::from("data2")).expect("test: block creation should succeed");
let block3 = Block::new(Bytes::from("data3")).expect("test: block creation should succeed");
let cid1 = *block1.cid();
let cid2 = *block2.cid();
let cid3 = *block3.cid();
manager
.pin(cid1, PinType::Recursive, None)
.expect("test: pin should succeed");
manager
.pin(cid2, PinType::Recursive, None)
.expect("test: pin should succeed");
manager.add_indirect(cid1, cid3);
manager.add_indirect(cid2, cid3);
assert!(manager.is_pinned(&cid1));
assert!(manager.is_pinned(&cid2));
assert!(manager.is_pinned(&cid3));
manager
.unpin(&cid1, true)
.expect("test: unpin should succeed");
assert!(!manager.is_pinned(&cid1));
assert!(manager.is_pinned(&cid2));
assert!(manager.is_pinned(&cid3));
manager
.unpin(&cid2, true)
.expect("test: unpin should succeed");
assert!(!manager.is_pinned(&cid3));
assert_eq!(manager.indirect_count(), 0);
}
#[test]
fn test_pin_manager_clear() {
let manager = PinManager::new();
let cid = Cid::default();
manager
.pin(cid, PinType::Direct, None)
.expect("test: pin should succeed");
assert_eq!(manager.count(), 1);
manager.clear();
assert_eq!(manager.count(), 0);
assert!(!manager.is_pinned(&cid));
}
#[tokio::test]
async fn test_pin_manager_persistence() {
let manager = PinManager::new();
let cid = Cid::default();
let temp_file =
std::env::temp_dir().join(format!("ipfrs_pin_test_{}.bin", std::process::id()));
manager
.pin(cid, PinType::Direct, Some("test".to_string()))
.expect("test: pin should succeed");
manager
.save(&temp_file)
.await
.expect("test: save should succeed");
let manager2 = PinManager::new();
manager2
.load(&temp_file)
.await
.expect("test: load should succeed");
assert!(manager2.is_pinned(&cid));
let info = manager2
.get(&cid)
.expect("test: get pin should return info");
assert_eq!(info.name, Some("test".to_string()));
let _ = tokio::fs::remove_file(&temp_file).await;
}
}