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 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 pub fn ssh_cfg(&self) -> SshCfg {
306 SshCfg {
307 usr: self.usr.ssh.clone(),
308 prj: self.prj.ssh.clone(),
309 }
310 }
311 #[cfg(feature = "azure_blob")]
312 pub fn azure_blob_cfg(&self) -> Option<AzureBlobCfg> {
313 self.prj
314 .azure_blob
315 .as_ref()
316 .map(|prj| AzureBlobCfg { prj: prj.clone() })
317 }
318 pub fn home_folder(&self) -> &str {
319 let ef = self.usr.home_folder.as_deref();
320 match ef {
321 None => file_util::get_default_homedir(),
322 Some(ef) => ef,
323 }
324 }
325
326 pub fn tmpdir(&self) -> &str {
327 match &self.usr.tmpdir {
328 Some(td) => td.as_str(),
329 None => DEFAULT_TMPDIR.to_str().unwrap(),
330 }
331 }
332
333 pub fn http_address(&self) -> &str {
334 match &self.prj.http_address {
335 Some(http_addr) => http_addr,
336 None => "127.0.0.1:5432",
337 }
338 }
339
340 pub fn current_prj_path(&self) -> &Path {
341 if let Some(pp) = &self.usr.current_prj_path {
342 pp
343 } else {
344 &DEFAULT_PRJ_PATH
345 }
346 }
347 pub fn set_current_prj_path(&mut self, pp: PathBuf) {
348 self.usr.current_prj_path = Some(pp);
349 }
350 pub fn unset_current_prj_path(&mut self) {
351 self.usr.current_prj_path = None;
352 }
353
354 pub fn write(&self) -> RvResult<()> {
355 let homefolder = Path::new(self.home_folder());
356 let cfg_usr_path = get_cfg_path_usr(homefolder);
357 if let Some(cfg_parent) = cfg_usr_path.parent() {
358 fs::create_dir_all(cfg_parent).map_err(to_rv)?;
359 }
360 let cfg_usr_str = toml::to_string_pretty(&self.usr).map_err(to_rv)?;
361 let log = true;
362 write_cfg_str(&cfg_usr_str, &cfg_usr_path, log).and_then(|_| {
363 let cfg_prj_path = get_cfg_path_prj(homefolder);
364 let cfg_prj_str = toml::to_string_pretty(&self.prj).map_err(to_rv)?;
365 write_cfg_str(&cfg_prj_str, &cfg_prj_path, log)
366 })
367 }
368 pub fn read(homefolder: &Path) -> RvResult<Self> {
369 let cfg_toml_path_usr = get_cfg_path_usr(homefolder);
370 let cfg_toml_path_prj = get_cfg_path_prj(homefolder);
371 let cfg_toml_path_legacy = get_cfg_path_legacy(homefolder);
372 read_cfg_from_paths(
373 &cfg_toml_path_usr,
374 &cfg_toml_path_prj,
375 &cfg_toml_path_legacy,
376 )
377 }
378}
379impl Default for Cfg {
380 fn default() -> Self {
381 let usr = CfgUsr::default();
382 let prj = CfgPrj::default();
383 let mut cfg = Cfg { usr, prj };
384 cfg.usr.current_prj_path = Some(DEFAULT_PRJ_PATH.to_path_buf());
385 cfg
386 }
387}
388#[cfg(test)]
389use file_util::get_default_homedir;
390
391#[test]
392fn test_default_cfg_paths() {
393 get_default_homedir();
394 DEFAULT_PRJ_PATH.to_str().unwrap();
395 DEFAULT_TMPDIR.to_str().unwrap();
396}
397
398#[test]
399fn test_read_cfg_legacy() {
400 let test_folder = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test_data");
401 let cfg_toml_path_usr = test_folder.join("rv_cfg_usr_doesntexist.toml");
402 let cfg_toml_path_prj = test_folder.join("rv_cfg_prj_doesntexist.toml");
403 let cfg_toml_path_legacy = test_folder.join("rv_cfg_legacy.toml");
404 let cfg = read_cfg_from_paths(
405 &cfg_toml_path_usr,
406 &cfg_toml_path_prj,
407 &cfg_toml_path_legacy,
408 )
409 .unwrap();
410 assert_eq!(
411 cfg.usr.current_prj_path,
412 Some(PathBuf::from("/Users/ultrauser/Desktop/ultra.json"))
413 );
414 assert_eq!(cfg.usr.darkmode, Some(true));
415 assert_eq!(cfg.usr.ssh.user, "someuser");
416 assert_eq!(cfg.prj.ssh.address, "73.42.73.42")
417}