bp_core/storage/
file_registry.rs

1//! Persistent catalogue of files uploaded by this node.
2//!
3//! Every successful `bp put` call appends a [`StoredFileEntry`] to the
4//! local registry so that `bp ls` can enumerate all uploaded files without
5//! re-contacting the network.
6//!
7//! The registry is stored as a flat JSON array in
8//! `~/.local/share/billpouch/file_registry.json`.
9
10use crate::error::{BpError, BpResult};
11use serde::{Deserialize, Serialize};
12use std::path::Path;
13
14// ── StoredFileEntry ───────────────────────────────────────────────────────────
15
16/// Metadata for a single file uploaded by this node.
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct StoredFileEntry {
19    /// Display name of the file (filename or user-supplied alias).
20    pub file_name: String,
21    /// Original size in bytes (before chunking / encryption).
22    pub size_bytes: u64,
23    /// BLAKE3 chunk_id returned by `PutFile`.
24    pub chunk_id: String,
25    /// Network the file was uploaded to.
26    pub network_id: String,
27    /// Unix timestamp (seconds) when the file was uploaded.
28    pub uploaded_at: u64,
29}
30
31// ── FileRegistry ──────────────────────────────────────────────────────────────
32
33/// In-memory catalogue backed by `file_registry.json`.
34#[derive(Debug, Default)]
35pub struct FileRegistry {
36    entries: Vec<StoredFileEntry>,
37}
38
39impl FileRegistry {
40    /// Create an empty registry (not persisted until [`insert_and_save`](Self::insert_and_save) is called).
41    pub fn new() -> Self {
42        Self::default()
43    }
44
45    /// Load from `path`, returning an empty registry if the file is missing
46    /// or cannot be parsed (never fails hard).
47    pub fn load(path: &Path) -> Self {
48        let entries = std::fs::read_to_string(path)
49            .ok()
50            .and_then(|s| serde_json::from_str::<Vec<StoredFileEntry>>(&s).ok())
51            .unwrap_or_default();
52        Self { entries }
53    }
54
55    /// Persist to `path` as a pretty-printed JSON array.
56    pub fn save(&self, path: &Path) -> BpResult<()> {
57        if let Some(parent) = path.parent() {
58            std::fs::create_dir_all(parent).map_err(BpError::Io)?;
59        }
60        let json = serde_json::to_string_pretty(&self.entries)?;
61        std::fs::write(path, json).map_err(BpError::Io)
62    }
63
64    /// Append a new entry and immediately persist.
65    ///
66    /// Persistence failures are logged as warnings and not propagated so
67    /// that a registry write failure never aborts a successful `PutFile`.
68    pub fn insert_and_save(&mut self, entry: StoredFileEntry, path: &Path) {
69        self.entries.push(entry);
70        if let Err(e) = self.save(path) {
71            tracing::warn!("FileRegistry: failed to persist: {e}");
72        }
73    }
74
75    /// Return all entries, optionally filtered by `network_id`.
76    ///
77    /// Pass an empty string to get all entries across networks.
78    pub fn list(&self, network_id: &str) -> Vec<&StoredFileEntry> {
79        if network_id.is_empty() {
80            self.entries.iter().collect()
81        } else {
82            self.entries
83                .iter()
84                .filter(|e| e.network_id == network_id)
85                .collect()
86        }
87    }
88
89    /// Total bytes of all uploaded files (across all networks).
90    pub fn total_uploaded_bytes(&self) -> u64 {
91        self.entries.iter().map(|e| e.size_bytes).sum()
92    }
93
94    /// Total number of registered files.
95    pub fn len(&self) -> usize {
96        self.entries.len()
97    }
98
99    /// Return `true` if no files have been uploaded yet.
100    pub fn is_empty(&self) -> bool {
101        self.entries.is_empty()
102    }
103}