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