1use std::path::Path;
2use std::{fmt, io};
3
4use spacetimedb_lib::ConnectionId;
5use spacetimedb_paths::cli::{ConfigDir, PrivKeyPath, PubKeyPath};
6use spacetimedb_paths::server::{ConfigToml, MetadataTomlPath};
7
8pub fn parse_config<T: serde::de::DeserializeOwned>(path: &Path) -> anyhow::Result<Option<T>> {
12 match std::fs::read_to_string(path) {
13 Ok(contents) => Ok(Some(toml::from_str(&contents)?)),
14 Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
15 Err(e) => Err(e.into()),
16 }
17}
18
19#[derive(serde::Serialize, serde::Deserialize, Debug)]
20pub struct MetadataFile {
21 pub version: semver::Version,
22 pub edition: String,
23
24 #[serde(skip_serializing_if = "Option::is_none")]
25 pub client_connection_id: Option<ConnectionId>,
28}
29
30impl MetadataFile {
31 pub fn new(edition: &str) -> Self {
32 let mut current_version: semver::Version = env!("CARGO_PKG_VERSION").parse().unwrap();
33 if let semver::Version { major: 1, minor: 0, .. } = current_version {
40 current_version.patch = 0;
41 }
42 Self {
43 version: current_version,
44 edition: edition.to_owned(),
45 client_connection_id: None,
46 }
47 }
48
49 pub fn read(path: &MetadataTomlPath) -> anyhow::Result<Option<Self>> {
50 parse_config(path.as_ref())
51 }
52
53 pub fn write(&self, path: &MetadataTomlPath) -> io::Result<()> {
54 path.write(self.to_string())
55 }
56
57 pub fn check_compatibility_and_update(mut self, current: Self) -> anyhow::Result<Self> {
65 anyhow::ensure!(
66 self.edition == current.edition,
67 "metadata.toml indicates that this database is from a different \
68 edition of SpacetimeDB (running {:?}, but this database is {:?})",
69 current.edition,
70 self.edition,
71 );
72 let cmp = semver::Comparator {
73 op: semver::Op::Caret,
74 major: self.version.major,
75 minor: Some(self.version.minor),
76 patch: None,
77 pre: self.version.pre.clone(),
78 };
79 anyhow::ensure!(
80 cmp.matches(¤t.version),
81 "metadata.toml indicates that this database is from a newer, \
82 incompatible version of SpacetimeDB (running {:?}, but this \
83 database is from {:?})",
84 current.version,
85 self.version,
86 );
87 self.version = std::cmp::max(self.version, current.version);
91 Ok(self)
92 }
93}
94
95impl fmt::Display for MetadataFile {
96 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97 writeln!(f, "# THIS FILE IS GENERATED BY SPACETIMEDB, DO NOT MODIFY!")?;
98 writeln!(f)?;
99 f.write_str(&toml::to_string(self).unwrap())
100 }
101}
102
103#[derive(serde::Deserialize, Default)]
104#[serde(rename_all = "kebab-case")]
105pub struct ConfigFile {
106 #[serde(default)]
107 pub certificate_authority: Option<CertificateAuthority>,
108 #[serde(default)]
109 pub logs: LogConfig,
110}
111
112impl ConfigFile {
113 pub fn read(path: &ConfigToml) -> anyhow::Result<Option<Self>> {
114 parse_config(path.as_ref())
115 }
116}
117
118#[derive(serde::Deserialize)]
119#[serde(rename_all = "kebab-case")]
120pub struct CertificateAuthority {
121 pub jwt_priv_key_path: PrivKeyPath,
122 pub jwt_pub_key_path: PubKeyPath,
123}
124
125impl CertificateAuthority {
126 pub fn in_cli_config_dir(dir: &ConfigDir) -> Self {
127 Self {
128 jwt_priv_key_path: dir.jwt_priv_key(),
129 jwt_pub_key_path: dir.jwt_pub_key(),
130 }
131 }
132
133 pub fn get_or_create_keys(&self) -> anyhow::Result<crate::auth::JwtKeys> {
134 crate::auth::get_or_create_keys(self)
135 }
136}
137
138#[serde_with::serde_as]
139#[derive(Clone, serde::Deserialize, Default)]
140#[serde(rename_all = "kebab-case")]
141pub struct LogConfig {
142 #[serde_as(as = "Option<serde_with::DisplayFromStr>")]
143 pub level: Option<tracing_core::LevelFilter>,
144 #[serde(default)]
145 pub directives: Vec<String>,
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151
152 fn mkver(major: u64, minor: u64, patch: u64) -> semver::Version {
153 semver::Version::new(major, minor, patch)
154 }
155
156 fn mkmeta(major: u64, minor: u64, patch: u64) -> MetadataFile {
157 MetadataFile {
158 version: mkver(major, minor, patch),
159 edition: "standalone".to_owned(),
160 client_connection_id: None,
161 }
162 }
163
164 #[test]
165 fn check_metadata_compatibility_checking() {
166 assert_eq!(
167 mkmeta(1, 0, 0)
168 .check_compatibility_and_update(mkmeta(1, 0, 1))
169 .unwrap()
170 .version,
171 mkver(1, 0, 1)
172 );
173 assert_eq!(
174 mkmeta(1, 0, 1)
175 .check_compatibility_and_update(mkmeta(1, 0, 0))
176 .unwrap()
177 .version,
178 mkver(1, 0, 1)
179 );
180
181 mkmeta(1, 1, 0)
182 .check_compatibility_and_update(mkmeta(1, 0, 5))
183 .unwrap_err();
184 mkmeta(2, 0, 0)
185 .check_compatibility_and_update(mkmeta(1, 3, 5))
186 .unwrap_err();
187 }
188}