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}