1use super::{
16 read_all_toml_files, ConfigStorage, Error, LoadConfigOptions, PingapConf,
17 Result,
18};
19use async_trait::async_trait;
20use futures_util::TryFutureExt;
21use std::path::Path;
22use tokio::fs;
23
24pub struct FileStorage {
25 path: String,
27 separation: bool,
29}
30impl FileStorage {
31 pub fn new(path: &str) -> Result<Self> {
33 let mut separation = false;
34 let mut filepath = pingap_util::resolve_path(path);
35 if let Some((path, query)) = path.split_once('?') {
37 let m = pingap_core::convert_query_map(query);
38 separation = m.contains_key("separation");
39 filepath = pingap_util::resolve_path(path);
40 }
41 if filepath.is_empty() {
43 return Err(Error::Invalid {
44 message: "Config path is empty".to_string(),
45 });
46 }
47
48 Ok(Self {
49 path: filepath,
50 separation,
51 })
52 }
53}
54
55#[async_trait]
56impl ConfigStorage for FileStorage {
57 async fn load_config(&self, opts: LoadConfigOptions) -> Result<PingapConf> {
59 let filepath = self.path.clone();
60 let dir = Path::new(&filepath);
61 if opts.admin && !dir.exists() {
63 return Ok(PingapConf::default());
64 }
65 if !filepath.ends_with(".toml") && !dir.exists() {
67 fs::create_dir_all(&filepath)
68 .map_err(|e| Error::Io {
69 source: e,
70 file: filepath.clone(),
71 })
72 .await?;
73 }
74
75 let mut data = vec![];
76 if dir.is_dir() {
78 let mut result = read_all_toml_files(&filepath).await?;
79 data.append(&mut result);
80 } else {
81 let mut buf = fs::read(&filepath).await.map_err(|e| Error::Io {
83 source: e,
84 file: filepath,
85 })?;
86 data.append(&mut buf);
87 }
88 PingapConf::new(data.as_slice(), opts.replace_include)
89 }
90 async fn save_config(
92 &self,
93 conf: &PingapConf,
94 category: &str,
95 name: Option<&str>,
96 ) -> Result<()> {
97 let filepath = self.path.clone();
98 conf.validate()?;
99 let path = Path::new(&filepath);
100 if !path.exists() && path.extension().unwrap_or_default() == "toml" {
101 fs::File::create(&path).await.map_err(|e| Error::Io {
102 source: e,
103 file: filepath.clone(),
104 })?;
105 }
106
107 if path.is_file() {
108 let ping_conf = toml::to_string_pretty(&conf)
113 .map_err(|e| Error::Ser { source: e })?;
114 let mut values: toml::Table = toml::from_str(&ping_conf)
115 .map_err(|e| Error::De { source: e })?;
116 let mut omit_keys = vec![];
118 for key in values.keys() {
119 if let Some(value) = values.get(key) {
120 if value.to_string() == "{}" {
121 omit_keys.push(key.clone());
122 }
123 }
124 }
125 for key in omit_keys {
126 values.remove(&key);
127 }
128 let ping_conf = toml::to_string_pretty(&values)
129 .map_err(|e| Error::Ser { source: e })?;
130 return fs::write(path, ping_conf).await.map_err(|e| Error::Io {
131 source: e,
132 file: filepath,
133 });
134 }
135
136 let (path, toml_value) = if self.separation && name.is_some() {
139 conf.get_toml(category, name)?
140 } else {
141 conf.get_toml(category, None)?
142 };
143
144 let filepath = pingap_util::path_join(&filepath, &path);
145 let target_file = Path::new(&filepath);
146 if let Some(p) = Path::new(&target_file).parent() {
147 fs::create_dir_all(p).await.map_err(|e| Error::Io {
148 source: e,
149 file: filepath.clone(),
150 })?;
151 }
152 if toml_value.is_empty() {
153 if target_file.exists() {
154 fs::remove_file(&filepath).await.map_err(|e| Error::Io {
155 source: e,
156 file: filepath,
157 })?;
158 }
159 } else {
160 fs::write(&filepath, toml_value)
161 .await
162 .map_err(|e| Error::Io {
163 source: e,
164 file: filepath,
165 })?;
166 }
167
168 Ok(())
169 }
170 async fn save(&self, key: &str, data: &[u8]) -> Result<()> {
171 let key = pingap_util::path_join(&self.path, key);
172 let path = Path::new(&key);
173 if let Some(p) = path.parent() {
174 fs::create_dir_all(p).await.map_err(|e| Error::Io {
175 source: e,
176 file: key.to_string(),
177 })?;
178 }
179 fs::write(path, data).await.map_err(|e| Error::Io {
180 source: e,
181 file: key.to_string(),
182 })?;
183 Ok(())
184 }
185 async fn load(&self, key: &str) -> Result<Vec<u8>> {
186 let key = pingap_util::path_join(&self.path, key);
187 let path = Path::new(&key);
188 let buf = fs::read(path).await.map_err(|e| Error::Io {
189 source: e,
190 file: key.to_string(),
191 })?;
192 Ok(buf)
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::FileStorage;
199 use crate::*;
200 use nanoid::nanoid;
201 use pretty_assertions::assert_eq;
202
203 #[tokio::test]
204 async fn test_file_storage() {
205 let result = FileStorage::new("");
206 assert_eq!(
207 "Invalid error Config path is empty",
208 result.err().unwrap().to_string()
209 );
210
211 let path = format!("/tmp/{}", nanoid!(16));
212 let file = format!("/tmp/{}.toml", nanoid!(16));
213 tokio::fs::write(&file, b"").await.unwrap();
214 let storage = FileStorage::new(&path).unwrap();
215 let file_storage = FileStorage::new(&file).unwrap();
216 let result = storage.load_config(LoadConfigOptions::default()).await;
217 assert_eq!(true, result.is_ok());
218
219 let toml_data = read_all_toml_files("../../conf").await.unwrap();
220 let conf =
221 PingapConf::new(toml_data.to_vec().as_slice(), false).unwrap();
222 for category in [
223 CATEGORY_CERTIFICATE.to_string(),
224 CATEGORY_UPSTREAM.to_string(),
225 CATEGORY_LOCATION.to_string(),
226 CATEGORY_SERVER.to_string(),
227 CATEGORY_PLUGIN.to_string(),
228 CATEGORY_STORAGE.to_string(),
229 CATEGORY_BASIC.to_string(),
230 ]
231 .iter()
232 {
233 storage.save_config(&conf, category, None).await.unwrap();
234 file_storage
235 .save_config(&conf, category, None)
236 .await
237 .unwrap();
238 }
239
240 let current_conf = storage
241 .load_config(LoadConfigOptions::default())
242 .await
243 .unwrap();
244 assert_eq!(current_conf.hash().unwrap(), conf.hash().unwrap());
245
246 let current_conf = file_storage
247 .load_config(LoadConfigOptions::default())
248 .await
249 .unwrap();
250 assert_eq!(current_conf.hash().unwrap(), conf.hash().unwrap());
251 }
252}