1use camino::{Utf8Path, Utf8PathBuf};
12
13use crate::paths;
14use crate::{Error, Result};
15
16pub fn current_timestamp(format: &str) -> Result<String> {
18 let now = jiff::Zoned::now();
19 let bdt = jiff::fmt::strtime::BrokenDownTime::from(&now);
20 bdt.to_string(format)
21 .map_err(|e| Error::Other(anyhow::anyhow!("timestamp format '{format}': {e}")))
22}
23
24pub fn backup_path(backup_root: &Utf8Path, abs_target: &Utf8Path, timestamp: &str) -> Utf8PathBuf {
25 let mirrored = paths::mirror_into_backup(backup_root, abs_target);
26 paths::append_timestamp(&mirrored, timestamp)
27}
28
29pub fn backup_file(src: &Utf8Path, dest: &Utf8Path) -> Result<()> {
31 if let Some(parent) = dest.parent() {
32 std::fs::create_dir_all(parent)?;
33 }
34 std::fs::copy(src, dest)?;
35 Ok(())
36}
37
38pub fn backup_dir(src: &Utf8Path, dest: &Utf8Path) -> Result<()> {
42 if let Some(parent) = dest.parent() {
43 std::fs::create_dir_all(parent)?;
44 }
45 copy_dir_recursive(src, dest)
46}
47
48fn copy_dir_recursive(src: &Utf8Path, dest: &Utf8Path) -> Result<()> {
49 std::fs::create_dir_all(dest)?;
50 for entry in std::fs::read_dir(src)? {
51 let entry = entry?;
52 let name_os = entry.file_name();
53 let Some(name) = name_os.to_str() else {
54 continue;
55 };
56 let src_path = src.join(name);
57 let dest_path = dest.join(name);
58 let ft = entry.file_type()?;
59 if ft.is_symlink() {
60 continue;
61 }
62 if ft.is_dir() {
63 copy_dir_recursive(&src_path, &dest_path)?;
64 } else if ft.is_file() {
65 std::fs::copy(&src_path, &dest_path)?;
66 }
67 }
68 Ok(())
69}
70
71#[cfg(test)]
72mod tests {
73 use super::*;
74 use tempfile::TempDir;
75
76 #[test]
77 fn backup_path_combines_mirror_and_timestamp() {
78 let r = backup_path(
79 Utf8Path::new("/dotfiles/.yui/backup"),
80 Utf8Path::new("/home/u/.config/foo.yml"),
81 "20260429_143022123",
82 );
83 assert_eq!(
84 r,
85 Utf8PathBuf::from("/dotfiles/.yui/backup/home/u/.config/foo_20260429_143022123.yml")
86 );
87 }
88
89 #[test]
90 fn current_timestamp_renders() {
91 let s = current_timestamp("%Y%m%d_%H%M%S").unwrap();
92 assert_eq!(s.len(), 15);
94 assert!(s.chars().nth(8) == Some('_'));
95 }
96
97 #[test]
98 fn backup_file_creates_parent_dirs() {
99 let tmp = TempDir::new().unwrap();
100 let src = Utf8PathBuf::from_path_buf(tmp.path().join("input.txt")).unwrap();
101 std::fs::write(&src, "hello").unwrap();
102 let dest = Utf8PathBuf::from_path_buf(tmp.path().join("nested/dir/out.txt")).unwrap();
103 backup_file(&src, &dest).unwrap();
104 assert_eq!(std::fs::read_to_string(&dest).unwrap(), "hello");
105 }
106
107 #[test]
108 fn backup_dir_copies_tree() {
109 let tmp = TempDir::new().unwrap();
110 let src = Utf8PathBuf::from_path_buf(tmp.path().join("src")).unwrap();
111 std::fs::create_dir_all(src.join("sub")).unwrap();
112 std::fs::write(src.join("a.txt"), "A").unwrap();
113 std::fs::write(src.join("sub/b.txt"), "B").unwrap();
114
115 let dest = Utf8PathBuf::from_path_buf(tmp.path().join("dest")).unwrap();
116 backup_dir(&src, &dest).unwrap();
117
118 assert_eq!(std::fs::read_to_string(dest.join("a.txt")).unwrap(), "A");
119 assert_eq!(
120 std::fs::read_to_string(dest.join("sub/b.txt")).unwrap(),
121 "B"
122 );
123 }
124}