Skip to main content

rvlib/
cfg.rs

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