rvlib/
cfg.rs

1use crate::{
2    cache::FileCacheCfgArgs,
3    file_util::{self, DEFAULT_PRJ_PATH, DEFAULT_TMPDIR, path_to_str},
4    result::trace_ok_err,
5    sort_params::SortParams,
6    ssh,
7};
8use rvimage_domain::{RvResult, rverr, to_rv};
9use serde::{Deserialize, Serialize, de::DeserializeOwned};
10use std::{
11    fmt::Debug,
12    fs,
13    path::{Path, PathBuf},
14};
15use tracing::{info, warn};
16
17#[cfg(feature = "azure_blob")]
18#[derive(Deserialize, Serialize, Debug, Default, Clone, PartialEq, Eq)]
19pub struct AzureBlobCfgLegacy {
20    pub connection_string_path: String,
21    pub container_name: String,
22    pub prefix: String,
23}
24#[derive(Deserialize, Serialize, Debug, Default, Clone, PartialEq, Eq)]
25pub struct SshCfgLegacy {
26    pub user: String,
27    pub ssh_identity_file_path: String,
28    n_reconnection_attempts: Option<usize>,
29    pub remote_folder_paths: Vec<String>,
30    pub address: String,
31}
32#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq, Default)]
33pub struct CfgLegacy {
34    pub connection: Connection,
35    pub cache: Cache,
36    http_address: Option<String>,
37    tmpdir: Option<String>,
38    current_prj_path: Option<PathBuf>,
39    pub file_cache_args: Option<FileCacheCfgArgs>,
40    pub ssh_cfg: SshCfgLegacy,
41    pub home_folder: Option<String>,
42    pub py_http_reader_cfg: Option<PyHttpReaderCfg>,
43    pub darkmode: Option<bool>,
44    pub n_autosaves: Option<u8>,
45    pub import_old_path: Option<String>,
46    pub import_new_path: Option<String>,
47    #[cfg(feature = "azure_blob")]
48    pub azure_blob_cfg: Option<AzureBlobCfgLegacy>,
49}
50impl CfgLegacy {
51    pub fn to_cfg(self) -> Cfg {
52        let usr = CfgUsr {
53            darkmode: self.darkmode,
54            n_autosaves: self.n_autosaves,
55            home_folder: self.home_folder,
56            cache: self.cache,
57            tmpdir: self.tmpdir,
58            current_prj_path: self.current_prj_path,
59            file_cache_args: self.file_cache_args.unwrap_or_default(),
60            image_change_delay_on_held_key_ms: get_image_change_delay_on_held_key_ms(),
61
62            ssh: SshCfgUsr {
63                user: self.ssh_cfg.user,
64                ssh_identity_file_path: self.ssh_cfg.ssh_identity_file_path,
65                n_reconnection_attempts: self.ssh_cfg.n_reconnection_attempts,
66            },
67            n_prev_thumbs: get_default_n_thumbs(),
68            n_next_thumbs: get_default_n_thumbs(),
69            hide_thumbs: true,
70        };
71        let prj = CfgPrj {
72            connection: self.connection,
73            http_address: self.http_address,
74            py_http_reader_cfg: self.py_http_reader_cfg,
75            ssh: SshCfgPrj {
76                remote_folder_paths: self.ssh_cfg.remote_folder_paths,
77                address: self.ssh_cfg.address,
78            },
79            azure_blob: self.azure_blob_cfg.map(|ab| AzureBlobCfgPrj {
80                connection_string_path: ab.connection_string_path,
81                container_name: ab.container_name,
82                prefix: ab.prefix,
83                blob_list_timeout_s: get_blob_list_timeout_s(),
84            }),
85            sort_params: SortParams::default(),
86        };
87        Cfg { usr, prj }
88    }
89}
90
91pub fn get_cfg_path_legacy(homefolder: &Path) -> PathBuf {
92    homefolder.join("rv_cfg.toml")
93}
94
95pub fn get_cfg_path_usr(homefolder: &Path) -> PathBuf {
96    homefolder.join("rv_cfg_usr.toml")
97}
98
99pub fn get_cfg_path_prj(homefolder: &Path) -> PathBuf {
100    homefolder.join("rv_cfg_prjtmp.toml")
101}
102
103pub fn get_cfg_tmppath(cfg: &Cfg) -> PathBuf {
104    Path::new(cfg.tmpdir())
105        .join(".rvimage")
106        .join("rv_cfg_tmp.toml")
107}
108
109pub fn get_log_folder(homefolder: &Path) -> PathBuf {
110    homefolder.join("logs")
111}
112
113fn parse_toml_str<CFG: Debug + DeserializeOwned + Default>(toml_str: &str) -> RvResult<CFG> {
114    match toml::from_str(toml_str) {
115        Ok(cfg) => Ok(cfg),
116        Err(_) => {
117            // lets try replacing \ by / and see if we can parse it
118            let toml_str = toml_str.replace('\\', "/");
119            match toml::from_str(&toml_str) {
120                Ok(cfg) => Ok(cfg),
121                Err(_) => {
122                    // lets try replacing " by ' and see if we can parse it
123                    let toml_str = toml_str.replace('"', "'");
124                    toml::from_str(&toml_str)
125                        .map_err(|e| rverr!("failed to parse cfg due to {e:?}"))
126                }
127            }
128        }
129    }
130}
131
132pub fn read_cfg_gen<CFG: Debug + DeserializeOwned + Default>(
133    cfg_toml_path: &Path,
134) -> RvResult<CFG> {
135    if cfg_toml_path.exists() {
136        let toml_str = file_util::read_to_string(cfg_toml_path)?;
137        parse_toml_str(&toml_str)
138    } else {
139        warn!("cfg {cfg_toml_path:?} file does not exist. using default cfg");
140        Ok(CFG::default())
141    }
142}
143
144fn read_cfg_from_paths(
145    cfg_toml_path_usr: &Path,
146    cfg_toml_path_prj: &Path,
147    cfg_toml_path_legacy: &Path,
148) -> RvResult<Cfg> {
149    if cfg_toml_path_usr.exists() || cfg_toml_path_prj.exists() {
150        let usr = read_cfg_gen::<CfgUsr>(cfg_toml_path_usr)?;
151        let prj = read_cfg_gen::<CfgPrj>(cfg_toml_path_prj)?;
152        Ok(Cfg { usr, prj })
153    } else if cfg_toml_path_legacy.exists() {
154        tracing::warn!("using legacy cfg file {cfg_toml_path_legacy:?}");
155        let legacy = read_cfg_gen::<CfgLegacy>(cfg_toml_path_legacy)?;
156        Ok(legacy.to_cfg())
157    } else {
158        tracing::info!("no cfg file found. using default cfg");
159        Ok(Cfg::default())
160    }
161}
162
163pub fn write_cfg_str(cfg_str: &str, p: &Path, log: bool) -> RvResult<()> {
164    file_util::write(p, cfg_str)?;
165    if log {
166        info!("wrote cfg to {p:?}");
167    }
168    Ok(())
169}
170
171#[derive(Deserialize, Serialize, Debug, Eq, PartialEq, Clone, Copy, Default)]
172pub enum Connection {
173    Ssh,
174    PyHttp,
175    #[cfg(feature = "azure_blob")]
176    AzureBlob,
177    #[default]
178    Local,
179}
180#[derive(Deserialize, Serialize, Debug, Eq, PartialEq, Clone, Default)]
181pub enum Cache {
182    #[default]
183    FileCache,
184    NoCache,
185}
186#[derive(Deserialize, Serialize, Debug, Default, Clone, PartialEq, Eq)]
187pub struct SshCfgUsr {
188    pub user: String,
189    pub ssh_identity_file_path: String,
190    n_reconnection_attempts: Option<usize>,
191}
192#[derive(Deserialize, Serialize, Debug, Default, Clone, PartialEq, Eq)]
193pub struct SshCfgPrj {
194    pub remote_folder_paths: Vec<String>,
195    pub address: String,
196}
197#[derive(Deserialize, Serialize, Debug, Default, Clone, PartialEq, Eq)]
198pub struct SshCfg {
199    pub usr: SshCfgUsr,
200    pub prj: SshCfgPrj,
201}
202impl SshCfg {
203    pub fn n_reconnection_attempts(&self) -> usize {
204        let default = 5;
205        self.usr.n_reconnection_attempts.unwrap_or(default)
206    }
207}
208
209fn get_blob_list_timeout_s() -> u64 {
210    10
211}
212
213#[cfg(feature = "azure_blob")]
214#[derive(Deserialize, Serialize, Debug, Default, Clone, PartialEq, Eq)]
215pub struct AzureBlobCfgPrj {
216    #[serde(default)]
217    pub connection_string_path: String,
218    pub container_name: String,
219    pub prefix: String,
220    #[serde(default = "get_blob_list_timeout_s")]
221    pub blob_list_timeout_s: u64,
222}
223
224#[cfg(feature = "azure_blob")]
225#[derive(Deserialize, Serialize, Debug, Default, Clone, PartialEq, Eq)]
226pub struct AzureBlobCfg {
227    pub prj: AzureBlobCfgPrj,
228}
229
230#[derive(Deserialize, Serialize, Debug, Default, Clone, PartialEq, Eq)]
231pub struct PyHttpReaderCfg {
232    pub server_addresses: Vec<String>,
233}
234
235#[derive(Deserialize, Serialize, Default, Clone, Debug, PartialEq, Eq)]
236pub enum ExportPathConnection {
237    Ssh,
238    #[default]
239    Local,
240}
241impl ExportPathConnection {
242    pub fn write_bytes(
243        &self,
244        data: &[u8],
245        dst_path: &Path,
246        ssh_cfg: Option<&SshCfg>,
247    ) -> RvResult<()> {
248        match (self, ssh_cfg) {
249            (ExportPathConnection::Ssh, Some(ssh_cfg)) => {
250                let sess = ssh::auth(ssh_cfg)?;
251                ssh::write_bytes(data, dst_path, &sess).map_err(to_rv)?;
252                Ok(())
253            }
254            (ExportPathConnection::Local, _) => {
255                file_util::write(dst_path, data)?;
256                Ok(())
257            }
258            (ExportPathConnection::Ssh, None) => Err(rverr!("cannot save to ssh. config missing")),
259        }
260    }
261    pub fn write(&self, data_str: &str, dst_path: &Path, ssh_cfg: Option<&SshCfg>) -> RvResult<()> {
262        self.write_bytes(data_str.as_bytes(), dst_path, ssh_cfg)
263    }
264    pub fn read(&self, src_path: &Path, ssh_cfg: Option<&SshCfg>) -> RvResult<String> {
265        match (self, ssh_cfg) {
266            (ExportPathConnection::Ssh, Some(ssh_cfg)) => {
267                let sess = ssh::auth(ssh_cfg)?;
268                let read_bytes = ssh::download(path_to_str(src_path)?, &sess)?;
269                String::from_utf8(read_bytes).map_err(to_rv)
270            }
271            (ExportPathConnection::Local, _) => file_util::read_to_string(src_path),
272            (ExportPathConnection::Ssh, None) => {
273                Err(rverr!("cannot read from ssh. config missing"))
274            }
275        }
276    }
277}
278#[derive(Deserialize, Serialize, Default, Clone, Debug, PartialEq, Eq)]
279pub struct ExportPath {
280    pub path: PathBuf,
281    pub conn: ExportPathConnection,
282}
283
284pub enum Style {
285    Dark,
286    Light,
287}
288
289fn get_default_n_thumbs() -> usize {
290    4
291}
292
293fn get_default_n_autosaves() -> Option<u8> {
294    Some(2)
295}
296
297fn get_image_change_delay_on_held_key_ms() -> u64 {
298    300
299}
300
301#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq, Default)]
302pub struct CfgUsr {
303    pub darkmode: Option<bool>,
304    #[serde(default = "get_default_n_autosaves")]
305    pub n_autosaves: Option<u8>,
306
307    #[serde(default = "get_image_change_delay_on_held_key_ms")]
308    pub image_change_delay_on_held_key_ms: u64,
309
310    // This is only variable to make the CLI and tests not override your config.
311    // You shall not change this when actually running RV Image.
312    pub home_folder: Option<String>,
313
314    pub cache: Cache,
315    tmpdir: Option<String>,
316    current_prj_path: Option<PathBuf>,
317    #[serde(default)]
318    pub file_cache_args: FileCacheCfgArgs,
319    pub ssh: SshCfgUsr,
320    #[serde(default = "get_default_n_thumbs")]
321    pub n_prev_thumbs: usize,
322    #[serde(default = "get_default_n_thumbs")]
323    pub n_next_thumbs: usize,
324    #[serde(default)]
325    pub hide_thumbs: bool,
326}
327
328impl CfgUsr {
329    pub fn get_n_autosaves(&self) -> u8 {
330        self.n_autosaves
331            .unwrap_or(get_default_n_autosaves().unwrap())
332    }
333}
334
335#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq, Default)]
336pub struct CfgPrj {
337    pub py_http_reader_cfg: Option<PyHttpReaderCfg>,
338    pub connection: Connection,
339    http_address: Option<String>,
340    pub ssh: SshCfgPrj,
341    #[cfg(feature = "azure_blob")]
342    pub azure_blob: Option<AzureBlobCfgPrj>,
343    #[serde(default)]
344    pub sort_params: SortParams,
345}
346#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
347pub struct Cfg {
348    pub usr: CfgUsr,
349    pub prj: CfgPrj,
350}
351
352impl Cfg {
353    /// for multiple cli instances to run in parallel
354    pub fn with_unique_folders() -> Self {
355        let mut cfg = Self::default();
356        let uuid_str = format!("{}", uuid::Uuid::new_v4());
357        let tmpdir_str = DEFAULT_TMPDIR
358            .to_str()
359            .expect("default tmpdir does not exist. cannot work without")
360            .to_string();
361        cfg.usr.tmpdir = Some(format!("{tmpdir_str}/rvimage_tmp_{uuid_str}"));
362        let tmp_homedir = format!("{tmpdir_str}/rvimage_home_{uuid_str}");
363
364        // copy user cfg to tmp homedir
365        trace_ok_err(fs::create_dir_all(&tmp_homedir));
366        if let Some(home_folder) = &cfg.usr.home_folder {
367            let usrcfg_path = get_cfg_path_usr(Path::new(home_folder));
368            if usrcfg_path.exists()
369                && let Some(filename) = usrcfg_path.file_name()
370            {
371                trace_ok_err(fs::copy(
372                    &usrcfg_path,
373                    Path::new(&tmp_homedir).join(filename),
374                ));
375            }
376        }
377        cfg.usr.home_folder = Some(tmp_homedir);
378        cfg
379    }
380    pub fn ssh_cfg(&self) -> SshCfg {
381        SshCfg {
382            usr: self.usr.ssh.clone(),
383            prj: self.prj.ssh.clone(),
384        }
385    }
386    #[cfg(feature = "azure_blob")]
387    pub fn azure_blob_cfg(&self) -> Option<AzureBlobCfg> {
388        self.prj
389            .azure_blob
390            .as_ref()
391            .map(|prj| AzureBlobCfg { prj: prj.clone() })
392    }
393    pub fn home_folder(&self) -> &str {
394        let ef = self.usr.home_folder.as_deref();
395        match ef {
396            None => file_util::get_default_homedir(),
397            Some(ef) => ef,
398        }
399    }
400
401    pub fn tmpdir(&self) -> &str {
402        match &self.usr.tmpdir {
403            Some(td) => td.as_str(),
404            None => DEFAULT_TMPDIR.to_str().unwrap(),
405        }
406    }
407
408    pub fn http_address(&self) -> &str {
409        match &self.prj.http_address {
410            Some(http_addr) => http_addr,
411            None => "127.0.0.1:5432",
412        }
413    }
414
415    pub fn current_prj_path(&self) -> &Path {
416        if let Some(pp) = &self.usr.current_prj_path {
417            pp
418        } else {
419            &DEFAULT_PRJ_PATH
420        }
421    }
422    pub fn set_current_prj_path(&mut self, pp: PathBuf) {
423        self.usr.current_prj_path = Some(pp);
424    }
425    pub fn unset_current_prj_path(&mut self) {
426        self.usr.current_prj_path = None;
427    }
428
429    pub fn write(&self) -> RvResult<()> {
430        let homefolder = Path::new(self.home_folder());
431        let cfg_usr_path = get_cfg_path_usr(homefolder);
432        if let Some(cfg_parent) = cfg_usr_path.parent() {
433            fs::create_dir_all(cfg_parent).map_err(to_rv)?;
434        }
435        let cfg_usr_str = toml::to_string_pretty(&self.usr).map_err(to_rv)?;
436        let log = true;
437        write_cfg_str(&cfg_usr_str, &cfg_usr_path, log).and_then(|_| {
438            let cfg_prj_path = get_cfg_path_prj(homefolder);
439            let cfg_prj_str = toml::to_string_pretty(&self.prj).map_err(to_rv)?;
440            write_cfg_str(&cfg_prj_str, &cfg_prj_path, log)
441        })
442    }
443    pub fn read(homefolder: &Path) -> RvResult<Self> {
444        let cfg_toml_path_usr = get_cfg_path_usr(homefolder);
445        let cfg_toml_path_prj = get_cfg_path_prj(homefolder);
446        let cfg_toml_path_legacy = get_cfg_path_legacy(homefolder);
447        read_cfg_from_paths(
448            &cfg_toml_path_usr,
449            &cfg_toml_path_prj,
450            &cfg_toml_path_legacy,
451        )
452    }
453}
454impl Default for Cfg {
455    fn default() -> Self {
456        let usr = CfgUsr::default();
457        let prj = CfgPrj::default();
458
459        let mut cfg = Cfg { usr, prj };
460        cfg.usr.current_prj_path = Some(DEFAULT_PRJ_PATH.to_path_buf());
461        cfg.usr.n_prev_thumbs = get_default_n_thumbs();
462        cfg.usr.n_next_thumbs = get_default_n_thumbs();
463        cfg.usr.hide_thumbs = true;
464        cfg
465    }
466}
467#[cfg(test)]
468use file_util::get_default_homedir;
469
470#[test]
471fn test_default_cfg_paths() {
472    get_default_homedir();
473    DEFAULT_PRJ_PATH.to_str().unwrap();
474    DEFAULT_TMPDIR.to_str().unwrap();
475}
476
477#[test]
478fn test_read_cfg_legacy() {
479    let test_folder = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test_data");
480    let cfg_toml_path_usr = test_folder.join("rv_cfg_usr_doesntexist.toml");
481    let cfg_toml_path_prj = test_folder.join("rv_cfg_prj_doesntexist.toml");
482    let cfg_toml_path_legacy = test_folder.join("rv_cfg_legacy.toml");
483    let cfg = read_cfg_from_paths(
484        &cfg_toml_path_usr,
485        &cfg_toml_path_prj,
486        &cfg_toml_path_legacy,
487    )
488    .unwrap();
489    assert_eq!(
490        cfg.usr.current_prj_path,
491        Some(PathBuf::from("/Users/ultrauser/Desktop/ultra.json"))
492    );
493    assert_eq!(cfg.usr.darkmode, Some(true));
494    assert_eq!(cfg.usr.ssh.user, "someuser");
495    assert_eq!(cfg.prj.ssh.address, "73.42.73.42")
496}
497
498#[cfg(test)]
499fn make_cfg_str(ssh_identity_filepath: &str) -> String {
500    let part1 = r#"
501[usr]
502n_autosaves = 10
503image_change_delay_on_held_key_ms = 10
504cache = "FileCache"
505current_prj_path = "someprjpath.json"
506
507[usr.file_cache_args]
508n_prev_images = 4
509n_next_images = 8
510n_threads = 2
511clear_on_close = true
512cachedir = "C:/Users/ShafeiB/.rvimage/cache"
513
514[usr.ssh]
515user = "auser"
516ssh_identity_file_path ="#;
517
518    let part2 = r#"
519[prj]
520connection = "Local"
521
522[prj.py_http_reader_cfg]
523server_addresses = [
524    "http://localhost:8000/somewhere",
525    "http://localhost:8000/elsewhere",
526]
527
528[prj.ssh]
529remote_folder_paths = ["/"]
530address = "12.11.10.13:22"
531
532[prj.sort_params]
533kind = "Natural"
534sort_by_filename = false
535"#;
536
537    format!("{part1} {ssh_identity_filepath} {part2}")
538}
539
540#[test]
541fn test_parse_toml() {
542    fn test(ssh_path: &str, ssh_path_expected: &str) {
543        let toml_str = make_cfg_str(ssh_path);
544        let cfg: Cfg = parse_toml_str(&toml_str).unwrap();
545        assert_eq!(cfg.usr.ssh.ssh_identity_file_path, ssh_path_expected);
546    }
547    test("\"c:\\somehome\\.ssh\\id_rsa\"", "c:/somehome/.ssh/id_rsa");
548    test(
549        "'c:\\some home\\.ssh\\id_rsa'",
550        "c:\\some home\\.ssh\\id_rsa",
551    );
552    test("\"/s omehome\\.ssh\\id_rsa\"", "/s omehome/.ssh/id_rsa");
553    test("'/some home/.ssh/id_rsa'", "/some home/.ssh/id_rsa");
554}