Skip to main content

exiftool_rs_wrapper/
file_ops.rs

1//! 文件操作模块
2//!
3//! 支持基于元数据的文件重命名、图像提取等操作
4
5use crate::ExifTool;
6use crate::error::Result;
7use crate::types::TagId;
8use std::path::{Path, PathBuf};
9
10/// 文件重命名模式
11#[derive(Debug, Clone)]
12pub enum RenamePattern {
13    /// 基于日期时间
14    DateTime { format: String },
15    /// 基于特定标签
16    Tag { tag: TagId, suffix: Option<String> },
17    /// 自定义格式字符串
18    Custom(String),
19}
20
21impl RenamePattern {
22    /// 创建日期时间模式
23    pub fn datetime(format: impl Into<String>) -> Self {
24        Self::DateTime {
25            format: format.into(),
26        }
27    }
28
29    /// 创建标签模式
30    pub fn tag(tag: TagId) -> Self {
31        Self::Tag { tag, suffix: None }
32    }
33
34    /// 创建带后缀的标签模式
35    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    /// 创建自定义模式
43    pub fn custom(format: impl Into<String>) -> Self {
44        Self::Custom(format.into())
45    }
46
47    /// 转换为 ExifTool 文件名格式
48    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/// 文件组织选项
66#[derive(Debug, Clone)]
67pub struct OrganizeOptions {
68    /// 目标目录
69    pub target_dir: PathBuf,
70    /// 子目录模式
71    pub subdir_pattern: Option<RenamePattern>,
72    /// 文件名模式
73    pub filename_pattern: RenamePattern,
74    /// 文件扩展名
75    pub extension: Option<String>,
76}
77
78impl OrganizeOptions {
79    /// 创建新的组织选项
80    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    /// 设置子目录模式
90    pub fn subdir(mut self, pattern: RenamePattern) -> Self {
91        self.subdir_pattern = Some(pattern);
92        self
93    }
94
95    /// 设置文件名模式
96    pub fn filename(mut self, pattern: RenamePattern) -> Self {
97        self.filename_pattern = pattern;
98        self
99    }
100
101    /// 设置文件扩展名
102    pub fn extension(mut self, ext: impl Into<String>) -> Self {
103        self.extension = Some(ext.into());
104        self
105    }
106}
107
108/// 文件操作 trait
109pub trait FileOperations {
110    /// 重命名单个文件
111    fn rename_file<P: AsRef<Path>>(&self, path: P, pattern: &RenamePattern) -> Result<PathBuf>;
112
113    /// 批量重命名文件
114    fn rename_files<P: AsRef<Path>>(
115        &self,
116        paths: &[P],
117        pattern: &RenamePattern,
118    ) -> Result<Vec<PathBuf>>;
119
120    /// 按日期组织文件到目录
121    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    /// 根据元数据组织文件
129    fn organize<P: AsRef<Path>>(&self, path: P, options: &OrganizeOptions) -> Result<PathBuf>;
130
131    /// 生成元数据备份文件 (.mie 格式)
132    fn create_metadata_backup<P: AsRef<Path>, Q: AsRef<Path>>(
133        &self,
134        source: P,
135        backup_path: Q,
136    ) -> Result<()>;
137
138    /// 从备份恢复元数据
139    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        // 构建新路径(简化版,实际需要查询 ExifTool 输出)
155        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        // 目录选项
197        args.push("-d".to_string());
198        args.push(options.target_dir.to_string_lossy().to_string());
199
200        // 子目录模式
201        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        // 文件扩展名
215        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        // 返回目标路径(简化版)
224        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}