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}
157
158pub const DEFAULT_PRJ_NAME: &str = "default";
159pub fn is_prjname_set(prj_name: &str) -> bool {
160    prj_name != DEFAULT_PRJ_NAME
161}
162
163#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
164pub struct ExportData {
165    pub version: Option<String>,
166    pub tools_data_map: ToolsDataMap,
167}
168
169#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
170#[serde(untagged)]
171#[allow(clippy::large_enum_variant)]
172pub enum SavedCfg {
173    CfgPrj(CfgPrj),
174    CfgLegacy(CfgLegacy),
175}
176
177pub fn save<T>(file_path: &Path, data: T) -> RvResult<()>
178where
179    T: Serialize,
180{
181    let data_str = serde_json::to_string(&data).map_err(to_rv)?;
182    write(file_path, data_str)
183}
184pub fn checked_remove<'a, P: AsRef<Path> + Debug>(
185    path: &'a P,
186    func: fn(p: &'a P) -> io::Result<()>,
187) {
188    match func(path) {
189        Ok(_) => info!("removed {path:?}"),
190        Err(e) => error!("could not remove {path:?} due to {e:?}"),
191    }
192}
193#[macro_export]
194macro_rules! defer_folder_removal {
195    ($path:expr) => {
196        let func = || $crate::file_util::checked_remove($path, std::fs::remove_dir_all);
197        $crate::defer!(func);
198    };
199}
200#[macro_export]
201macro_rules! defer_file_removal {
202    ($path:expr) => {
203        let func = || $crate::file_util::checked_remove($path, std::fs::remove_file);
204        $crate::defer!(func);
205    };
206}
207
208#[allow(clippy::needless_lifetimes)]
209pub fn files_in_folder<'a>(
210    folder: &'a str,
211    prefix: &'a str,
212    extension: &'a str,
213) -> RvResult<impl Iterator<Item = PathBuf> + 'a> {
214    Ok(fs::read_dir(folder)
215        .map_err(|e| rverr!("could not open folder {} due to {}", folder, e))?
216        .flatten()
217        .map(|de| de.path())
218        .filter(|p| {
219            let prefix: &str = prefix; // Not sure why the borrow checker needs this.
220            p.is_file()
221                && if let Some(fname) = p.file_name() {
222                    fname.to_str().unwrap().starts_with(prefix)
223                } else {
224                    false
225                }
226                && (p.extension() == Some(OsStr::new(extension)))
227        }))
228}
229
230pub fn write<P, C>(path: P, contents: C) -> RvResult<()>
231where
232    P: AsRef<Path> + Debug,
233    C: AsRef<[u8]>,
234{
235    fs::write(&path, contents).map_err(|e| rverr!("could not write to {:?} since {:?}", path, e))
236}
237
238#[macro_export]
239macro_rules! p_to_rv {
240    ($path:expr, $expr:expr) => {
241        $expr.map_err(|e| format_rverr!("{:?}, failed on {e:?}", $path))
242    };
243}
244
245pub struct LastPartOfPath<'a> {
246    pub last_folder: &'a str,
247    // will transform /a/b/c/ to /a/b/c
248    pub path_wo_final_sep: &'a str,
249    // offset is defined by " or ' that might by at the beginning and end of the path
250    pub offset: usize,
251    // ', ", or empty string depending on their existence
252    pub mark: &'a str,
253    // separators can be / on Linux or for http and \ on Windows
254    pub n_removed_separators: usize,
255}
256
257impl LastPartOfPath<'_> {
258    pub fn name(&self) -> String {
259        format!(
260            "{}{}{}",
261            self.mark,
262            self.last_folder.replace(':', "_"),
263            self.mark
264        )
265    }
266}
267
268pub fn url_encode(url: &str) -> String {
269    let mappings = [
270        (" ", "%20"),
271        ("+", "%2B"),
272        (",", "%2C"),
273        (";", "%3B"),
274        ("*", "%2A"),
275        ("(", "%28"),
276        (")", "%29"),
277    ];
278    let mut url = url.replace(mappings[0].0, mappings[0].1);
279    for m in mappings[1..].iter() {
280        url = url
281            .replace(m.0, m.1)
282            .replace(m.1.to_lowercase().as_str(), m.1);
283    }
284    url
285}
286
287fn get_last_part_of_path_by_sep(path: &str, sep: char) -> Option<LastPartOfPath<'_>> {
288    if path.contains(sep) {
289        let mark = if path.starts_with('\'') && path.ends_with('\'') {
290            "\'"
291        } else if path.starts_with('"') && path.ends_with('"') {
292            "\""
293        } else {
294            ""
295        };
296        let offset = mark.len();
297        let mut path_wo_final_sep = &path[offset..(path.len() - offset)];
298        let n_fp_slice_initial = path_wo_final_sep.len();
299        let mut last_folder = path_wo_final_sep.split(sep).next_back().unwrap_or("");
300        while last_folder.is_empty() && !path_wo_final_sep.is_empty() {
301            path_wo_final_sep = &path_wo_final_sep[0..path_wo_final_sep.len() - 1];
302            last_folder = path_wo_final_sep.split(sep).next_back().unwrap_or("");
303        }
304        Some(LastPartOfPath {
305            last_folder,
306            path_wo_final_sep,
307            offset,
308            mark,
309            n_removed_separators: n_fp_slice_initial - path_wo_final_sep.len(),
310        })
311    } else {
312        None
313    }
314}
315
316pub fn get_last_part_of_path(path: &str) -> Option<LastPartOfPath<'_>> {
317    let lp_fw = get_last_part_of_path_by_sep(path, '/');
318    if let Some(lp) = &lp_fw {
319        if let Some(lp_fwbw) = get_last_part_of_path_by_sep(lp.last_folder, '\\') {
320            Some(lp_fwbw)
321        } else {
322            lp_fw
323        }
324    } else {
325        get_last_part_of_path_by_sep(path, '\\')
326    }
327}
328
329pub fn get_prj_name<'a>(prj_path: &'a Path, opened_folder: Option<&'a str>) -> &'a str {
330    let default_prjname = if let Some(of) = opened_folder {
331        of
332    } else {
333        DEFAULT_PRJ_NAME
334    };
335    osstr_to_str(prj_path.file_stem()).unwrap_or(default_prjname)
336}
337
338pub fn local_file_info<P>(p: P) -> String
339where
340    P: AsRef<Path>,
341{
342    fs::metadata(p)
343        .map(|md| {
344            let n_bytes = md.len();
345            if n_bytes < 1024 {
346                format!("{}b", md.len())
347            } else if n_bytes < 1024u64.pow(2) {
348                format!("{:.3}kb", md.len() as f64 / 1024f64)
349            } else {
350                format!("{:.3}mb", md.len() as f64 / 1024f64.powi(2))
351            }
352        })
353        .unwrap_or_else(|_| "".to_string())
354}
355
356pub fn copy_and_unzip<P>(src_zip: P, dst_folder: P) -> RvResult<()>
357where
358    P: AsRef<Path>,
359{
360    let file = std::fs::File::open(src_zip).map_err(to_rv)?;
361    let mut archive = zip::ZipArchive::new(file).map_err(to_rv)?;
362    archive.extract(dst_folder).map_err(to_rv)
363}
364pub fn dl_and_unzip<T, P>(src_url: T, dst_folder: P) -> RvResult<()>
365where
366    T: IntoUrl,
367    P: AsRef<Path>,
368{
369    let response = reqwest::blocking::get(src_url).map_err(to_rv)?;
370    let content = response.bytes().map_err(to_rv)?;
371    let des = Cursor::new(content);
372    let mut archive = zip::ZipArchive::new(des).map_err(to_rv)?;
373    archive.extract(dst_folder).map_err(to_rv)
374}
375pub fn get_test_folder() -> PathBuf {
376    PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test_data")
377}
378
379pub fn copy_folder_recursively<P>(src: P, dst: P) -> RvResult<()>
380where
381    P: AsRef<Path>,
382{
383    std::fs::create_dir_all(&dst).map_err(to_rv)?;
384    for entry in std::fs::read_dir(src).map_err(to_rv)? {
385        let entry = entry.map_err(to_rv)?;
386        let file_type = entry.file_type().map_err(to_rv)?;
387        let src_path = entry.path();
388        let dst_path = dst.as_ref().join(entry.file_name());
389        if file_type.is_dir() {
390            copy_folder_recursively(&src_path, &dst_path)?;
391        } else {
392            std::fs::copy(&src_path, &dst_path).map_err(to_rv)?;
393        }
394    }
395    Ok(())
396}
397
398pub fn is_url<P>(s: P) -> bool
399where
400    P: AsRef<str>,
401{
402    s.as_ref().starts_with("http://")
403        || s.as_ref().starts_with("https://")
404        || s.as_ref().starts_with("ftp://")
405}
406
407pub fn relative_to_prj_path<P>(prj_path: &Path, filepath: P) -> RvResult<PathBuf>
408where
409    P: AsRef<Path>,
410{
411    let prj_parent = prj_path.parent();
412    if let Some(prj_parent) = prj_parent
413        && !filepath.as_ref().is_absolute()
414    {
415        let full_path = prj_parent.join(filepath);
416        Ok(full_path)
417    } else {
418        Ok(filepath.as_ref().to_path_buf())
419    }
420}
421
422#[test]
423fn get_last_part() {
424    let path = "http://localhost:8000/a/21%20%20b/Beg.png";
425    let lp = get_last_part_of_path(path).unwrap();
426    assert_eq!(lp.last_folder, "Beg.png");
427}
428
429#[test]
430fn encode() {
431    let url = "http://localhost:8000/a/ +,;*()/Beg.png";
432    let encoded = url_encode(url);
433    let url_enc = "http://localhost:8000/a/%20%2B%2C%3B%2A%28%29/Beg.png";
434    assert_eq!(encoded, url_enc);
435}
436
437#[test]
438fn last_folder_part() {
439    assert_eq!(
440        get_last_part_of_path("a/b/c").map(|lp| lp.name()),
441        Some("c".to_string())
442    );
443    assert_eq!(
444        get_last_part_of_path_by_sep("a/b/c", '\\').map(|lp| lp.name()),
445        None
446    );
447    assert_eq!(
448        get_last_part_of_path_by_sep("a\\b\\c", '/').map(|lp| lp.name()),
449        None
450    );
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!(get_last_part_of_path("").map(|lp| lp.name()), None);
456    assert_eq!(
457        get_last_part_of_path("a/b/c/").map(|lp| lp.name()),
458        Some("c".to_string())
459    );
460    assert_eq!(
461        get_last_part_of_path("aadfh//bdafl////aksjc/////").map(|lp| lp.name()),
462        Some("aksjc".to_string())
463    );
464    assert_eq!(
465        get_last_part_of_path("\"aa dfh//bdafl////aks jc/////\"").map(|lp| lp.name()),
466        Some("\"aks jc\"".to_string())
467    );
468    assert_eq!(
469        get_last_part_of_path("'aa dfh//bdafl////aks jc/////'").map(|lp| lp.name()),
470        Some("'aks jc'".to_string())
471    );
472}
473
474#[cfg(target_family = "windows")]
475#[test]
476fn test_stem() {
477    assert_eq!(to_stem_str(Path::new("a/b/c.png")).unwrap(), "c");
478    assert_eq!(to_stem_str(Path::new("c:\\c.png")).unwrap(), "c");
479    assert_eq!(to_stem_str(Path::new("c:\\")).unwrap(), "");
480}
481#[cfg(target_family = "unix")]
482#[test]
483fn test_stem() {
484    assert_eq!(to_stem_str(Path::new("a/b/c.png")).unwrap(), "c");
485    assert_eq!(to_stem_str(Path::new("c:\\c.png")).unwrap(), "c:\\c");
486    assert_eq!(to_stem_str(Path::new("/c.png")).unwrap(), "c");
487    assert_eq!(to_stem_str(Path::new("/")).unwrap(), "");
488}
489
490#[test]
491fn test_pathpair() {
492    fn test(
493        path: &str,
494        prj_path: &str,
495        expected_absolute: &str,
496        expected_relative: &str,
497        skip_from_relative: bool,
498    ) {
499        let pp = PathPair::new(path.to_string(), Path::new(prj_path));
500        assert_eq!(pp.path_absolute(), expected_absolute);
501        assert_eq!(pp.path_relative(), expected_relative);
502        if !skip_from_relative {
503            let pp = PathPair::from_relative_path(
504                expected_relative.to_string(),
505                Some(Path::new(prj_path)),
506            );
507            assert_eq!(pp.path_absolute(), expected_absolute);
508            assert_eq!(pp.path_relative(), expected_relative);
509        }
510    }
511
512    let relative_path = "somesubfolder/notanimage.png";
513    let prj_path_p = get_test_folder().join("rvprj_v3-3_test_dummy.rvi");
514    let prj_path_parent_p = prj_path_p.parent().unwrap();
515    let path_p = prj_path_parent_p.join(relative_path);
516    let prj_path = path_to_str(prj_path_p.as_path()).unwrap();
517    let path = path_to_str(path_p.as_path()).unwrap();
518    test(
519        path,
520        prj_path,
521        &path.replace("\\", "/"),
522        relative_path,
523        false,
524    );
525
526    #[cfg(target_family = "windows")]
527    {
528        let prj_path = "a\\b\\c\\prj.rvi";
529        let path = "a\\b\\c\\d\\e.png";
530        test(path, prj_path, &path.replace("\\", "/"), "d/e.png", true);
531    }
532}