bp_core/storage/
meta.rs

1//! `PouchMeta` — on-disk metadata for a Pouch storage bid.
2//!
3//! Serialised as `meta.json` inside
4//! `~/.local/share/billpouch/storage/<network_id>/<service_id>/meta.json`.
5
6use crate::error::{BpError, BpResult};
7use serde::{Deserialize, Serialize};
8use std::path::Path;
9
10/// Persistent metadata for a Pouch storage bid.
11///
12/// Written when the Pouch service is first hatched and updated whenever
13/// a fragment is stored or removed.
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct PouchMeta {
16    /// Network this Pouch is participating in.
17    pub network_id: String,
18    /// UUID of the Pouch service instance.
19    pub service_id: String,
20    /// Bytes offered to the network at bid time.
21    pub storage_bytes_bid: u64,
22    /// Bytes currently occupied by stored fragments.
23    pub storage_bytes_used: u64,
24    /// Unix timestamp (seconds) when this Pouch joined.
25    pub joined_at: u64,
26}
27
28impl PouchMeta {
29    /// Create a new meta record with `storage_bytes_used = 0`.
30    pub fn new(network_id: String, service_id: String, storage_bytes_bid: u64) -> Self {
31        Self {
32            network_id,
33            service_id,
34            storage_bytes_bid,
35            storage_bytes_used: 0,
36            joined_at: chrono::Utc::now().timestamp() as u64,
37        }
38    }
39
40    /// Bytes still available for new fragments.
41    pub fn available_bytes(&self) -> u64 {
42        self.storage_bytes_bid
43            .saturating_sub(self.storage_bytes_used)
44    }
45
46    /// Whether the Pouch can accept `bytes` more data.
47    pub fn has_capacity(&self, bytes: u64) -> bool {
48        self.available_bytes() >= bytes
49    }
50
51    /// Load from a `meta.json` file.
52    pub fn load(path: &Path) -> BpResult<Self> {
53        let json = std::fs::read_to_string(path).map_err(BpError::Io)?;
54        serde_json::from_str(&json).map_err(BpError::Serde)
55    }
56
57    /// Persist to a `meta.json` file (creates or overwrites).
58    pub fn save(&self, path: &Path) -> BpResult<()> {
59        let json = serde_json::to_string_pretty(self)?;
60        std::fs::write(path, json).map_err(BpError::Io)
61    }
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67
68    #[test]
69    fn available_bytes_correct() {
70        let mut m = PouchMeta::new("net".into(), "svc".into(), 1000);
71        assert_eq!(m.available_bytes(), 1000);
72        m.storage_bytes_used = 400;
73        assert_eq!(m.available_bytes(), 600);
74    }
75
76    #[test]
77    fn has_capacity_boundary() {
78        let mut m = PouchMeta::new("net".into(), "svc".into(), 100);
79        assert!(m.has_capacity(100));
80        assert!(!m.has_capacity(101));
81        m.storage_bytes_used = 50;
82        assert!(m.has_capacity(50));
83        assert!(!m.has_capacity(51));
84    }
85
86    #[test]
87    #[cfg(unix)]
88    fn save_and_load_roundtrip() {
89        let dir =
90            std::env::temp_dir().join(format!("bp_meta_test_{}", uuid::Uuid::new_v4().simple()));
91        std::fs::create_dir_all(&dir).unwrap();
92        let path = dir.join("meta.json");
93
94        let meta = PouchMeta::new("amici".into(), "svc-uuid".into(), 10_737_418_240);
95        meta.save(&path).unwrap();
96
97        let loaded = PouchMeta::load(&path).unwrap();
98        assert_eq!(loaded.network_id, "amici");
99        assert_eq!(loaded.storage_bytes_bid, 10_737_418_240);
100        assert_eq!(loaded.storage_bytes_used, 0);
101
102        std::fs::remove_dir_all(dir).ok();
103    }
104}