1use std::collections::BTreeMap;
2use std::path::{Path, PathBuf};
3
4use serde::{Deserialize, Serialize};
5
6use crate::record::SecretInfo;
7use crate::store::StoreError;
8
9#[derive(Debug, Default, Serialize, Deserialize)]
11pub struct Index {
12 pub version: u32,
13 pub entries: BTreeMap<String, BTreeMap<String, SecretInfo>>,
15}
16
17impl Index {
18 pub fn path(root: &Path) -> PathBuf {
19 root.join("index.json")
20 }
21
22 pub fn load(root: &Path) -> Result<Self, StoreError> {
23 let p = Self::path(root);
24 if !p.exists() {
25 return Ok(Self {
26 version: 1,
27 entries: BTreeMap::new(),
28 });
29 }
30 let text = std::fs::read_to_string(p)?;
31 serde_json::from_str(&text).map_err(|e| StoreError::Encoding(e.to_string()))
32 }
33
34 pub fn save(&self, root: &Path) -> Result<(), StoreError> {
35 create_dir_private(root)?;
36 let text =
37 serde_json::to_string_pretty(self).map_err(|e| StoreError::Encoding(e.to_string()))?;
38 write_private(&Self::path(root), text.as_bytes())
39 }
40
41 pub fn upsert(&mut self, component: &str, info: SecretInfo) {
42 self.entries
43 .entry(component.to_string())
44 .or_default()
45 .insert(info.key.clone(), info);
46 }
47
48 pub fn remove(&mut self, component: &str, key: &str) {
49 if let Some(m) = self.entries.get_mut(component) {
50 m.remove(key);
51 if m.is_empty() {
52 self.entries.remove(component);
53 }
54 }
55 }
56
57 pub fn list(&self, component: Option<&str>) -> Vec<SecretInfo> {
58 match component {
59 Some(c) => self
60 .entries
61 .get(c)
62 .map(|m| m.values().cloned().collect())
63 .unwrap_or_default(),
64 None => self
65 .entries
66 .values()
67 .flat_map(|m| m.values().cloned())
68 .collect(),
69 }
70 }
71}
72
73pub fn create_dir_private(dir: &Path) -> Result<(), StoreError> {
89 std::fs::create_dir_all(dir)?;
90 #[cfg(unix)]
91 {
92 use std::os::unix::fs::PermissionsExt;
93 std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o700))?;
94 }
95 Ok(())
96}
97
98pub fn write_private(path: &Path, bytes: &[u8]) -> Result<(), StoreError> {
112 use std::io::Write;
113
114 let dir = path.parent().unwrap_or_else(|| Path::new("."));
115 create_dir_private(dir)?;
116 let tmp = path.with_extension("tmp");
117
118 match std::fs::remove_file(&tmp) {
119 Ok(()) => {}
120 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
121 Err(e) => return Err(e.into()),
122 }
123
124 let mut opts = std::fs::OpenOptions::new();
125 opts.write(true).create_new(true);
126 #[cfg(unix)]
127 {
128 use std::os::unix::fs::OpenOptionsExt;
129 opts.mode(0o600);
130 }
131 let mut f = opts.open(&tmp)?;
132 f.write_all(bytes)?;
133 drop(f);
134
135 std::fs::rename(&tmp, path)?;
136 Ok(())
137}