rvlib/
cfg.rs

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