agglayer_config/
storage.rs

1use std::path::{Component, Path, PathBuf};
2
3use agglayer_types::EpochNumber;
4use backup::BackupConfig;
5use serde::{Deserialize, Serialize};
6
7pub(crate) const STORAGE_DIR: &str = "storage";
8const METADATA_DB_NAME: &str = "metadata";
9const PENDING_DB_NAME: &str = "pending";
10const STATE_DB_NAME: &str = "state";
11const EPOCHS_DB_PATH: &str = "epochs";
12const DEBUG_DB_PATH: &str = "debug";
13
14pub mod backup;
15
16/// Configuration for the storage.
17#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
18#[serde(from = "StorageConfigHelper", into = "StorageConfigHelper")]
19#[serde(default)]
20pub struct StorageConfig {
21    /// Custom metadata storage path or inferred from the db path.
22    pub metadata_db_path: PathBuf,
23    /// Custom pending storage path or inferred from the db path.
24    pub pending_db_path: PathBuf,
25    /// Custom state storage path or inferred from the db path.
26    pub state_db_path: PathBuf,
27    /// Custom epochs storage path or inferred from the db path.
28    pub epochs_db_path: PathBuf,
29    /// Custom debug storage path or inferred from the db path.
30    pub debug_db_path: PathBuf,
31    /// Backup config
32    pub backup: BackupConfig,
33}
34
35impl Default for StorageConfig {
36    fn default() -> Self {
37        StorageConfig {
38            metadata_db_path: Path::new("./").join(STORAGE_DIR).join(METADATA_DB_NAME),
39            pending_db_path: Path::new("./").join(STORAGE_DIR).join(PENDING_DB_NAME),
40            state_db_path: Path::new("./").join(STORAGE_DIR).join(STATE_DB_NAME),
41            epochs_db_path: Path::new("./").join(STORAGE_DIR).join(EPOCHS_DB_PATH),
42            debug_db_path: Path::new("./").join(STORAGE_DIR).join(DEBUG_DB_PATH),
43            backup: BackupConfig::default(),
44        }
45    }
46}
47
48impl StorageConfig {
49    pub fn path_contextualized(mut self, base_path: &Path) -> Self {
50        self.metadata_db_path = normalize_path(&base_path.join(&self.metadata_db_path));
51        self.pending_db_path = normalize_path(&base_path.join(&self.pending_db_path));
52        self.state_db_path = normalize_path(&base_path.join(&self.state_db_path));
53        self.epochs_db_path = normalize_path(&base_path.join(&self.epochs_db_path));
54        self.debug_db_path = normalize_path(&base_path.join(&self.debug_db_path));
55
56        self
57    }
58
59    /// Creates a new storage configuration with the default path.
60    pub fn new_with_default_path() -> Self {
61        Self::new_from_path(&Path::new("./").join(STORAGE_DIR))
62    }
63
64    /// Creates a new storage configuration with the given path.
65    pub fn new_from_path(value: &Path) -> Self {
66        let db_path = value.join(STORAGE_DIR);
67
68        Self {
69            metadata_db_path: db_path.join(METADATA_DB_NAME),
70            pending_db_path: db_path.join(PENDING_DB_NAME),
71            state_db_path: db_path.join(STATE_DB_NAME),
72            epochs_db_path: db_path.join(EPOCHS_DB_PATH),
73            debug_db_path: db_path.join(DEBUG_DB_PATH),
74            backup: BackupConfig::default(),
75        }
76    }
77
78    pub fn epoch_db_path(&self, epoch_number: EpochNumber) -> PathBuf {
79        self.epochs_db_path.join(format!("{epoch_number}"))
80    }
81}
82
83/// Helper struct to deserialize the storage configuration.
84#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
85#[serde(rename_all = "kebab-case")]
86struct StorageConfigHelper {
87    #[serde(default = "default_db_path")]
88    db_path: PathBuf,
89    /// Custom metadata storage path or inferred from the db path.
90    pub metadata_db_path: Option<PathBuf>,
91    /// Custom pending storage path or inferred from the db path.
92    pub pending_db_path: Option<PathBuf>,
93    /// Custom state storage path or inferred from the db path.
94    pub state_db_path: Option<PathBuf>,
95    /// Custom epochs storage path or inferred from the db path.
96    pub epochs_db_path: Option<PathBuf>,
97    /// Custom debug storage path or inferred from the db path.
98    pub debug_db_path: Option<PathBuf>,
99    /// Backup config.
100    #[serde(default, skip_serializing_if = "BackupConfig::is_disabled")]
101    pub backup: BackupConfig,
102}
103
104impl From<StorageConfigHelper> for StorageConfig {
105    fn from(value: StorageConfigHelper) -> Self {
106        StorageConfig {
107            metadata_db_path: value
108                .metadata_db_path
109                .unwrap_or_else(|| value.db_path.join(METADATA_DB_NAME)),
110            pending_db_path: value
111                .pending_db_path
112                .unwrap_or_else(|| value.db_path.join(PENDING_DB_NAME)),
113            state_db_path: value
114                .state_db_path
115                .unwrap_or_else(|| value.db_path.join(STATE_DB_NAME)),
116            epochs_db_path: value
117                .epochs_db_path
118                .unwrap_or_else(|| value.db_path.join(EPOCHS_DB_PATH)),
119            debug_db_path: value
120                .debug_db_path
121                .unwrap_or_else(|| value.db_path.join(DEBUG_DB_PATH)),
122            backup: value.backup,
123        }
124    }
125}
126
127impl From<StorageConfig> for StorageConfigHelper {
128    fn from(value: StorageConfig) -> Self {
129        let db_path = value
130            .state_db_path
131            .parent()
132            .expect("Unable to define a base_path for the storage")
133            .to_path_buf();
134
135        Self {
136            db_path,
137            metadata_db_path: None,
138            pending_db_path: None,
139            state_db_path: None,
140            epochs_db_path: None,
141            debug_db_path: None,
142            backup: value.backup,
143        }
144    }
145}
146
147fn default_db_path() -> PathBuf {
148    PathBuf::new().join(STORAGE_DIR)
149}
150
151/// This function is extracted from `cargo`'s internal lib.
152///
153/// Link: https://github.com/rust-lang/cargo/blob/40ff7be1ad10d1947e22dfeb0f9fa8d2c26025a1/crates/cargo-util/src/paths.rs#L84
154///
155/// ## Explanation
156///
157/// Normalize a path, removing things like `.` and `..`.
158///
159/// CAUTION: This does not resolve symlinks (unlike
160/// [`std::fs::canonicalize`]). This may cause incorrect or surprising
161/// behavior at times. This should be used carefully. Unfortunately,
162/// [`std::fs::canonicalize`] can be hard to use correctly, since it can often
163/// fail, or on Windows returns annoying device paths. This is a problem Cargo
164/// needs to improve on.
165pub(crate) fn normalize_path(path: &Path) -> PathBuf {
166    let mut components = path.components().peekable();
167    let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
168        components.next();
169        PathBuf::from(c.as_os_str())
170    } else {
171        PathBuf::new()
172    };
173
174    for component in components {
175        match component {
176            Component::Prefix(..) => unreachable!(),
177            Component::RootDir => {
178                ret.push(component.as_os_str());
179            }
180            Component::CurDir => {}
181            Component::ParentDir => {
182                ret.pop();
183            }
184            Component::Normal(c) => {
185                ret.push(c);
186            }
187        }
188    }
189    ret
190}
191#[cfg(test)]
192mod tests {
193    use pretty_assertions::assert_eq;
194
195    use super::*;
196
197    #[test]
198    fn base_path() {
199        let value = toml::toml! {
200            db-path = "/tmp/base"
201        };
202
203        let cfg = toml::to_string(&value).unwrap();
204        let config: StorageConfig = toml::from_str(&cfg).unwrap();
205
206        assert_eq!(config.metadata_db_path, PathBuf::from("/tmp/base/metadata"));
207        assert_eq!(config.pending_db_path, PathBuf::from("/tmp/base/pending"));
208        assert_eq!(config.state_db_path, PathBuf::from("/tmp/base/state"));
209        assert_eq!(config.epochs_db_path, PathBuf::from("/tmp/base/epochs"));
210        assert_eq!(config.debug_db_path, PathBuf::from("/tmp/base/debug"));
211    }
212}