spin_plugins/badger/
store.rs

1use std::path::PathBuf;
2
3use anyhow::anyhow;
4use serde::{Deserialize, Serialize};
5
6const DEFAULT_STORE_DIR: &str = "spin";
7const DEFAULT_STORE_FILE: &str = "plugins-notifications.json";
8
9pub struct BadgerRecordManager {
10    db_path: PathBuf,
11}
12
13#[derive(Serialize, Deserialize)]
14pub struct BadgerRecord {
15    name: String,
16    badgered_from: semver::Version,
17    badgered_to: Vec<semver::Version>,
18    when: chrono::DateTime<chrono::Utc>,
19}
20
21pub enum PreviousBadger {
22    Fresh,
23    FromCurrent {
24        to: Vec<semver::Version>,
25        when: chrono::DateTime<chrono::Utc>,
26    },
27}
28
29impl PreviousBadger {
30    fn includes(&self, version: &semver::Version) -> bool {
31        match self {
32            Self::Fresh => false,
33            Self::FromCurrent { to, .. } => to.contains(version),
34        }
35    }
36
37    pub fn includes_any(&self, version: &[&semver::Version]) -> bool {
38        version.iter().any(|version| self.includes(version))
39    }
40}
41
42impl BadgerRecordManager {
43    pub fn default() -> anyhow::Result<Self> {
44        let base_dir = dirs::cache_dir()
45            .or_else(|| dirs::home_dir().map(|p| p.join(".spin")))
46            .ok_or_else(|| anyhow!("Unable to get local data directory or home directory"))?;
47        let db_path = base_dir.join(DEFAULT_STORE_DIR).join(DEFAULT_STORE_FILE);
48        Ok(Self { db_path })
49    }
50
51    fn load(&self) -> Vec<BadgerRecord> {
52        match std::fs::read(&self.db_path) {
53            Ok(v) => serde_json::from_slice(&v).unwrap_or_default(),
54            Err(_) => vec![], // There's no meaningful action or recovery, so swallow the error and treat the situation as fresh badger.
55        }
56    }
57
58    fn save(&self, records: Vec<BadgerRecord>) -> anyhow::Result<()> {
59        if let Some(dir) = self.db_path.parent() {
60            std::fs::create_dir_all(dir)?;
61        }
62        let json = serde_json::to_vec_pretty(&records)?;
63        std::fs::write(&self.db_path, json)?;
64        Ok(())
65    }
66
67    async fn last_badger(&self, name: &str) -> Option<BadgerRecord> {
68        self.load().into_iter().find(|r| r.name == name)
69    }
70
71    pub async fn previous_badger(
72        &self,
73        name: &str,
74        current_version: &semver::Version,
75    ) -> PreviousBadger {
76        match self.last_badger(name).await {
77            Some(b) if &b.badgered_from == current_version => PreviousBadger::FromCurrent {
78                to: b.badgered_to,
79                when: b.when,
80            },
81            _ => PreviousBadger::Fresh,
82        }
83    }
84
85    pub async fn record_badger(&self, name: &str, from: &semver::Version, to: &[&semver::Version]) {
86        let new = BadgerRecord {
87            name: name.to_owned(),
88            badgered_from: from.clone(),
89            badgered_to: to.iter().cloned().map(<semver::Version>::clone).collect(),
90            when: chrono::Utc::now(),
91        };
92
93        // There is a potential race condition here if someone runs two plugins at
94        // the same time. As this is unlikely, and the worst outcome is that a user
95        // misses a badger or gets a double badger, let's not worry about it for now.
96        let mut existing = self.load();
97        match existing.iter().position(|r| r.name == name) {
98            Some(index) => existing[index] = new,
99            None => existing.push(new),
100        };
101        _ = self.save(existing);
102    }
103}