fuel_core_chain_config/config/
snapshot_metadata.rs

1use anyhow::Context;
2use std::{
3    io::Read,
4    path::{
5        Path,
6        PathBuf,
7    },
8};
9
10#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
11pub enum TableEncoding {
12    Json {
13        filepath: PathBuf,
14    },
15    #[cfg(feature = "parquet")]
16    Parquet {
17        tables: std::collections::HashMap<String, PathBuf>,
18        latest_block_config_path: PathBuf,
19    },
20}
21impl TableEncoding {
22    #[allow(clippy::assigning_clones)] // False positive will be fixed in 1.81 Rust (https://github.com/rust-lang/rust-clippy/pull/12756)
23    fn strip_prefix(&mut self, dir: &Path) -> anyhow::Result<()> {
24        match self {
25            TableEncoding::Json { filepath } => {
26                *filepath = filepath.strip_prefix(dir)?.to_owned();
27            }
28            #[cfg(feature = "parquet")]
29            TableEncoding::Parquet {
30                tables,
31                latest_block_config_path,
32                ..
33            } => {
34                for path in tables.values_mut() {
35                    *path = path.strip_prefix(dir)?.to_owned();
36                }
37                *latest_block_config_path =
38                    latest_block_config_path.strip_prefix(dir)?.to_owned();
39            }
40        }
41        Ok(())
42    }
43
44    fn prepend_path(&mut self, dir: &Path) {
45        match self {
46            TableEncoding::Json { filepath } => {
47                *filepath = dir.join(&filepath);
48            }
49            #[cfg(feature = "parquet")]
50            TableEncoding::Parquet {
51                tables,
52                latest_block_config_path,
53                ..
54            } => {
55                for path in tables.values_mut() {
56                    *path = dir.join(&path);
57                }
58                *latest_block_config_path = dir.join(&latest_block_config_path);
59            }
60        }
61    }
62}
63
64#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
65pub struct SnapshotMetadata {
66    pub chain_config: PathBuf,
67    pub table_encoding: TableEncoding,
68}
69
70impl SnapshotMetadata {
71    const METADATA_FILENAME: &'static str = "metadata.json";
72    pub fn read(dir: impl AsRef<Path>) -> anyhow::Result<Self> {
73        let path = dir.as_ref().join(Self::METADATA_FILENAME);
74        let mut json = String::new();
75        std::fs::File::open(&path)
76            .with_context(|| format!("Could not open snapshot file: {path:?}"))?
77            .read_to_string(&mut json)?;
78        let mut snapshot: Self = serde_json::from_str(json.as_str())?;
79        snapshot.prepend_path(dir.as_ref());
80
81        Ok(snapshot)
82    }
83
84    #[allow(clippy::assigning_clones)] // False positive will be fixed in 1.81 Rust (https://github.com/rust-lang/rust-clippy/pull/12756)
85    fn strip_prefix(&mut self, dir: &Path) -> anyhow::Result<&mut Self> {
86        self.chain_config = self.chain_config.strip_prefix(dir)?.to_owned();
87        self.table_encoding.strip_prefix(dir)?;
88        Ok(self)
89    }
90
91    fn prepend_path(&mut self, dir: &Path) {
92        self.chain_config = dir.join(&self.chain_config);
93        self.table_encoding.prepend_path(dir);
94    }
95
96    pub fn write(mut self, dir: &Path) -> anyhow::Result<()> {
97        self.strip_prefix(dir)?;
98        let path = dir.join(Self::METADATA_FILENAME);
99        let file = std::fs::File::create(path)?;
100        serde_json::to_writer_pretty(file, &self)?;
101        Ok(())
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    mod json {
110        use super::*;
111
112        #[test]
113        fn directory_added_to_paths_upon_load() {
114            // given
115            let temp_dir = tempfile::tempdir().unwrap();
116            let dir = temp_dir.path();
117            let data = SnapshotMetadata {
118                chain_config: "some_chain_config.json".into(),
119                table_encoding: TableEncoding::Json {
120                    filepath: "some_state_file.json".into(),
121                },
122            };
123            serde_json::to_writer(
124                std::fs::File::create(dir.join("metadata.json")).unwrap(),
125                &data,
126            )
127            .unwrap();
128
129            // when
130            let snapshot = SnapshotMetadata::read(temp_dir.path()).unwrap();
131
132            // then
133            assert_eq!(
134                snapshot,
135                SnapshotMetadata {
136                    chain_config: dir.join("some_chain_config.json"),
137                    table_encoding: TableEncoding::Json {
138                        filepath: temp_dir.path().join("some_state_file.json"),
139                    }
140                }
141            );
142        }
143
144        #[test]
145        fn directory_removed_from_paths_upon_save() {
146            // given
147            let temp_dir = tempfile::tempdir().unwrap();
148            let dir = temp_dir.path();
149            let snapshot = SnapshotMetadata {
150                chain_config: dir.join("some_chain_config.json"),
151                table_encoding: TableEncoding::Json {
152                    filepath: dir.join("some_state_file.json"),
153                },
154            };
155
156            // when
157            snapshot.write(temp_dir.path()).unwrap();
158
159            // then
160            let data: SnapshotMetadata = serde_json::from_reader(
161                std::fs::File::open(temp_dir.path().join("metadata.json")).unwrap(),
162            )
163            .unwrap();
164            assert_eq!(
165                data,
166                SnapshotMetadata {
167                    chain_config: "some_chain_config.json".into(),
168                    table_encoding: TableEncoding::Json {
169                        filepath: "some_state_file.json".into(),
170                    }
171                }
172            );
173        }
174    }
175
176    #[cfg(feature = "parquet")]
177    mod parquet {
178        use super::*;
179        #[test]
180        fn directory_added_to_paths_upon_load() {
181            // given
182            let temp_dir = tempfile::tempdir().unwrap();
183            let dir = temp_dir.path();
184            let data = SnapshotMetadata {
185                chain_config: "some_chain_config.json".into(),
186                table_encoding: TableEncoding::Parquet {
187                    tables: std::collections::HashMap::from_iter(vec![(
188                        "coins".into(),
189                        "coins.parquet".into(),
190                    )]),
191                    latest_block_config_path: "latest_block_config.parquet".into(),
192                },
193            };
194            serde_json::to_writer(
195                std::fs::File::create(dir.join("metadata.json")).unwrap(),
196                &data,
197            )
198            .unwrap();
199
200            // when
201            let snapshot = SnapshotMetadata::read(temp_dir.path()).unwrap();
202
203            // then
204            assert_eq!(
205                snapshot,
206                SnapshotMetadata {
207                    chain_config: dir.join("some_chain_config.json"),
208                    table_encoding: TableEncoding::Parquet {
209                        tables: std::collections::HashMap::from_iter(vec![(
210                            "coins".into(),
211                            temp_dir.path().join("coins.parquet")
212                        )]),
213                        latest_block_config_path: temp_dir
214                            .path()
215                            .join("latest_block_config.parquet"),
216                    }
217                }
218            );
219        }
220
221        #[test]
222        fn directory_removed_from_paths_upon_save() {
223            // given
224            let temp_dir = tempfile::tempdir().unwrap();
225            let dir = temp_dir.path();
226            let snapshot = SnapshotMetadata {
227                chain_config: dir.join("some_chain_config.json"),
228                table_encoding: TableEncoding::Parquet {
229                    tables: std::collections::HashMap::from_iter([(
230                        "coins".into(),
231                        dir.join("coins.parquet"),
232                    )]),
233                    latest_block_config_path: dir.join("latest_block_config.parquet"),
234                },
235            };
236
237            // when
238            snapshot.write(temp_dir.path()).unwrap();
239
240            // then
241            let data: SnapshotMetadata = serde_json::from_reader(
242                std::fs::File::open(temp_dir.path().join("metadata.json")).unwrap(),
243            )
244            .unwrap();
245            assert_eq!(
246                data,
247                SnapshotMetadata {
248                    chain_config: "some_chain_config.json".into(),
249                    table_encoding: TableEncoding::Parquet {
250                        tables: std::collections::HashMap::from_iter([(
251                            "coins".into(),
252                            "coins.parquet".into(),
253                        )]),
254                        latest_block_config_path: "latest_block_config.parquet".into(),
255                    }
256                }
257            );
258        }
259    }
260}