Skip to main content

ferripfs_pinning/
store.rs

1// Ported from: kubo/boxo/pinning/pinner/dspinner
2// Kubo version: v0.39.0
3// Original: https://github.com/ipfs/kubo/tree/v0.39.0/boxo/pinning/pinner/dspinner
4//
5// Original work: Copyright (c) Protocol Labs, Inc.
6// Port: Copyright (c) 2026 ferripfs contributors
7// SPDX-License-Identifier: MIT OR Apache-2.0
8
9//! Persistent pin storage.
10
11use cid::Cid;
12use parking_lot::RwLock;
13use std::collections::{HashMap, HashSet};
14use std::path::Path;
15
16use crate::{PinInfo, PinMode, PinResult};
17
18/// Pin storage state
19#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
20struct PinState {
21    /// Direct pins: CID -> name
22    direct: HashMap<String, Option<String>>,
23    /// Recursive pins: CID -> name
24    recursive: HashMap<String, Option<String>>,
25    /// Indirect pins: CID -> set of recursive pin CIDs that reference this
26    indirect: HashMap<String, HashSet<String>>,
27}
28
29/// Persistent pin store
30pub struct PinStore {
31    state: RwLock<PinState>,
32    path: Option<std::path::PathBuf>,
33}
34
35impl PinStore {
36    /// Create a new in-memory pin store
37    pub fn new() -> Self {
38        Self {
39            state: RwLock::new(PinState::default()),
40            path: None,
41        }
42    }
43
44    /// Create or open a persistent pin store
45    pub fn open(path: impl AsRef<Path>) -> PinResult<Self> {
46        let path = path.as_ref().to_path_buf();
47
48        let state = if path.exists() {
49            let data = std::fs::read_to_string(&path)?;
50            serde_json::from_str(&data)?
51        } else {
52            PinState::default()
53        };
54
55        Ok(Self {
56            state: RwLock::new(state),
57            path: Some(path),
58        })
59    }
60
61    /// Save to disk (if persistent)
62    pub fn save(&self) -> PinResult<()> {
63        if let Some(ref path) = self.path {
64            let state = self.state.read();
65            let data = serde_json::to_string_pretty(&*state)?;
66
67            // Write atomically
68            let tmp_path = path.with_extension("tmp");
69            std::fs::write(&tmp_path, &data)?;
70            std::fs::rename(&tmp_path, path)?;
71        }
72        Ok(())
73    }
74
75    /// Check if a CID is pinned (any mode)
76    pub fn is_pinned(&self, cid: &Cid) -> bool {
77        let cid_str = cid.to_string();
78        let state = self.state.read();
79
80        state.direct.contains_key(&cid_str)
81            || state.recursive.contains_key(&cid_str)
82            || state.indirect.contains_key(&cid_str)
83    }
84
85    /// Check if a CID is pinned with a specific mode
86    pub fn is_pinned_with_mode(&self, cid: &Cid, mode: PinMode) -> bool {
87        let cid_str = cid.to_string();
88        let state = self.state.read();
89
90        match mode {
91            PinMode::Direct => state.direct.contains_key(&cid_str),
92            PinMode::Recursive => state.recursive.contains_key(&cid_str),
93            PinMode::Indirect => state.indirect.contains_key(&cid_str),
94        }
95    }
96
97    /// Add a direct pin
98    pub fn add_direct(&mut self, cid: &Cid, name: Option<String>) {
99        let cid_str = cid.to_string();
100        let mut state = self.state.write();
101        state.direct.insert(cid_str, name);
102    }
103
104    /// Remove a direct pin
105    pub fn remove_direct(&mut self, cid: &Cid) {
106        let cid_str = cid.to_string();
107        let mut state = self.state.write();
108        state.direct.remove(&cid_str);
109    }
110
111    /// Add a recursive pin
112    pub fn add_recursive(&mut self, cid: &Cid, name: Option<String>) {
113        let cid_str = cid.to_string();
114        let mut state = self.state.write();
115        state.recursive.insert(cid_str, name);
116    }
117
118    /// Remove a recursive pin
119    pub fn remove_recursive(&mut self, cid: &Cid) {
120        let cid_str = cid.to_string();
121        let mut state = self.state.write();
122        state.recursive.remove(&cid_str);
123    }
124
125    /// Add an indirect pin (called when a block is referenced by a recursive pin)
126    pub fn add_indirect(&mut self, cid: &Cid, pinned_by: &Cid) {
127        let cid_str = cid.to_string();
128        let pinned_by_str = pinned_by.to_string();
129        let mut state = self.state.write();
130
131        state
132            .indirect
133            .entry(cid_str)
134            .or_default()
135            .insert(pinned_by_str);
136    }
137
138    /// Remove an indirect pin reference
139    pub fn remove_indirect(&mut self, cid: &Cid, pinned_by: &Cid) {
140        let cid_str = cid.to_string();
141        let pinned_by_str = pinned_by.to_string();
142        let mut state = self.state.write();
143
144        if let Some(refs) = state.indirect.get_mut(&cid_str) {
145            refs.remove(&pinned_by_str);
146            if refs.is_empty() {
147                state.indirect.remove(&cid_str);
148            }
149        }
150    }
151
152    /// Get pin info for a CID
153    pub fn get(&self, cid: &Cid) -> Option<PinInfo> {
154        let cid_str = cid.to_string();
155        let state = self.state.read();
156
157        if let Some(name) = state.direct.get(&cid_str) {
158            return Some(PinInfo {
159                cid: cid_str,
160                mode: PinMode::Direct,
161                name: name.clone(),
162            });
163        }
164
165        if let Some(name) = state.recursive.get(&cid_str) {
166            return Some(PinInfo {
167                cid: cid_str,
168                mode: PinMode::Recursive,
169                name: name.clone(),
170            });
171        }
172
173        if state.indirect.contains_key(&cid_str) {
174            return Some(PinInfo {
175                cid: cid_str,
176                mode: PinMode::Indirect,
177                name: None,
178            });
179        }
180
181        None
182    }
183
184    /// List all pins, optionally filtered by mode
185    pub fn list(&self, mode: Option<PinMode>) -> Vec<PinInfo> {
186        let state = self.state.read();
187        let mut pins = Vec::new();
188
189        let include_direct = mode.is_none() || mode == Some(PinMode::Direct);
190        let include_recursive = mode.is_none() || mode == Some(PinMode::Recursive);
191        let include_indirect = mode.is_none() || mode == Some(PinMode::Indirect);
192
193        if include_direct {
194            for (cid, name) in &state.direct {
195                pins.push(PinInfo {
196                    cid: cid.clone(),
197                    mode: PinMode::Direct,
198                    name: name.clone(),
199                });
200            }
201        }
202
203        if include_recursive {
204            for (cid, name) in &state.recursive {
205                pins.push(PinInfo {
206                    cid: cid.clone(),
207                    mode: PinMode::Recursive,
208                    name: name.clone(),
209                });
210            }
211        }
212
213        if include_indirect {
214            for cid in state.indirect.keys() {
215                pins.push(PinInfo {
216                    cid: cid.clone(),
217                    mode: PinMode::Indirect,
218                    name: None,
219                });
220            }
221        }
222
223        pins
224    }
225
226    /// Get count of each pin type
227    pub fn counts(&self) -> (usize, usize, usize) {
228        let state = self.state.read();
229        (
230            state.direct.len(),
231            state.recursive.len(),
232            state.indirect.len(),
233        )
234    }
235}
236
237impl Default for PinStore {
238    fn default() -> Self {
239        Self::new()
240    }
241}
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246    use ferripfs_blockstore::create_cid_v0;
247    use tempfile::tempdir;
248
249    fn test_cid(data: &[u8]) -> Cid {
250        create_cid_v0(data).unwrap()
251    }
252
253    #[test]
254    fn test_direct_pin_operations() {
255        let mut store = PinStore::new();
256        let cid = test_cid(b"test");
257
258        assert!(!store.is_pinned(&cid));
259
260        store.add_direct(&cid, Some("test-pin".to_string()));
261        assert!(store.is_pinned(&cid));
262        assert!(store.is_pinned_with_mode(&cid, PinMode::Direct));
263        assert!(!store.is_pinned_with_mode(&cid, PinMode::Recursive));
264
265        let info = store.get(&cid).unwrap();
266        assert_eq!(info.mode, PinMode::Direct);
267        assert_eq!(info.name, Some("test-pin".to_string()));
268
269        store.remove_direct(&cid);
270        assert!(!store.is_pinned(&cid));
271    }
272
273    #[test]
274    fn test_indirect_pin_operations() {
275        let mut store = PinStore::new();
276        let cid = test_cid(b"child");
277        let parent = test_cid(b"parent");
278
279        store.add_indirect(&cid, &parent);
280        assert!(store.is_pinned(&cid));
281        assert!(store.is_pinned_with_mode(&cid, PinMode::Indirect));
282
283        store.remove_indirect(&cid, &parent);
284        assert!(!store.is_pinned(&cid));
285    }
286
287    #[test]
288    fn test_persistence() {
289        let dir = tempdir().unwrap();
290        let path = dir.path().join("pins.json");
291
292        let cid = test_cid(b"persistent");
293
294        // Create and save
295        {
296            let mut store = PinStore::open(&path).unwrap();
297            store.add_direct(&cid, Some("saved-pin".to_string()));
298            store.save().unwrap();
299        }
300
301        // Reopen and verify
302        {
303            let store = PinStore::open(&path).unwrap();
304            assert!(store.is_pinned(&cid));
305            let info = store.get(&cid).unwrap();
306            assert_eq!(info.name, Some("saved-pin".to_string()));
307        }
308    }
309
310    #[test]
311    fn test_list_with_filter() {
312        let mut store = PinStore::new();
313
314        let cid1 = test_cid(b"direct");
315        let cid2 = test_cid(b"recursive");
316        let cid3 = test_cid(b"indirect");
317        let parent = test_cid(b"parent");
318
319        store.add_direct(&cid1, None);
320        store.add_recursive(&cid2, None);
321        store.add_indirect(&cid3, &parent);
322
323        assert_eq!(store.list(None).len(), 3);
324        assert_eq!(store.list(Some(PinMode::Direct)).len(), 1);
325        assert_eq!(store.list(Some(PinMode::Recursive)).len(), 1);
326        assert_eq!(store.list(Some(PinMode::Indirect)).len(), 1);
327    }
328}