rvlib/
cfg.rs

1use crate::{
2    cache::FileCacheCfgArgs,
3    file_util::{self, DEFAULT_PRJ_PATH, DEFAULT_TMPDIR},
4    sort_params::SortParams,
5    ssh,
6};
7use rvimage_domain::{rverr, to_rv, RvResult};
8use serde::{de::DeserializeOwned, Deserialize, Serialize};
9use std::{
10    fmt::Debug,
11    fs,
12    path::{Path, PathBuf},
13};
14use tracing::{info, warn};
15
16#[cfg(feature = "azure_blob")]
17#[derive(Deserialize, Serialize, Debug, Default, Clone, PartialEq, Eq)]
18pub struct AzureBlobCfgLegacy {
19    pub connection_string_path: String,
20    pub container_name: String,
21    pub prefix: String,
22}
23#[derive(Deserialize, Serialize, Debug, Default, Clone, PartialEq, Eq)]
24pub struct SshCfgLegacy {
25    pub user: String,
26    pub ssh_identity_file_path: String,
27    n_reconnection_attempts: Option<usize>,
28    pub remote_folder_paths: Vec<String>,
29    pub address: String,
30}
31#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq, Default)]
32pub struct CfgLegacy {
33    pub connection: Connection,
34    pub cache: Cache,
35    http_address: Option<String>,
36    tmpdir: Option<String>,
37    current_prj_path: Option<PathBuf>,
38    pub file_cache_args: Option<FileCacheCfgArgs>,
39    pub ssh_cfg: SshCfgLegacy,
40    pub home_folder: Option<String>,
41    pub py_http_reader_cfg: Option<PyHttpReaderCfg>,
42    pub darkmode: Option<bool>,
43    pub n_autosaves: Option<u8>,
44    pub import_old_path: Option<String>,
45    pub import_new_path: Option<String>,
46    #[cfg(feature = "azure_blob")]
47    pub azure_blob_cfg: Option<AzureBlobCfgLegacy>,
48}
49impl CfgLegacy {
50    pub fn to_cfg(self) -> Cfg {
51        let usr = CfgUsr {
52            darkmode: self.darkmode,
53            n_autosaves: self.n_autosaves,
54            home_folder: self.home_folder,
55            cache: self.cache,
56            tmpdir: self.tmpdir,
57            current_prj_path: self.current_prj_path,
58            file_cache_args: self.file_cache_args.unwrap_or_default(),
59            ssh: SshCfgUsr {
60                user: self.ssh_cfg.user,
61                ssh_identity_file_path: self.ssh_cfg.ssh_identity_file_path,
62                n_reconnection_attempts: self.ssh_cfg.n_reconnection_attempts,
63            },
64        };
65        let prj = CfgPrj {
66            connection: self.connection,
67            http_address: self.http_address,
68            py_http_reader_cfg: self.py_http_reader_cfg,
69            ssh: SshCfgPrj {
70                remote_folder_paths: self.ssh_cfg.remote_folder_paths,
71                address: self.ssh_cfg.address,
72            },
73            azure_blob: self.azure_blob_cfg.map(|ab| AzureBlobCfgPrj {
74                connection_string_path: ab.connection_string_path,
75                container_name: ab.container_name,
76                prefix: ab.prefix,
77            }),
78            sort_params: SortParams::default(),
79        };
80        Cfg { usr, prj }
81    }
82}
83
84pub fn get_cfg_path_legacy(homefolder: &Path) -> PathBuf {
85    homefolder.join("rv_cfg.toml")
86}
87
88pub fn get_cfg_path_usr(homefolder: &Path) -> PathBuf {
89    homefolder.join("rv_cfg_usr.toml")
90}
91
92pub fn get_cfg_path_prj(homefolder: &Path) -> PathBuf {
93    homefolder.join("rv_cfg_prjtmp.toml")
94}
95
96pub fn get_cfg_tmppath(cfg: &Cfg) -> PathBuf {
97    Path::new(cfg.tmpdir())
98        .join(".rvimage")
99        .join("rv_cfg_tmp.toml")
100}
101
102pub fn get_log_folder(homefolder: &Path) -> PathBuf {
103    homefolder.join("logs")
104}
105
106pub fn read_cfg_gen<CFG: Debug + DeserializeOwned + Default>(
107    cfg_toml_path: &Path,
108) -> RvResult<CFG> {
109    if cfg_toml_path.exists() {
110        let toml_str = file_util::read_to_string(cfg_toml_path)?;
111        toml::from_str(&toml_str).map_err(|e| rverr!("could not parse cfg due to {:?}", e))
112    } else {
113        warn!("cfg {cfg_toml_path:?} file does not exist. using default cfg");
114        Ok(CFG::default())
115    }
116}
117
118fn read_cfg_from_paths(
119    cfg_toml_path_usr: &Path,
120    cfg_toml_path_prj: &Path,
121    cfg_toml_path_legacy: &Path,
122) -> RvResult<Cfg> {
123    if cfg_toml_path_usr.exists() || cfg_toml_path_prj.exists() {
124        let usr = read_cfg_gen::<CfgUsr>(cfg_toml_path_usr)?;
125        let prj = read_cfg_gen::<CfgPrj>(cfg_toml_path_prj)?;
126        Ok(Cfg { usr, prj })
127    } else if cfg_toml_path_legacy.exists() {
128        tracing::warn!("using legacy cfg file {cfg_toml_path_legacy:?}");
129        let legacy = read_cfg_gen::<CfgLegacy>(cfg_toml_path_legacy)?;
130        Ok(legacy.to_cfg())
131    } else {
132        tracing::info!("no cfg file found. using default cfg");
133        Ok(Cfg::default())
134    }
135}
136
137pub fn write_cfg_str(cfg_str: &str, p: &Path, log: bool) -> RvResult<()> {
138    file_util::write(p, cfg_str)?;
139    if log {
140        info!("wrote cfg to {p:?}");
141    }
142    Ok(())
143}
144
145#[derive(Deserialize, Serialize, Debug, Eq, PartialEq, Clone, Copy, Default)]
146pub enum Connection {
147    Ssh,
148    PyHttp,
149    #[cfg(feature = "azure_blob")]
150    AzureBlob,
151    #[default]
152    Local,
153}
154#[derive(Deserialize, Serialize, Debug, Eq, PartialEq, Clone, Default)]
155pub enum Cache {
156    #[default]
157    FileCache,
158    NoCache,
159}
160#[derive(Deserialize, Serialize, Debug, Default, Clone, PartialEq, Eq)]
161pub struct SshCfgUsr {
162    pub user: String,
163    pub ssh_identity_file_path: String,
164    n_reconnection_attempts: Option<usize>,
165}
166#[derive(Deserialize, Serialize, Debug, Default, Clone, PartialEq, Eq)]
167pub struct SshCfgPrj {
168    pub remote_folder_paths: Vec<String>,
169    pub address: String,
170}
171#[derive(Deserialize, Serialize, Debug, Default, Clone, PartialEq, Eq)]
172pub struct SshCfg {
173    pub usr: SshCfgUsr,
174    pub prj: SshCfgPrj,
175}
176impl SshCfg {
177    pub fn n_reconnection_attempts(&self) -> usize {
178        let default = 5;
179        self.usr.n_reconnection_attempts.unwrap_or(default)
180    }
181}
182
183#[cfg(feature = "azure_blob")]
184#[derive(Deserialize, Serialize, Debug, Default, Clone, PartialEq, Eq)]
185pub struct AzureBlobCfgPrj {
186    #[serde(default)]
187    pub connection_string_path: String,
188    pub container_name: String,
189    pub prefix: String,
190}
191
192#[cfg(feature = "azure_blob")]
193#[derive(Deserialize, Serialize, Debug, Default, Clone, PartialEq, Eq)]
194pub struct AzureBlobCfg {
195    pub prj: AzureBlobCfgPrj,
196}
197
198#[derive(Deserialize, Serialize, Debug, Default, Clone, PartialEq, Eq)]
199pub struct PyHttpReaderCfg {
200    pub server_addresses: Vec<String>,
201}
202
203#[derive(Deserialize, Serialize, Default, Clone, Debug, PartialEq, Eq)]
204pub enum ExportPathConnection {
205    Ssh,
206    #[default]
207    Local,
208}
209impl ExportPathConnection {
210    pub fn write_bytes(
211        &self,
212        data: &[u8],
213        dst_path: &Path,
214        ssh_cfg: Option<&SshCfg>,
215    ) -> RvResult<()> {
216        match (self, ssh_cfg) {
217            (ExportPathConnection::Ssh, Some(ssh_cfg)) => {
218                let sess = ssh::auth(ssh_cfg)?;
219                ssh::write_bytes(data, dst_path, &sess).map_err(to_rv)?;
220                Ok(())
221            }
222            (ExportPathConnection::Local, _) => {
223                file_util::write(dst_path, data)?;
224                Ok(())
225            }
226            (ExportPathConnection::Ssh, None) => Err(rverr!("cannot save to ssh. config missing")),
227        }
228    }
229    pub fn write(&self, data_str: &str, dst_path: &Path, ssh_cfg: Option<&SshCfg>) -> RvResult<()> {
230        self.write_bytes(data_str.as_bytes(), dst_path, ssh_cfg)
231    }
232}
233#[derive(Deserialize, Serialize, Default, Clone, Debug, PartialEq, Eq)]
234pub struct ExportPath {
235    pub path: PathBuf,
236    pub conn: ExportPathConnection,
237}
238
239pub enum Style {
240    Dark,
241    Light,
242}
243
244fn get_default_n_autosaves() -> Option<u8> {
245    Some(2)
246}
247
248#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq, Default)]
249pub struct CfgUsr {
250    pub darkmode: Option<bool>,
251    #[serde(default = "get_default_n_autosaves")]
252    pub n_autosaves: Option<u8>,
253
254    // This is only variable to make tests not override your config.
255    // You shall not change this when actually running RV Image.
256    pub home_folder: Option<String>,
257
258    pub cache: Cache,
259    tmpdir: Option<String>,
260    current_prj_path: Option<PathBuf>,
261    #[serde(default)]
262    pub file_cache_args: FileCacheCfgArgs,
263    pub ssh: SshCfgUsr,
264}
265#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq, Default)]
266pub struct CfgPrj {
267    pub py_http_reader_cfg: Option<PyHttpReaderCfg>,
268    pub connection: Connection,
269    http_address: Option<String>,
270    pub ssh: SshCfgPrj,
271    #[cfg(feature = "azure_blob")]
272    pub azure_blob: Option<AzureBlobCfgPrj>,
273    #[serde(default)]
274    pub sort_params: SortParams,
275}
276#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
277pub struct Cfg {
278    pub usr: CfgUsr,
279    pub prj: CfgPrj,
280}
281
282impl Cfg {
283    pub fn ssh_cfg(&self) -> SshCfg {
284        SshCfg {
285            usr: self.usr.ssh.clone(),
286            prj: self.prj.ssh.clone(),
287        }
288    }
289    #[cfg(feature = "azure_blob")]
290    pub fn azure_blob_cfg(&self) -> Option<AzureBlobCfg> {
291        self.prj
292            .azure_blob
293            .as_ref()
294            .map(|prj| AzureBlobCfg { prj: prj.clone() })
295    }
296    pub fn home_folder(&self) -> &str {
297        let ef = self.usr.home_folder.as_deref();
298        match ef {
299            None => file_util::get_default_homedir(),
300            Some(ef) => ef,
301        }
302    }
303
304    pub fn tmpdir(&self) -> &str {
305        match &self.usr.tmpdir {
306            Some(td) => td.as_str(),
307            None => DEFAULT_TMPDIR.to_str().unwrap(),
308        }
309    }
310
311    pub fn http_address(&self) -> &str {
312        match &self.prj.http_address {
313            Some(http_addr) => http_addr,
314            None => "127.0.0.1:5432",
315        }
316    }
317
318    pub fn current_prj_path(&self) -> &Path {
319        if let Some(pp) = &self.usr.current_prj_path {
320            pp
321        } else {
322            &DEFAULT_PRJ_PATH
323        }
324    }
325    pub fn set_current_prj_path(&mut self, pp: PathBuf) {
326        self.usr.current_prj_path = Some(pp);
327    }
328    pub fn unset_current_prj_path(&mut self) {
329        self.usr.current_prj_path = None;
330    }
331
332    pub fn write(&self) -> RvResult<()> {
333        let homefolder = Path::new(self.home_folder());
334        let cfg_usr_path = get_cfg_path_usr(homefolder);
335        if let Some(cfg_parent) = cfg_usr_path.parent() {
336            fs::create_dir_all(cfg_parent).map_err(to_rv)?;
337        }
338        let cfg_usr_str = toml::to_string_pretty(&self.usr).map_err(to_rv)?;
339        let log = true;
340        write_cfg_str(&cfg_usr_str, &cfg_usr_path, log).and_then(|_| {
341            let cfg_prj_path = get_cfg_path_prj(homefolder);
342            let cfg_prj_str = toml::to_string_pretty(&self.prj).map_err(to_rv)?;
343            write_cfg_str(&cfg_prj_str, &cfg_prj_path, log)
344        })
345    }
346    pub fn read(homefolder: &Path) -> RvResult<Self> {
347        let cfg_toml_path_usr = get_cfg_path_usr(homefolder);
348        let cfg_toml_path_prj = get_cfg_path_prj(homefolder);
349        let cfg_toml_path_legacy = get_cfg_path_legacy(homefolder);
350        read_cfg_from_paths(
351            &cfg_toml_path_usr,
352            &cfg_toml_path_prj,
353            &cfg_toml_path_legacy,
354        )
355    }
356}
357impl Default for Cfg {
358    fn default() -> Self {
359        let usr = CfgUsr::default();
360        let prj = CfgPrj::default();
361        let mut cfg = Cfg { usr, prj };
362        cfg.usr.current_prj_path = Some(DEFAULT_PRJ_PATH.to_path_buf());
363        cfg
364    }
365}
366#[cfg(test)]
367use file_util::get_default_homedir;
368
369#[test]
370fn test_default_cfg_paths() {
371    get_default_homedir();
372    DEFAULT_PRJ_PATH.to_str().unwrap();
373    DEFAULT_TMPDIR.to_str().unwrap();
374}
375
376#[test]
377fn test_read_cfg_legacy() {
378    let test_folder = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test_data");
379    let cfg_toml_path_usr = test_folder.join("rv_cfg_usr_doesntexist.toml");
380    let cfg_toml_path_prj = test_folder.join("rv_cfg_prj_doesntexist.toml");
381    let cfg_toml_path_legacy = test_folder.join("rv_cfg_legacy.toml");
382    let cfg = read_cfg_from_paths(
383        &cfg_toml_path_usr,
384        &cfg_toml_path_prj,
385        &cfg_toml_path_legacy,
386    )
387    .unwrap();
388    assert_eq!(
389        cfg.usr.current_prj_path,
390        Some(PathBuf::from("/Users/ultrauser/Desktop/ultra.json"))
391    );
392    assert_eq!(cfg.usr.darkmode, Some(true));
393    assert_eq!(cfg.usr.ssh.user, "someuser");
394    assert_eq!(cfg.prj.ssh.address, "73.42.73.42")
395}