id3_cli/
backup.rs

1use crate::error::{BackupFailure, DirCreationFailure, Error, InvalidFilePath};
2use chrono::{DateTime, Datelike, Local, Timelike};
3use pipe_trait::Pipe;
4use std::{
5    fs::{copy, create_dir_all},
6    path::{Path, PathBuf},
7};
8use typed_builder::TypedBuilder;
9
10/// Settings of a backup.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, TypedBuilder)]
12pub struct Backup<'a> {
13    /// Path to the source file.
14    pub source_file_path: &'a Path,
15    /// Hash of the source file in hexadecimal string.
16    pub source_file_hash: &'a str,
17    /// Current date time.
18    #[builder(default = Local::now())]
19    pub date_time: DateTime<Local>,
20}
21
22impl<'a> Backup<'a> {
23    /// Construct backup file path.
24    pub fn path(self) -> Result<PathBuf, InvalidFilePath> {
25        let Backup {
26            source_file_path,
27            source_file_hash,
28            date_time,
29        } = self;
30        let source_file_parent = source_file_path.parent().ok_or(InvalidFilePath)?;
31        let source_file_name = source_file_path.file_name().ok_or(InvalidFilePath)?;
32        let date = date_time.date();
33        let date = format!("{:04}-{:02}-{:02}", date.year(), date.month(), date.day());
34        let time = date_time.time();
35        let time = format!(
36            "{:02}.{:02}.{:02}",
37            time.hour(),
38            time.minute(),
39            time.second(),
40        );
41        source_file_parent
42            .join(".id3-backups")
43            .join(source_file_name)
44            .join(date)
45            .join(time)
46            .join(source_file_hash)
47            .pipe(Ok)
48    }
49
50    /// Copy the original file to the backup destination.
51    ///
52    /// If the backup destination already exists, skip copying and return `Ok(false)`.
53    ///
54    /// If the backup destination does not exist, perform copying and return `Ok(true)`.
55    pub fn backup(self) -> Result<bool, Error> {
56        let src = self.source_file_path;
57        let dest = self.path()?;
58        if dest.exists() {
59            eprintln!("backup: {dest:?} already exists. Skip.");
60            return Ok(false);
61        }
62        if let Some(parent) = dest.parent() {
63            eprintln!("backup: Creating a directory at {parent:?}");
64            create_dir_all(parent).map_err(move |error| DirCreationFailure {
65                dir: parent.to_path_buf(),
66                error,
67            })?;
68        }
69        eprintln!("backup: Copying {src:?} to {dest:?}");
70        copy(src, &dest).map_err(|error| BackupFailure {
71            src: src.to_path_buf(),
72            dest: dest.to_path_buf(),
73            error,
74        })?;
75        Ok(true)
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use super::Backup;
82    use chrono::{Local, TimeZone};
83    use pretty_assertions::assert_eq;
84    use std::path::Path;
85
86    #[test]
87    fn file_path() {
88        let source_file_parent = Path::new("Music").join("fav");
89        let source_file_name = "mysterious-file.mp3";
90        let source_file_path = source_file_parent.join(source_file_name);
91        let received = Backup::builder()
92            .source_file_path(&source_file_path)
93            .source_file_hash("34a1e24aba0a02316b786933761beedcea40c8eda46a39054f994e0fdef87adf")
94            .date_time(Local.ymd(2022, 7, 16).and_hms(12, 26, 5))
95            .build()
96            .path()
97            .expect("get backup file path");
98        let expected = source_file_parent
99            .join(".id3-backups")
100            .join(source_file_name)
101            .join("2022-07-16")
102            .join("12.26.05")
103            .join("34a1e24aba0a02316b786933761beedcea40c8eda46a39054f994e0fdef87adf");
104        assert_eq!(received, expected);
105    }
106}