Skip to main content

ferripfs_pinning/
pinner.rs

1// Ported from: kubo/boxo/pinning/pinner/pin.go
2// Kubo version: v0.39.0
3// Original: https://github.com/ipfs/kubo/blob/v0.39.0/boxo/pinning/pinner/pin.go
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//! Pinner trait and implementation.
10
11use cid::Cid;
12use ferripfs_blockstore::Blockstore;
13use std::collections::HashSet;
14
15use crate::{PinError, PinInfo, PinMode, PinResult, PinStore};
16
17/// Trait for pinning operations
18pub trait Pinner {
19    /// Check if a CID is pinned (any mode)
20    fn is_pinned(&self, cid: &Cid) -> PinResult<bool>;
21
22    /// Check if a CID is pinned with a specific mode
23    fn is_pinned_with_mode(&self, cid: &Cid, mode: PinMode) -> PinResult<bool>;
24
25    /// Pin a CID with the specified mode
26    fn pin(&mut self, cid: &Cid, mode: PinMode) -> PinResult<()>;
27
28    /// Pin a CID with the specified mode and name
29    fn pin_with_name(&mut self, cid: &Cid, mode: PinMode, name: Option<String>) -> PinResult<()>;
30
31    /// Unpin a CID
32    fn unpin(&mut self, cid: &Cid, recursive: bool) -> PinResult<()>;
33
34    /// Get pin info for a CID
35    fn get_pin(&self, cid: &Cid) -> PinResult<Option<PinInfo>>;
36
37    /// List all pins, optionally filtered by mode
38    fn list_pins(&self, mode: Option<PinMode>) -> PinResult<Vec<PinInfo>>;
39
40    /// Update a pin from old CID to new CID
41    fn update_pin(&mut self, old: &Cid, new: &Cid, unpin: bool) -> PinResult<()>;
42
43    /// Verify all pins (check that all pinned blocks exist)
44    fn verify(&self) -> PinResult<Vec<(Cid, String)>>;
45
46    /// Get all CIDs that should not be garbage collected
47    fn pinned_cids(&self) -> PinResult<HashSet<Cid>>;
48}
49
50/// Default pinner implementation using a blockstore and pin store
51pub struct BlockstorePinner<'a, B: Blockstore> {
52    blockstore: &'a B,
53    store: PinStore,
54}
55
56impl<'a, B: Blockstore> BlockstorePinner<'a, B> {
57    /// Create a new pinner
58    pub fn new(blockstore: &'a B, store: PinStore) -> Self {
59        Self { blockstore, store }
60    }
61
62    /// Get a reference to the pin store
63    pub fn store(&self) -> &PinStore {
64        &self.store
65    }
66
67    /// Get a mutable reference to the pin store
68    pub fn store_mut(&mut self) -> &mut PinStore {
69        &mut self.store
70    }
71
72    /// Collect all CIDs referenced by a block (recursively)
73    fn collect_refs(&self, cid: &Cid, refs: &mut HashSet<Cid>) -> PinResult<()> {
74        if refs.contains(cid) {
75            return Ok(());
76        }
77
78        // Get the block
79        let block = self
80            .blockstore
81            .get(cid)?
82            .ok_or_else(|| PinError::BlockNotFound(cid.to_string()))?;
83
84        // Try to decode as UnixFS/DAG-PB to find links
85        if let Ok(links) = extract_links(block.data()) {
86            for link_cid in links {
87                refs.insert(link_cid);
88                self.collect_refs(&link_cid, refs)?;
89            }
90        }
91
92        Ok(())
93    }
94}
95
96impl<'a, B: Blockstore> Pinner for BlockstorePinner<'a, B> {
97    fn is_pinned(&self, cid: &Cid) -> PinResult<bool> {
98        Ok(self.store.is_pinned(cid))
99    }
100
101    fn is_pinned_with_mode(&self, cid: &Cid, mode: PinMode) -> PinResult<bool> {
102        Ok(self.store.is_pinned_with_mode(cid, mode))
103    }
104
105    fn pin(&mut self, cid: &Cid, mode: PinMode) -> PinResult<()> {
106        self.pin_with_name(cid, mode, None)
107    }
108
109    fn pin_with_name(&mut self, cid: &Cid, mode: PinMode, name: Option<String>) -> PinResult<()> {
110        // Verify block exists
111        if !self.blockstore.has(cid)? {
112            return Err(PinError::BlockNotFound(cid.to_string()));
113        }
114
115        match mode {
116            PinMode::Direct => {
117                self.store.add_direct(cid, name);
118            }
119            PinMode::Recursive => {
120                // Collect all referenced blocks
121                let mut refs = HashSet::new();
122                self.collect_refs(cid, &mut refs)?;
123
124                // Add indirect pins for all refs
125                for ref_cid in &refs {
126                    self.store.add_indirect(ref_cid, cid);
127                }
128
129                // Add the recursive pin
130                self.store.add_recursive(cid, name);
131            }
132            PinMode::Indirect => {
133                // Indirect pins are created automatically, not directly
134                return Err(PinError::AlreadyPinned(
135                    "Cannot create indirect pin directly".to_string(),
136                ));
137            }
138        }
139
140        Ok(())
141    }
142
143    fn unpin(&mut self, cid: &Cid, recursive: bool) -> PinResult<()> {
144        // Check what type of pin exists
145        let pin_info = self.store.get(cid);
146
147        match pin_info {
148            Some(info) => match info.mode {
149                PinMode::Direct => {
150                    self.store.remove_direct(cid);
151                    Ok(())
152                }
153                PinMode::Recursive => {
154                    if !recursive {
155                        return Err(PinError::NotPinned(format!(
156                            "{} is pinned recursively, use recursive unpin",
157                            cid
158                        )));
159                    }
160
161                    // Collect all refs to remove indirect pins
162                    let mut refs = HashSet::new();
163                    let _ = self.collect_refs(cid, &mut refs);
164
165                    // Remove indirect pins
166                    for ref_cid in &refs {
167                        self.store.remove_indirect(ref_cid, cid);
168                    }
169
170                    // Remove the recursive pin
171                    self.store.remove_recursive(cid);
172                    Ok(())
173                }
174                PinMode::Indirect => Err(PinError::CannotUnpinIndirect(cid.to_string())),
175            },
176            None => Err(PinError::NotPinned(cid.to_string())),
177        }
178    }
179
180    fn get_pin(&self, cid: &Cid) -> PinResult<Option<PinInfo>> {
181        Ok(self.store.get(cid))
182    }
183
184    fn list_pins(&self, mode: Option<PinMode>) -> PinResult<Vec<PinInfo>> {
185        Ok(self.store.list(mode))
186    }
187
188    fn update_pin(&mut self, old: &Cid, new: &Cid, unpin: bool) -> PinResult<()> {
189        // Get the old pin info
190        let old_info = self
191            .store
192            .get(old)
193            .ok_or_else(|| PinError::NotPinned(old.to_string()))?;
194
195        // Verify new block exists
196        if !self.blockstore.has(new)? {
197            return Err(PinError::BlockNotFound(new.to_string()));
198        }
199
200        // Pin the new CID with the same mode and name
201        self.pin_with_name(new, old_info.mode, old_info.name)?;
202
203        // Optionally unpin the old CID
204        if unpin {
205            let recursive = old_info.mode == PinMode::Recursive;
206            self.unpin(old, recursive)?;
207        }
208
209        Ok(())
210    }
211
212    fn verify(&self) -> PinResult<Vec<(Cid, String)>> {
213        let mut errors = Vec::new();
214
215        // Check all direct and recursive pins
216        for pin in self.store.list(None) {
217            if pin.mode == PinMode::Indirect {
218                continue; // Skip indirect pins, they'll be verified via their parent
219            }
220
221            let cid =
222                Cid::try_from(pin.cid.as_str()).map_err(|e| PinError::CidParse(e.to_string()))?;
223
224            // Check if block exists
225            match self.blockstore.has(&cid) {
226                Ok(true) => {}
227                Ok(false) => {
228                    errors.push((cid, "block not found".to_string()));
229                }
230                Err(e) => {
231                    errors.push((cid, format!("error checking block: {}", e)));
232                }
233            }
234
235            // For recursive pins, verify all refs exist
236            if pin.mode == PinMode::Recursive {
237                let mut refs = HashSet::new();
238                if let Err(e) = self.collect_refs(&cid, &mut refs) {
239                    errors.push((cid, format!("error collecting refs: {}", e)));
240                } else {
241                    for ref_cid in refs {
242                        match self.blockstore.has(&ref_cid) {
243                            Ok(true) => {}
244                            Ok(false) => {
245                                errors.push((ref_cid, "referenced block not found".to_string()));
246                            }
247                            Err(e) => {
248                                errors.push((ref_cid, format!("error checking block: {}", e)));
249                            }
250                        }
251                    }
252                }
253            }
254        }
255
256        Ok(errors)
257    }
258
259    fn pinned_cids(&self) -> PinResult<HashSet<Cid>> {
260        let mut cids = HashSet::new();
261
262        for pin in self.store.list(None) {
263            let cid =
264                Cid::try_from(pin.cid.as_str()).map_err(|e| PinError::CidParse(e.to_string()))?;
265            cids.insert(cid);
266        }
267
268        Ok(cids)
269    }
270}
271
272/// Extract CID links from block data (DAG-PB format)
273fn extract_links(data: &[u8]) -> Result<Vec<Cid>, ()> {
274    use prost::Message;
275
276    // Try to decode as PBNode
277    #[derive(Clone, PartialEq, Message)]
278    struct PbLink {
279        #[prost(bytes, optional, tag = "1")]
280        hash: Option<Vec<u8>>,
281        #[prost(string, optional, tag = "2")]
282        name: Option<String>,
283        #[prost(uint64, optional, tag = "3")]
284        tsize: Option<u64>,
285    }
286
287    #[derive(Clone, PartialEq, Message)]
288    struct PbNode {
289        #[prost(message, repeated, tag = "2")]
290        links: Vec<PbLink>,
291        #[prost(bytes, optional, tag = "1")]
292        data: Option<Vec<u8>>,
293    }
294
295    let node = PbNode::decode(data).map_err(|_| ())?;
296
297    let mut cids = Vec::new();
298    for link in node.links {
299        if let Some(hash) = link.hash {
300            if let Ok(cid) = Cid::try_from(hash) {
301                cids.push(cid);
302            }
303        }
304    }
305
306    Ok(cids)
307}
308
309#[cfg(test)]
310mod tests {
311    use super::*;
312    use ferripfs_blockstore::{create_cid_v0, Block, FlatFsBlockstore};
313    use tempfile::tempdir;
314
315    fn create_test_block(data: &[u8]) -> Block {
316        let cid = create_cid_v0(data).unwrap();
317        Block::new(cid, data.to_vec())
318    }
319
320    #[test]
321    fn test_direct_pin() {
322        let dir = tempdir().unwrap();
323        let mut bs = FlatFsBlockstore::new_default(dir.path().join("blocks")).unwrap();
324
325        // Add a block
326        let block = create_test_block(b"test data");
327        let cid = *block.cid();
328        bs.put(block).unwrap();
329
330        // Create pinner
331        let store = PinStore::new();
332        let mut pinner = BlockstorePinner::new(&bs, store);
333
334        // Pin it
335        pinner.pin(&cid, PinMode::Direct).unwrap();
336
337        // Check it's pinned
338        assert!(pinner.is_pinned(&cid).unwrap());
339        assert!(pinner.is_pinned_with_mode(&cid, PinMode::Direct).unwrap());
340        assert!(!pinner
341            .is_pinned_with_mode(&cid, PinMode::Recursive)
342            .unwrap());
343
344        // Unpin it
345        pinner.unpin(&cid, false).unwrap();
346        assert!(!pinner.is_pinned(&cid).unwrap());
347    }
348
349    #[test]
350    fn test_pin_with_name() {
351        let dir = tempdir().unwrap();
352        let mut bs = FlatFsBlockstore::new_default(dir.path().join("blocks")).unwrap();
353
354        let block = create_test_block(b"test data");
355        let cid = *block.cid();
356        bs.put(block).unwrap();
357
358        let store = PinStore::new();
359        let mut pinner = BlockstorePinner::new(&bs, store);
360
361        pinner
362            .pin_with_name(&cid, PinMode::Direct, Some("my-pin".to_string()))
363            .unwrap();
364
365        let info = pinner.get_pin(&cid).unwrap().unwrap();
366        assert_eq!(info.name, Some("my-pin".to_string()));
367    }
368
369    #[test]
370    fn test_list_pins() {
371        let dir = tempdir().unwrap();
372        let mut bs = FlatFsBlockstore::new_default(dir.path().join("blocks")).unwrap();
373
374        let block1 = create_test_block(b"data 1");
375        let block2 = create_test_block(b"data 2");
376        let cid1 = *block1.cid();
377        let cid2 = *block2.cid();
378        bs.put(block1).unwrap();
379        bs.put(block2).unwrap();
380
381        let store = PinStore::new();
382        let mut pinner = BlockstorePinner::new(&bs, store);
383
384        pinner.pin(&cid1, PinMode::Direct).unwrap();
385        pinner.pin(&cid2, PinMode::Direct).unwrap();
386
387        let all_pins = pinner.list_pins(None).unwrap();
388        assert_eq!(all_pins.len(), 2);
389
390        let direct_pins = pinner.list_pins(Some(PinMode::Direct)).unwrap();
391        assert_eq!(direct_pins.len(), 2);
392    }
393}