Skip to main content

rvlib/
file_util.rs

1use crate::{
2    cfg::{CfgLegacy, CfgPrj},
3    tools_data::ToolsDataMap,
4};
5use lazy_static::lazy_static;
6use reqwest::IntoUrl;
7use rvimage_domain::{RvResult, rverr, to_rv};
8use serde::{Deserialize, Serialize};
9use std::{
10    ffi::OsStr,
11    fmt::Debug,
12    fs,
13    io::{self, Cursor},
14    path::{Path, PathBuf},
15};
16use tracing::{error, info};
17
18lazy_static! {
19    pub static ref DEFAULT_TMPDIR: PathBuf = std::env::temp_dir().join("rvimage");
20}
21lazy_static! {
22    pub static ref DEFAULT_HOMEDIR: PathBuf = match dirs::home_dir() {
23        Some(p) => p.join(".rvimage"),
24        _ => std::env::temp_dir().join("rvimage"),
25    };
26}
27lazy_static! {
28    pub static ref DEFAULT_PRJ_PATH: PathBuf =
29        DEFAULT_HOMEDIR.join(DEFAULT_PRJ_NAME).join("default.rvi");
30}
31
32pub fn get_default_homedir() -> &'static str {
33    DEFAULT_HOMEDIR
34        .to_str()
35        .expect("could not get default homedir. cannot work without.")
36}
37
38/// Keys of the annotation maps are the relative paths of the corresponding image files to the project folder.
39pub fn tf_to_annomap_key(path: String, curr_prj_path: Option<&Path>) -> String {
40    let path = path.replace('\\', "/");
41    if let Some(curr_prj_path) = curr_prj_path {
42        let path_ref = Path::new(&path);
43        let prj_parent = curr_prj_path
44            .parent()
45            .ok_or_else(|| rverr!("{curr_prj_path:?} has no parent"));
46        let relative_path =
47            prj_parent.and_then(|prj_parent| path_ref.strip_prefix(prj_parent).map_err(to_rv));
48        if let Ok(relative_path) = relative_path {
49            let without_base = path_to_str(relative_path);
50            if let Ok(without_base) = without_base {
51                without_base.to_string()
52            } else {
53                path
54            }
55        } else {
56            path
57        }
58    } else {
59        path
60    }
61}
62#[derive(Clone, Default, Debug, PartialEq, Eq)]
63pub struct PathPair {
64    path_absolute: String,
65    path_relative: String,
66}
67impl PathPair {
68    pub fn new(path_absolute: String, prj_path: &Path) -> Self {
69        let path_absolute = path_absolute.replace('\\', "/");
70        let prj_path = if prj_path == Path::new("") {
71            None
72        } else {
73            Some(prj_path)
74        };
75        let path_relative = tf_to_annomap_key(path_absolute.clone(), prj_path);
76        PathPair {
77            path_absolute,
78            path_relative,
79        }
80    }
81    pub fn from_relative_path(path_relative: String, prj_path: Option<&Path>) -> Self {
82        if let Some(prj_path) = prj_path {
83            let path_absolute = if let Some(parent) = prj_path.parent() {
84                let path_absolute = parent.join(path_relative.clone());
85                if path_absolute.exists() {
86                    path_to_str(&path_absolute).unwrap().replace('\\', "/")
87                } else {
88                    path_relative.replace('\\', "/")
89                }
90            } else {
91                path_relative.replace('\\', "/")
92            };
93            PathPair {
94                path_absolute,
95                path_relative,
96            }
97        } else {
98            PathPair {
99                path_relative: path_relative.clone(),
100                path_absolute: path_relative,
101            }
102        }
103    }
104    pub fn path_absolute(&self) -> &str {
105        &self.path_absolute
106    }
107    pub fn path_relative(&self) -> &str {
108        &self.path_relative
109    }
110    pub fn filename(&self) -> RvResult<&str> {
111        to_name_str(Path::new(&self.path_relative))
112    }
113    pub fn filestem(&self) -> RvResult<&str> {
114        to_stem_str(Path::new(&self.path_relative))
115    }
116}
117
118pub fn read_to_string<P>(p: P) -> RvResult<String>
119where
120    P: AsRef<Path> + Debug,
121{
122    fs::read_to_string(&p).map_err(|e| rverr!("could not read {:?} due to {:?}", p, e))
123}
124pub trait PixelEffect: FnMut(u32, u32) {}
125impl<T: FnMut(u32, u32)> PixelEffect for T {}
126
127pub fn path_to_str(p: &Path) -> RvResult<&str> {
128    osstr_to_str(Some(p.as_os_str()))
129        .map_err(|e| rverr!("path_to_str could not transform '{:?}' due to '{:?}'", p, e))
130}
131
132pub fn osstr_to_str(p: Option<&OsStr>) -> io::Result<&str> {
133    p.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, format!("{p:?} not found")))?
134        .to_str()
135        .ok_or_else(|| {
136            io::Error::new(
137                io::ErrorKind::InvalidData,
138                format!("{p:?} not convertible to unicode"),
139            )
140        })
141}
142
143pub fn to_stem_str(p: &Path) -> RvResult<&str> {
144    let stem = p.file_stem();
145    if stem.is_none() {
146        Ok("")
147    } else {
148        osstr_to_str(stem)
149            .map_err(|e| rverr!("to_stem_str could not transform '{:?}' due to '{:?}'", p, e))
150    }
151}
152
153pub fn to_name_str(p: &Path) -> RvResult<&str> {
154    osstr_to_str(p.file_name())
155        .map_err(|e| rverr!("to_name_str could not transform '{:?}' due to '{:?}'", p, e))
156}
157pub fn parent(p: &Path) -> RvResult<&str> {
158    let parent = p
159        .parent()
160        .ok_or_else(|| rverr!("couldn't get parent of {p:?}"))?;
161    parent.to_str().ok_or_else(|| {
162        rverr!(
163            "parent could not convert parent '{:?}' of '{:?}' to str",
164            parent,
165            p
166        )
167    })
168}
169
170pub const DEFAULT_PRJ_NAME: &str = "default";
171pub fn is_prjname_set(prj_name: &str) -> bool {
172    prj_name != DEFAULT_PRJ_NAME
173}
174
175#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
176pub struct ExportData {
177    pub version: Option<String>,
178    pub tools_data_map: ToolsDataMap,
179}
180
181#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
182#[serde(untagged)]
183#[allow(clippy::large_enum_variant)]
184pub enum SavedCfg {
185    CfgPrj(CfgPrj),
186    CfgLegacy(CfgLegacy),
187}
188
189pub fn save<T>(file_path: &Path, data: T) -> RvResult<()>
190where
191    T: Serialize,
192{
193    let data_str = serde_json::to_string(&data).map_err(to_rv)?;
194    write(file_path, data_str)
195}
196pub fn checked_remove<'a, P: AsRef<Path> + Debug>(
197    path: &'a P,
198    func: fn(p: &'a P) -> io::Result<()>,
199) {
200    match func(path) {
201        Ok(_) => info!("removed {path:?}"),
202        Err(e) => error!("could not remove {path:?} due to {e:?}"),
203    }
204}
205#[macro_export]
206macro_rules! defer_folder_removal {
207    ($path:expr) => {
208        let func = || $crate::file_util::checked_remove($path, std::fs::remove_dir_all);
209        $crate::defer!(func);
210    };
211}
212#[macro_export]
213macro_rules! defer_file_removal {
214    ($path:expr) => {
215        let func = || $crate::file_util::checked_remove($path, std::fs::remove_file);
216        $crate::defer!(func);
217    };
218}
219
220#[allow(clippy::needless_lifetimes)]
221pub fn files_in_folder<'a>(
222    folder: &'a str,
223    prefix: &'a str,
224    extension: &'a str,
225) -> RvResult<impl Iterator<Item = PathBuf> + 'a> {
226    Ok(fs::read_dir(folder)
227        .map_err(|e| rverr!("could not open folder {} due to {}", folder, e))?
228        .flatten()
229        .map(|de| de.path())
230        .filter(|p| {
231            let prefix: &str = prefix; // Not sure why the borrow checker needs this.
232            p.is_file()
233                && if let Some(fname) = p.file_name() {
234                    fname.to_str().unwrap().starts_with(prefix)
235                } else {
236                    false
237                }
238                && (p.extension() == Some(OsStr::new(extension)))
239        }))
240}
241
242pub fn write<P, C>(path: P, contents: C) -> RvResult<()>
243where
244    P: AsRef<Path> + Debug,
245    C: AsRef<[u8]>,
246{
247    fs::write(&path, contents).map_err(|e| rverr!("could not write to {:?} since {:?}", path, e))
248}
249
250#[macro_export]
251macro_rules! p_to_rv {
252    ($path:expr, $expr:expr) => {
253        $expr.map_err(|e| format_rverr!("{:?}, failed on {e:?}", $path))
254    };
255}
256
257pub struct LastPartOfPath<'a> {
258    pub last_folder: &'a str,
259    // will transform /a/b/c/ to /a/b/c
260    pub path_wo_final_sep: &'a str,
261    // offset is defined by " or ' that might by at the beginning and end of the path
262    pub offset: usize,
263    // ', ", or empty string depending on their existence
264    pub mark: &'a str,
265    // separators can be / on Linux or for http and \ on Windows
266    pub n_removed_separators: usize,
267}
268
269impl LastPartOfPath<'_> {
270    pub fn name(&self) -> String {
271        format!(
272            "{}{}{}",
273            self.mark,
274            self.last_folder.replace(':', "_"),
275            self.mark
276        )
277    }
278}
279
280pub fn url_encode(url: &str) -> String {
281    let mappings = [
282        (" ", "%20"),
283        ("+", "%2B"),
284        (",", "%2C"),
285        (";", "%3B"),
286        ("*", "%2A"),
287        ("(", "%28"),
288        (")", "%29"),
289    ];
290    let mut url = url.replace(mappings[0].0, mappings[0].1);
291    for m in mappings[1..].iter() {
292        url = url
293            .replace(m.0, m.1)
294            .replace(m.1.to_lowercase().as_str(), m.1);
295    }
296    url
297}
298
299fn get_last_part_of_path_by_sep(path: &str, sep: char) -> Option<LastPartOfPath<'_>> {
300    if path.contains(sep) {
301        let mark = if path.starts_with('\'') && path.ends_with('\'') {
302            "\'"
303        } else if path.starts_with('"') && path.ends_with('"') {
304            "\""
305        } else {
306            ""
307        };
308        let offset = mark.len();
309        let mut path_wo_final_sep = &path[offset..(path.len() - offset)];
310        let n_fp_slice_initial = path_wo_final_sep.len();
311        let mut last_folder = path_wo_final_sep.split(sep).next_back().unwrap_or("");
312        while last_folder.is_empty() && !path_wo_final_sep.is_empty() {
313            path_wo_final_sep = &path_wo_final_sep[0..path_wo_final_sep.len() - 1];
314            last_folder = path_wo_final_sep.split(sep).next_back().unwrap_or("");
315        }
316        Some(LastPartOfPath {
317            last_folder,
318            path_wo_final_sep,
319            offset,
320            mark,
321            n_removed_separators: n_fp_slice_initial - path_wo_final_sep.len(),
322        })
323    } else {
324        None
325    }
326}
327
328pub fn get_last_part_of_path(path: &str) -> Option<LastPartOfPath<'_>> {
329    let lp_fw = get_last_part_of_path_by_sep(path, '/');
330    if let Some(lp) = &lp_fw {
331        if let Some(lp_fwbw) = get_last_part_of_path_by_sep(lp.last_folder, '\\') {
332            Some(lp_fwbw)
333        } else {
334            lp_fw
335        }
336    } else {
337        get_last_part_of_path_by_sep(path, '\\')
338    }
339}
340
341pub fn get_prj_name<'a>(prj_path: &'a Path, opened_folder: Option<&'a str>) -> &'a str {
342    let default_prjname = if let Some(of) = opened_folder {
343        of
344    } else {
345        DEFAULT_PRJ_NAME
346    };
347    osstr_to_str(prj_path.file_stem()).unwrap_or(default_prjname)
348}
349
350pub fn local_file_info<P>(p: P) -> String
351where
352    P: AsRef<Path>,
353{
354    fs::metadata(p)
355        .map(|md| {
356            let n_bytes = md.len();
357            if n_bytes < 1024 {
358                format!("{}b", md.len())
359            } else if n_bytes < 1024u64.pow(2) {
360                format!("{:.3}kb", md.len() as f64 / 1024f64)
361            } else {
362                format!("{:.3}mb", md.len() as f64 / 1024f64.powi(2))
363            }
364        })
365        .unwrap_or_else(|_| "".to_string())
366}
367
368pub fn copy_and_unzip<P>(src_zip: P, dst_folder: P) -> RvResult<()>
369where
370    P: AsRef<Path>,
371{
372    let file = std::fs::File::open(src_zip).map_err(to_rv)?;
373    let mut archive = zip::ZipArchive::new(file).map_err(to_rv)?;
374    archive.extract(dst_folder).map_err(to_rv)
375}
376pub fn dl_and_unzip<T, P>(src_url: T, dst_folder: P) -> RvResult<()>
377where
378    T: IntoUrl,
379    P: AsRef<Path>,
380{
381    let response = reqwest::blocking::get(src_url).map_err(to_rv)?;
382    let content = response.bytes().map_err(to_rv)?;
383    let des = Cursor::new(content);
384    let mut archive = zip::ZipArchive::new(des).map_err(to_rv)?;
385    archive.extract(dst_folder).map_err(to_rv)
386}
387pub fn get_test_folder() -> PathBuf {
388    PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test_data")
389}
390
391pub fn copy_folder_recursively<P>(src: P, dst: P) -> RvResult<()>
392where
393    P: AsRef<Path>,
394{
395    std::fs::create_dir_all(&dst).map_err(to_rv)?;
396    for entry in std::fs::read_dir(src).map_err(to_rv)? {
397        let entry = entry.map_err(to_rv)?;
398        let file_type = entry.file_type().map_err(to_rv)?;
399        let src_path = entry.path();
400        let dst_path = dst.as_ref().join(entry.file_name());
401        if file_type.is_dir() {
402            copy_folder_recursively(&src_path, &dst_path)?;
403        } else {
404            std::fs::copy(&src_path, &dst_path).map_err(to_rv)?;
405        }
406    }
407    Ok(())
408}
409
410pub fn is_url<P>(s: P) -> bool
411where
412    P: AsRef<str>,
413{
414    s.as_ref().starts_with("http://")
415        || s.as_ref().starts_with("https://")
416        || s.as_ref().starts_with("ftp://")
417}
418
419pub fn relative_to_prj_path<P>(prj_path: &Path, filepath: P) -> RvResult<PathBuf>
420where
421    P: AsRef<Path>,
422{
423    let prj_parent = prj_path.parent();
424    if let Some(prj_parent) = prj_parent
425        && !filepath.as_ref().is_absolute()
426    {
427        let full_path = prj_parent.join(filepath);
428        Ok(full_path)
429    } else {
430        Ok(filepath.as_ref().to_path_buf())
431    }
432}
433
434#[test]
435fn get_last_part() {
436    let path = "http://localhost:8000/a/21%20%20b/Beg.png";
437    let lp = get_last_part_of_path(path).unwrap();
438    assert_eq!(lp.last_folder, "Beg.png");
439}
440
441#[test]
442fn encode() {
443    let url = "http://localhost:8000/a/ +,;*()/Beg.png";
444    let encoded = url_encode(url);
445    let url_enc = "http://localhost:8000/a/%20%2B%2C%3B%2A%28%29/Beg.png";
446    assert_eq!(encoded, url_enc);
447}
448
449#[test]
450fn last_folder_part() {
451    assert_eq!(
452        get_last_part_of_path("a/b/c").map(|lp| lp.name()),
453        Some("c".to_string())
454    );
455    assert_eq!(
456        get_last_part_of_path_by_sep("a/b/c", '\\').map(|lp| lp.name()),
457        None
458    );
459    assert_eq!(
460        get_last_part_of_path_by_sep("a\\b\\c", '/').map(|lp| lp.name()),
461        None
462    );
463    assert_eq!(
464        get_last_part_of_path("a\\b\\c").map(|lp| lp.name()),
465        Some("c".to_string())
466    );
467    assert_eq!(get_last_part_of_path("").map(|lp| lp.name()), None);
468    assert_eq!(
469        get_last_part_of_path("a/b/c/").map(|lp| lp.name()),
470        Some("c".to_string())
471    );
472    assert_eq!(
473        get_last_part_of_path("aadfh//bdafl////aksjc/////").map(|lp| lp.name()),
474        Some("aksjc".to_string())
475    );
476    assert_eq!(
477        get_last_part_of_path("\"aa dfh//bdafl////aks jc/////\"").map(|lp| lp.name()),
478        Some("\"aks jc\"".to_string())
479    );
480    assert_eq!(
481        get_last_part_of_path("'aa dfh//bdafl////aks jc/////'").map(|lp| lp.name()),
482        Some("'aks jc'".to_string())
483    );
484}
485
486#[cfg(target_family = "windows")]
487#[test]
488fn test_stem() {
489    assert_eq!(to_stem_str(Path::new("a/b/c.png")).unwrap(), "c");
490    assert_eq!(to_stem_str(Path::new("c:\\c.png")).unwrap(), "c");
491    assert_eq!(to_stem_str(Path::new("c:\\")).unwrap(), "");
492}
493#[cfg(target_family = "unix")]
494#[test]
495fn test_stem() {
496    assert_eq!(to_stem_str(Path::new("a/b/c.png")).unwrap(), "c");
497    assert_eq!(to_stem_str(Path::new("c:\\c.png")).unwrap(), "c:\\c");
498    assert_eq!(to_stem_str(Path::new("/c.png")).unwrap(), "c");
499    assert_eq!(to_stem_str(Path::new("/")).unwrap(), "");
500}
501
502#[test]
503fn test_pathpair() {
504    fn test(
505        path: &str,
506        prj_path: &str,
507        expected_absolute: &str,
508        expected_relative: &str,
509        skip_from_relative: bool,
510    ) {
511        let pp = PathPair::new(path.to_string(), Path::new(prj_path));
512        assert_eq!(pp.path_absolute(), expected_absolute);
513        assert_eq!(pp.path_relative(), expected_relative);
514        if !skip_from_relative {
515            let pp = PathPair::from_relative_path(
516                expected_relative.to_string(),
517                Some(Path::new(prj_path)),
518            );
519            assert_eq!(pp.path_absolute(), expected_absolute);
520            assert_eq!(pp.path_relative(), expected_relative);
521        }
522    }
523
524    let relative_path = "somesubfolder/notanimage.png";
525    let prj_path_p = get_test_folder().join("rvprj_v3-3_test_dummy.rvi");
526    let prj_path_parent_p = prj_path_p.parent().unwrap();
527    let path_p = prj_path_parent_p.join(relative_path);
528    let prj_path = path_to_str(prj_path_p.as_path()).unwrap();
529    let path = path_to_str(path_p.as_path()).unwrap();
530    test(
531        path,
532        prj_path,
533        &path.replace("\\", "/"),
534        relative_path,
535        false,
536    );
537
538    #[cfg(target_family = "windows")]
539    {
540        let prj_path = "a\\b\\c\\prj.rvi";
541        let path = "a\\b\\c\\d\\e.png";
542        test(path, prj_path, &path.replace("\\", "/"), "d/e.png", true);
543    }
544}