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