ferripfs_pinning/
store.rs1use cid::Cid;
12use parking_lot::RwLock;
13use std::collections::{HashMap, HashSet};
14use std::path::Path;
15
16use crate::{PinInfo, PinMode, PinResult};
17
18#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
20struct PinState {
21 direct: HashMap<String, Option<String>>,
23 recursive: HashMap<String, Option<String>>,
25 indirect: HashMap<String, HashSet<String>>,
27}
28
29pub struct PinStore {
31 state: RwLock<PinState>,
32 path: Option<std::path::PathBuf>,
33}
34
35impl PinStore {
36 pub fn new() -> Self {
38 Self {
39 state: RwLock::new(PinState::default()),
40 path: None,
41 }
42 }
43
44 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 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 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 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 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 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 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 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 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 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 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 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 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 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 {
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 {
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}