Skip to content

Commit

Permalink
feat: allow specifying a format string that allows subfolder creation #…
Browse files Browse the repository at this point in the history
  • Loading branch information
0xCCF4 committed Sep 13, 2024
1 parent 13909d6 commit 2484194
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 20 deletions.
40 changes: 37 additions & 3 deletions src/action.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use anyhow::{anyhow, Result};
use filetime::FileTime;
use log::{debug, error, warn};
use std::fmt::{Display, Formatter};
Expand Down Expand Up @@ -82,6 +83,7 @@ impl FromStr for ActualAction {
/// * `source` - A PathBuf reference to the source file.
/// * `target` - A PathBuf reference to the target file.
/// * `action` - An ActionMode reference specifying the action to be performed.
/// * `mkdir` - Mkdir subfolders on the way, in dry-run mode no subfolders are created.
///
/// # Returns
///
Expand All @@ -103,15 +105,47 @@ impl FromStr for ActualAction {
///
/// * The target file already exists.
/// * An error occurred during the file operation.
pub fn file_action(source: &PathBuf, target: &PathBuf, action: &ActionMode) -> std::io::Result<()> {
error_file_exists(target)?;
match action {
pub fn file_action(
source: &PathBuf,
target: &PathBuf,
action: &ActionMode,
mkdir: bool,
) -> Result<()> {
error_file_exists(target)
.map_err(|e| anyhow!("Target file already exists: {:?} - {:?}", target, e))?;

// check if parent folder exists
if let Some(parent) = target.parent() {
if !parent.exists() {
if !mkdir {
return Err(anyhow!(
"Target subfolder does not exist. Use --mkdir to create it: {:?}",
parent
));
}

if matches!(action, ActionMode::DryRun(_)) {
println!("[Mkdir] {:?}", parent);
} else {
fs::create_dir_all(parent).map_err(|e| {
anyhow!("Failed to create target subfolder: {:?} - {:?}", parent, e)
})?;
}
}
}

let result = match action {
ActionMode::Execute(ActualAction::Move) => move_file(source, target),
ActionMode::Execute(ActualAction::Copy) => copy_file(source, target),
ActionMode::Execute(ActualAction::Hardlink) => hardlink_file(source, target),
ActionMode::Execute(ActualAction::RelativeSymlink) => relative_symlink_file(source, target),
ActionMode::Execute(ActualAction::AbsoluteSymlink) => absolute_symlink_file(source, target),
ActionMode::DryRun(action) => dry_run(source, target, action),
};

match result {
Ok(_) => Ok(()),
Err(e) => Err(anyhow!("Failed to perform action: {:?}", e)),
}
}

Expand Down
40 changes: 30 additions & 10 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use log::{debug, error, info, warn};
use std::ffi::OsStr;
use std::fs;
use std::fs::File;
use std::path::{Path, PathBuf};
use std::path::PathBuf;
use std::str::FromStr;

pub mod action;
Expand Down Expand Up @@ -71,6 +71,7 @@ impl FromStr for AnalysisType {
/// * `date_format` - A string that represents the format of the dates in the files to analyze.
/// * `extensions` - A vector of strings that represent the file extensions to consider during analysis.
/// * `action_type` - An `ActionMode` that specifies the type of action to perform on a file after analysis.
/// * `mkdir` - A boolean that indicates whether to create the target directory if it does not exist.
#[derive(Debug, Clone)]
pub struct AnalyzerSettings {
pub analysis_type: AnalysisType,
Expand All @@ -83,6 +84,7 @@ pub struct AnalyzerSettings {
#[cfg(feature = "video")]
pub video_extensions: Vec<String>,
pub action_type: ActionMode,
pub mkdir: bool,
}

lazy_static! {
Expand Down Expand Up @@ -462,14 +464,27 @@ impl Analyzer {
};

let new_file_path = |file_name_info: &NameFormatterInvocationInfo| -> Result<PathBuf> {
self.replace_filepath_parts(self.settings.file_format.as_str(), file_name_info)
.map(|target_name| {
self.settings.target_dir.join(
Path::new("").with_file_name(target_name).with_extension(
path.extension().expect("There should be an extension"),
),
)
})
let path_split: Vec<_> = self
.settings
.file_format
.split('/')
.map(|component| self.replace_filepath_parts(component, file_name_info))
.collect();
for entry in &path_split {
if let Err(err) = entry {
return Err(anyhow!("Failed to format filename: {}", err));
}
}
let path_split = path_split.into_iter().map(Result::unwrap);

let mut target_path = self.settings.target_dir.clone();
for path_component in path_split {
let component = path_component.replace("/", "").replace("\\", "");
if component != ".." {
target_path.push(component);
}
}
Ok(target_path.with_extension(path.extension().expect("There should be an extension")))
};

let mut new_path = new_file_path(&file_name_info)?;
Expand All @@ -486,7 +501,12 @@ impl Analyzer {
info!("De-duplicated target file: {:?}", new_path);
}

action::file_action(path, &new_path, &self.settings.action_type)?;
action::file_action(
path,
&new_path,
&self.settings.action_type,
self.settings.mkdir,
)?;
Ok(())
}

Expand Down
20 changes: 13 additions & 7 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,23 @@ struct Arguments {
/// See [https://docs.rs/chrono/latest/chrono/format/strftime/index.html] for more information.
#[arg(long, default_value = "%Y%m%d-%H%M%S")]
date_format: String,
/// The target file format. Everything outside a {...} block is copied as is.
/// {name} / {n} is replaced with a filename without the date part.
/// {dup} is replaced with a number if a file with the target name already exists.
/// {date} / {d} is replaced with the date string, formatted according to the date_format parameter.
/// {date?<format>} is replaced with the date string, formatted according to the <format> parameter. See [https://docs.rs/chrono/latest/chrono/format/strftime/index.html] for more information.
/// {type} / {t} is replaced with MOV or IMG.
/// {type?<img>,<vid>} is replaced with <img> if the file is an image, <vid> if the file is a video. Note that, when using other types than IMG or MOV,
/// The target file format. Everything outside a {...} block is copied as is. The target file format may contain "/" to
/// indicate that the file should be placed in a subdirectory. Use the `--mkdir` flag to create the subdirectories.
/// `{name}` is replaced with a filename without the date part.
/// `{dup}` is replaced with a number if a file with the target name already exists.
/// `{date}` is replaced with the date string, formatted according to the date_format parameter.
/// `{date?format}` is replaced with the date string, formatted according to the "format" parameter. See [https://docs.rs/chrono/latest/chrono/format/strftime/index.html] for more information.
/// `{type}` is replaced with MOV or IMG.
/// `{type?img,vid}` is replaced with `img` if the file is an image, `vid` if the file is a video. Note that, when using other types than IMG or MOV,
/// and rerunning the program again, the custom type will be seen as part of the file name.
/// Commands of the form {label:cmd} are replaced by {cmd}; if the replacement string is not empty then a prefix of "label" is added.
/// This might be useful to add separators only if there is e.g. a {dup} part.
#[arg(short, long, default_value = "{type}{_:date}{-:name}{-:dup}")]
file_format: String,
/// If the file format contains a "/", indicating that the file should be placed in a subdirectory,
/// the mkdir flag controls if the tool is allowed to create non-existing subdirectories. No folder is created in dry-run mode.
#[arg(long, default_value = "false", alias = "mkdirs")]
mkdir: bool,
/// A comma separated list of file extensions to include in the analysis.
#[arg(short, long, default_value = "jpg,jpeg,png,tiff,heif,heic,avif,webp", value_delimiter = ',', num_args = 0..)]
extensions: Vec<String>,
Expand Down Expand Up @@ -87,6 +92,7 @@ fn main() {
file_format: args.file_format.clone(),
date_format: args.date_format.clone(),
extensions: args.extensions.clone(),
mkdir: args.mkdir,
action_type: if args.dry_run {
action::ActionMode::DryRun(args.move_mode)
} else {
Expand Down

0 comments on commit 2484194

Please sign in to comment.