remote_files/
configuration.rs1use crate::{
2 buckets::{GCSBucket, S3Bucket},
3 client::Client,
4 error::{self, StoredError},
5};
6use serde::{de::DeserializeOwned, Deserialize, Serialize};
7use std::{
8 collections::HashMap,
9 path::{Path, PathBuf},
10};
11use tokio::{
12 fs::{File, OpenOptions},
13 io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt},
14};
15
16pub static CONFIGURATION_FILEPATH_ENV_VAR: &str = "RF_CFG_FILEPATH";
17
18pub fn get_default_folder() -> Result<PathBuf, StoredError> {
19 dirs::config_dir()
20 .map(|pb| pb.join("rf"))
21 .ok_or(StoredError::Initialization(
22 "cannot access configuration directory".to_string(),
23 ))
24}
25
26async fn open_rw_fd<P>(path: P) -> Result<File, StoredError>
27where
28 P: AsRef<Path>,
29{
30 let fd = OpenOptions::new()
31 .read(true)
32 .write(true)
33 .create(true)
34 .truncate(false)
35 .open(path)
36 .await?;
37
38 Ok(fd)
39}
40
41async fn read<D>(file: &mut File) -> Result<D, StoredError>
42where
43 D: DeserializeOwned,
44{
45 let mut buffer = String::new();
46 file.read_to_string(&mut buffer).await?;
47
48 if buffer.is_empty() {
49 buffer.clear();
50 buffer.push_str("{}");
51 }
52
53 let configuration = serde_json::from_str(&buffer)?;
54
55 Ok(configuration)
56}
57
58#[derive(Debug, Clone, Deserialize, Serialize)]
59pub struct Persistence {
60 #[serde(skip_serializing_if = "Option::is_none")]
61 pub current: Option<String>,
62}
63
64#[derive(Debug, Clone, Deserialize, Serialize)]
65#[serde(tag = "type")]
66pub enum Bucket {
67 #[serde(rename = "gcs")]
68 Gcs(GCSBucket),
69 #[serde(rename = "s3")]
70 S3(S3Bucket),
71}
72
73pub type Configuration = HashMap<String, Bucket>;
74
75pub fn create_client(profile: &str, cfg: &Configuration) -> Result<Option<Client>, error::Client> {
76 if let Some(bucket) = cfg.get(profile) {
77 let client: Client = match bucket {
78 Bucket::Gcs(gcs) => gcs.configuration.clone().try_into()?,
79 Bucket::S3(s3) => s3.configuration.clone().try_into()?, };
82
83 Ok(Some(client))
84 } else {
85 Ok(None)
86 }
87}
88
89pub struct Stored<T>
90where
91 T: DeserializeOwned + Serialize,
92{
93 inner: T,
94 fd: File,
95}
96
97impl<T> Stored<T>
98where
99 T: DeserializeOwned + Serialize,
100{
101 pub fn get(&self) -> &T {
102 &self.inner
103 }
104
105 pub fn get_mut(&mut self) -> &mut T {
106 &mut self.inner
107 }
108
109 pub async fn persist(mut self) -> Result<(), StoredError> {
110 let content = serde_json::to_string_pretty(&self.inner)?;
111 let bytes = content.as_bytes();
112
113 self.fd.rewind().await?;
114 self.fd.set_len(content.len() as u64).await?;
115 self.fd.write_all(bytes).await?;
116 self.fd.flush().await?;
117
118 Ok(())
119 }
120}
121
122pub type PersistenceLayer = Stored<Persistence>;
123
124impl PersistenceLayer {
125 pub async fn try_init(value: Option<&Path>) -> Result<Self, StoredError> {
126 let main_folder = get_default_folder()?;
127
128 let default_persistence_filepath = main_folder.join("rf.json");
129 let persistence_filepath = value
130 .map(PathBuf::from)
131 .unwrap_or(default_persistence_filepath);
132
133 let mut persistence_fd = open_rw_fd(persistence_filepath.as_path()).await?;
134
135 Ok(PersistenceLayer {
136 inner: read(&mut persistence_fd).await?,
137 fd: persistence_fd,
138 })
139 }
140}
141
142pub type ConfigurationLayer = Stored<Configuration>;
143
144impl ConfigurationLayer {
145 pub async fn try_init(value: Option<&Path>) -> Result<Self, StoredError> {
146 let main_folder = get_default_folder()?;
147
148 let default_cfg_filepath = main_folder.join("configuration.json");
149 let cfg_filepath = value.map(PathBuf::from).unwrap_or(default_cfg_filepath);
150
151 let mut cfg_fd = open_rw_fd(cfg_filepath.as_path()).await?;
152
153 Ok(ConfigurationLayer {
154 inner: read(&mut cfg_fd).await?,
155 fd: cfg_fd,
156 })
157 }
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163 use std::{
164 env, error, fs,
165 path::{Path, PathBuf},
166 };
167 use tokio::fs::OpenOptions;
168 use uuid::Uuid;
169
170 struct TmpDir(PathBuf);
171
172 impl TmpDir {
173 fn create_tmp_dir() -> Self {
174 let id = Uuid::new_v4().to_string();
175 let tmp_dir = env::temp_dir().join(id.as_str());
176
177 fs::create_dir(&tmp_dir).unwrap();
178
179 TmpDir(tmp_dir)
180 }
181
182 async fn add_file<P>(
183 &self,
184 path: P,
185 content: &str,
186 ) -> Result<PathBuf, Box<dyn error::Error>>
187 where
188 P: AsRef<Path>,
189 {
190 let dst = self.0.join(path);
191 let mut fd = OpenOptions::new()
192 .create(true)
193 .truncate(true)
194 .write(true)
195 .open(&dst)
196 .await?;
197
198 fd.write_all(content.as_bytes()).await?;
199 fd.flush().await?;
200
201 Ok(dst)
202 }
203 }
204
205 impl Drop for TmpDir {
206 fn drop(&mut self) {
207 fs::remove_dir_all(&self.0)
208 .unwrap_or_else(|_| panic!("cannot cleanup temp dir '{}'", self.0.display()));
209 }
210 }
211
212 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
213 async fn should_load_a_valid_configuration() {
214 let dir = TmpDir::create_tmp_dir();
215 let cfg = r#"{
216 "gcs":{
217 "type": "gcs",
218 "configuration": {
219 "name": "my-gcs-bucket"
220 }
221 }
222 }"#;
223
224 let cfg_path = dir.add_file("configuration.json", cfg).await.unwrap();
225 let cfg_layer = ConfigurationLayer::try_init(Some(&cfg_path)).await.unwrap();
226 let cfg = cfg_layer.get();
227
228 assert!(cfg.contains_key("gcs"));
229 }
230
231 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
232 async fn should_persist_new_configuration() {
233 let dir = TmpDir::create_tmp_dir();
234 let cfg_path = dir.add_file("configuration.json", r#"{}"#).await.unwrap();
235
236 let mut state_layer = PersistenceLayer::try_init(Some(&cfg_path)).await.unwrap();
238 let state = state_layer.get_mut();
239
240 state.current = Some(String::from("gcs"));
242 state_layer.persist().await.unwrap();
243
244 let new_state_layer = PersistenceLayer::try_init(Some(&cfg_path)).await.unwrap();
245 assert_eq!(Some("gcs"), new_state_layer.get().current.as_deref());
246 }
247}