exiftool_rs_wrapper/
file_ops.rs1use crate::ExifTool;
6use crate::error::Result;
7use crate::types::TagId;
8use std::path::{Path, PathBuf};
9
10#[derive(Debug, Clone)]
12pub enum RenamePattern {
13 DateTime { format: String },
15 Tag { tag: TagId, suffix: Option<String> },
17 Custom(String),
19}
20
21impl RenamePattern {
22 pub fn datetime(format: impl Into<String>) -> Self {
24 Self::DateTime {
25 format: format.into(),
26 }
27 }
28
29 pub fn tag(tag: TagId) -> Self {
31 Self::Tag { tag, suffix: None }
32 }
33
34 pub fn tag_with_suffix(tag: TagId, suffix: impl Into<String>) -> Self {
36 Self::Tag {
37 tag,
38 suffix: Some(suffix.into()),
39 }
40 }
41
42 pub fn custom(format: impl Into<String>) -> Self {
44 Self::Custom(format.into())
45 }
46
47 fn to_exiftool_format(&self) -> String {
49 match self {
50 Self::DateTime { format } => {
51 format!("%{{DateTimeOriginal,{}}}", format)
52 }
53 Self::Tag { tag, suffix } => {
54 if let Some(suf) = suffix {
55 format!("%{{{}}}{}", tag.name(), suf)
56 } else {
57 format!("%{{{}}}", tag.name())
58 }
59 }
60 Self::Custom(format) => format.clone(),
61 }
62 }
63}
64
65#[derive(Debug, Clone)]
67pub struct OrganizeOptions {
68 pub target_dir: PathBuf,
70 pub subdir_pattern: Option<RenamePattern>,
72 pub filename_pattern: RenamePattern,
74 pub extension: Option<String>,
76}
77
78impl OrganizeOptions {
79 pub fn new<P: AsRef<Path>>(target_dir: P) -> Self {
81 Self {
82 target_dir: target_dir.as_ref().to_path_buf(),
83 subdir_pattern: None,
84 filename_pattern: RenamePattern::datetime("%Y%m%d_%H%M%S"),
85 extension: None,
86 }
87 }
88
89 pub fn subdir(mut self, pattern: RenamePattern) -> Self {
91 self.subdir_pattern = Some(pattern);
92 self
93 }
94
95 pub fn filename(mut self, pattern: RenamePattern) -> Self {
97 self.filename_pattern = pattern;
98 self
99 }
100
101 pub fn extension(mut self, ext: impl Into<String>) -> Self {
103 self.extension = Some(ext.into());
104 self
105 }
106}
107
108pub trait FileOperations {
110 fn rename_file<P: AsRef<Path>>(&self, path: P, pattern: &RenamePattern) -> Result<PathBuf>;
112
113 fn rename_files<P: AsRef<Path>>(
115 &self,
116 paths: &[P],
117 pattern: &RenamePattern,
118 ) -> Result<Vec<PathBuf>>;
119
120 fn organize_by_date<P: AsRef<Path>, Q: AsRef<Path>>(
122 &self,
123 path: P,
124 target_dir: Q,
125 date_format: &str,
126 ) -> Result<PathBuf>;
127
128 fn organize<P: AsRef<Path>>(&self, path: P, options: &OrganizeOptions) -> Result<PathBuf>;
130
131 fn create_metadata_backup<P: AsRef<Path>, Q: AsRef<Path>>(
133 &self,
134 source: P,
135 backup_path: Q,
136 ) -> Result<()>;
137
138 fn restore_from_backup<P: AsRef<Path>, Q: AsRef<Path>>(
140 &self,
141 backup: P,
142 target: Q,
143 ) -> Result<()>;
144}
145
146impl FileOperations for ExifTool {
147 fn rename_file<P: AsRef<Path>>(&self, path: P, pattern: &RenamePattern) -> Result<PathBuf> {
148 let format = pattern.to_exiftool_format();
149
150 self.write(path.as_ref())
151 .arg(format!("-filename={}", format))
152 .execute()?;
153
154 let parent = path.as_ref().parent().unwrap_or(Path::new("."));
156 let new_name = format!(
157 "renamed_{}",
158 path.as_ref()
159 .file_name()
160 .unwrap_or_default()
161 .to_string_lossy()
162 );
163
164 Ok(parent.join(new_name))
165 }
166
167 fn rename_files<P: AsRef<Path>>(
168 &self,
169 paths: &[P],
170 pattern: &RenamePattern,
171 ) -> Result<Vec<PathBuf>> {
172 let mut results = Vec::with_capacity(paths.len());
173
174 for path in paths {
175 let new_path = self.rename_file(path, pattern)?;
176 results.push(new_path);
177 }
178
179 Ok(results)
180 }
181
182 fn organize_by_date<P: AsRef<Path>, Q: AsRef<Path>>(
183 &self,
184 path: P,
185 target_dir: Q,
186 date_format: &str,
187 ) -> Result<PathBuf> {
188 let options = OrganizeOptions::new(target_dir).subdir(RenamePattern::datetime(date_format));
189
190 self.organize(path, &options)
191 }
192
193 fn organize<P: AsRef<Path>>(&self, path: P, options: &OrganizeOptions) -> Result<PathBuf> {
194 let mut args = Vec::new();
195
196 args.push("-d".to_string());
198 args.push(options.target_dir.to_string_lossy().to_string());
199
200 if let Some(ref subdir) = options.subdir_pattern {
202 args.push(format!(
203 "-filename={}/{}",
204 subdir.to_exiftool_format(),
205 options.filename_pattern.to_exiftool_format()
206 ));
207 } else {
208 args.push(format!(
209 "-filename={}",
210 options.filename_pattern.to_exiftool_format()
211 ));
212 }
213
214 if let Some(ref ext) = options.extension {
216 args.push(format!("-ext {}", ext));
217 }
218
219 args.push(path.as_ref().to_string_lossy().to_string());
220
221 self.execute_raw(&args)?;
222
223 Ok(options.target_dir.clone())
225 }
226
227 fn create_metadata_backup<P: AsRef<Path>, Q: AsRef<Path>>(
228 &self,
229 source: P,
230 backup_path: Q,
231 ) -> Result<()> {
232 self.write(source)
233 .arg(format!("-o {}", backup_path.as_ref().display()))
234 .arg("-tagsFromFile @")
235 .arg("-all:all")
236 .execute()?;
237
238 Ok(())
239 }
240
241 fn restore_from_backup<P: AsRef<Path>, Q: AsRef<Path>>(
242 &self,
243 backup: P,
244 target: Q,
245 ) -> Result<()> {
246 self.write(target)
247 .copy_from(backup)
248 .overwrite_original(true)
249 .execute()?;
250
251 Ok(())
252 }
253}
254
255#[cfg(test)]
256mod tests {
257 use super::*;
258
259 #[test]
260 fn test_rename_pattern() {
261 let pattern = RenamePattern::datetime("%Y-%m-%d");
262 assert_eq!(pattern.to_exiftool_format(), "%{DateTimeOriginal,%Y-%m-%d}");
263
264 let pattern = RenamePattern::tag(TagId::MAKE);
265 assert_eq!(pattern.to_exiftool_format(), "%{Make}");
266
267 let pattern = RenamePattern::tag_with_suffix(TagId::MODEL, "_photo");
268 assert_eq!(pattern.to_exiftool_format(), "%{Model}_photo");
269 }
270
271 #[test]
272 fn test_organize_options() {
273 let opts = OrganizeOptions::new("/output")
274 .subdir(RenamePattern::datetime("%Y/%m"))
275 .filename(RenamePattern::datetime("%d_%H%M%S"))
276 .extension("jpg");
277
278 assert_eq!(opts.target_dir, PathBuf::from("/output"));
279 assert!(opts.subdir_pattern.is_some());
280 assert!(opts.extension.is_some());
281 }
282}