Skip to content

Commit

Permalink
working tests
Browse files Browse the repository at this point in the history
  • Loading branch information
igaray committed Jan 10, 2025
1 parent 6851cab commit 083096e
Show file tree
Hide file tree
Showing 7 changed files with 960 additions and 1,207 deletions.
41 changes: 3 additions & 38 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,47 +4,12 @@ extern crate lalrpop_util;
extern crate regex;
extern crate walkdir;

use clap::Parser;
use clap::Subcommand;
use crate::clap::Parser;
use crate::ocd::Cli;
use crate::ocd::OcdCommand;

mod ocd;

/// The command line interface configuration.
#[derive(Debug, Parser)]
#[clap(name = "ocd")]
#[clap(author = "Iñaki Garay <[email protected]>")]
#[clap(version = env!("VERSION_STR") )] // set in build.rs
#[clap(about = "A swiss army knife of utilities to work with files.")]
#[clap(long_about = None)]
struct Cli {
#[clap(subcommand)]
command: OcdCommand,
}

/// All OCD commands.
#[derive(Clone, Debug, Subcommand)]
enum OcdCommand {
#[clap(about = "Mass Re-Name")]
#[clap(name = "mrn")]
MassRename(crate::ocd::mrn::MassRenameArgs),

#[clap(about = "Time Stamp Sort")]
#[clap(name = "tss")]
TimeStampSort(crate::ocd::tss::TimeStampSortArgs),

#[clap(about = "Fix ID3 tags")]
#[clap(name = "id3")]
FixID3 {},

#[clap(about = "Run the Elephant client")]
#[clap(name = "lphc")]
ElephantClient {},

#[clap(about = "Start the Elephant server")]
#[clap(name = "lphs")]
ElephantServer {},
}

fn main() {
let cli = Cli::parse();
match cli.command {
Expand Down
55 changes: 39 additions & 16 deletions src/ocd/date.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,35 +9,28 @@ use std::path::PathBuf;
use std::sync::LazyLock;

// The default date regex string.
pub const DATE_FLORB_REGEX_STR: &str = r"(?<date>[0-9];{4}.?[0-9]{2}.?[0-9]{2}|(?:(?:\d{1,2})\s(?i)(?:jan|january|feb|february|mar|march|apr|april|may|jun|june|jul|july|aug|august|sep|september|oct|october|nov|november|dec|december)\s(?:\d{1,4})))";
pub static DATE_FLORB_REGEX: LazyLock<Regex> =
pub(crate) const DATE_FLORB_REGEX_STR: &str = r"(?<date>[0-9];{4}.?[0-9]{2}.?[0-9]{2}|(?:(?:\d{1,2})\s(?i)(?:jan|january|feb|february|mar|march|apr|april|may|jun|june|jul|july|aug|august|sep|september|oct|october|nov|november|dec|december)\s(?:\d{1,4})))";
pub(crate) static DATE_FLORB_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(DEFAULT_DATEFINDER_REGEX_STR).unwrap());
// pub const DEFAULT_DATE_REGEX_STR: &str = r"(?<a>(?<y1>1\d\d\d|20\d\d).?(?<m1>0[1-9]|1[012]).?(?<d1>0[1-9]|[12]\d|30|31))|(?i)(?<b>(?<d2>\d{1,2})\s(?<m2>jan|january|feb|february|mar|march|apr|april|may|jun|june|jul|july|aug|august|sep|september|oct|october|nov|november|dec|december)\s(?<y2>\d{1,4}))";

/// The default datefinder reges string is the same as the default date regex but includes non-alphanumeric catch-all patterns before and after.
/// Case A: a date in the format YYYY?MM?DD or YYYYMMDD
/// `(?<a>(?<y1>1\d\d\d|20\d\d).?(?<m1>0[1-9]|1[012]).?(?<d1>0[1-9]|[12]\d|30|31))`
/// Case B: case insensitive, DD MONTH YYYY
/// where MONTH may be the full month name or the three letter short version.
/// `(?i)(?<b>(?<d2>\d{1,2})\s(?<m2>jan|january|feb|february|mar|march|apr|april|may|jun|june|jul|july|aug|august|sep|september|oct|october|nov|november|dec|december)\s(?<y2>\d{1,4}))`
pub const DEFAULT_DATEFINDER_REGEX_STR: &str = r"\D*(?<a>(?<y1>1\d\d\d|20\d\d).?(?<m1>0[1-9]|1[012]).?(?<d1>0[1-9]|[12]\d|30|31))|(?i)(?<b>(?<d2>\d{1,2})\s(?<m2>jan|january|feb|february|mar|march|apr|april|may|jun|june|jul|july|aug|august|sep|september|oct|october|nov|november|dec|december)\s(?<y2>\d{1,4}))\D*";
pub static DEFAULT_DATEFINDER_REGEX: LazyLock<Regex> =
const DEFAULT_DATEFINDER_REGEX_STR: &str = r"\D*(?<a>(?<y1>1\d\d\d|20\d\d).?(?<m1>0[1-9]|1[012]).?(?<d1>0[1-9]|[12]\d|30|31))|(?i)(?<b>(?<d2>\d{1,2})\s(?<m2>jan|january|feb|february|mar|march|apr|april|may|jun|june|jul|july|aug|august|sep|september|oct|october|nov|november|dec|december)\s(?<y2>\d{1,4}))\D*";
pub(crate) static DEFAULT_DATEFINDER_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(DEFAULT_DATEFINDER_REGEX_STR).unwrap());

#[derive(Debug, Default)]
pub enum DateFlorbRegex {
#[default]
Default,
}

#[derive(Debug, PartialEq)]
pub enum DateSource {
pub(crate) enum DateSource {
Filename,
Exif,
Filesystem,
}

pub fn regex_date(haystack: &str) -> Option<(u32, u32, u32)> {
pub(crate) fn regex_date(haystack: &str) -> Option<(u32, u32, u32)> {
DEFAULT_DATEFINDER_REGEX.captures(haystack).map(|capture| {
if capture.name("a").is_some() {
let year = capture.get(2).unwrap().as_str().parse::<u32>().unwrap();
Expand Down Expand Up @@ -81,7 +74,7 @@ fn english_month_to_number(month: &str) -> u32 {
}

/// Given a filename, extracts a date by matching against a regex.
pub fn filename_date(file_name: &Path) -> Option<(DateSource, u32, u32, u32)> {
pub(crate) fn filename_date(file_name: &Path) -> Option<(DateSource, u32, u32, u32)> {
file_name
.to_str()
.and_then(crate::ocd::date::regex_date)
Expand All @@ -98,7 +91,7 @@ pub fn filename_date(file_name: &Path) -> Option<(DateSource, u32, u32, u32)> {
/// - if for some reason the exif tag is not in the right format and
/// `chrono::NaiveDateTime::parse_from_str` cannot parse it, parse the result
/// with `dateparser::parse` as a date with format `"%Y-%m-%d`.
pub fn exif_date(path: &PathBuf) -> Option<(DateSource, u32, u32, u32)> {
pub(crate) fn exif_date(path: &PathBuf) -> Option<(DateSource, u32, u32, u32)> {
std::fs::File::open(path).ok().and_then(|file| {
let mut bufreader = std::io::BufReader::new(&file);
exif::Reader::new()
Expand Down Expand Up @@ -138,7 +131,7 @@ pub fn exif_date(path: &PathBuf) -> Option<(DateSource, u32, u32, u32)> {
/// - obtain the file metadata
/// - get the `created` field
/// - check whether the created is the same as the current date
pub fn metadata_date(path: &PathBuf) -> Option<(DateSource, u32, u32, u32)> {
pub(crate) fn metadata_date(path: &PathBuf) -> Option<(DateSource, u32, u32, u32)> {
std::fs::metadata(path).ok().and_then(|metadata| {
metadata.created().ok().and_then(|system_time| {
let today: chrono::DateTime<chrono::offset::Local> = chrono::Local::now();
Expand All @@ -155,3 +148,33 @@ pub fn metadata_date(path: &PathBuf) -> Option<(DateSource, u32, u32, u32)> {
})
})
}

#[cfg(test)]
mod test {
use super::*;
use std::path::Path;

#[test]
fn filename_date1() {
let file_name = Path::new("An image file from 2024-12-31.jpg");
let expected = Some((DateSource::Filename, 2024, 12, 31));
let result = filename_date(file_name);
assert_eq!(expected, result);
}

#[test]
fn filename_date2() {
let file_name = Path::new("An image file from 20241231.jpg");
let expected = Some((DateSource::Filename, 2024, 12, 31));
let result = filename_date(file_name);
assert_eq!(expected, result);
}

#[test]
fn filename_date3() {
let file_name = Path::new("An image file from 2024-12-01 to 2024-12-31.jpg");
let expected = Some((DateSource::Filename, 2024, 12, 1));
let result = filename_date(file_name);
assert_eq!(expected, result);
}
}
68 changes: 53 additions & 15 deletions src/ocd/mod.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
//! Main OCD module.
pub(crate) mod date;
mod date;
pub(crate) mod mrn;
pub(crate) mod tests;
mod tests;

Check failure on line 4 in src/ocd/mod.rs

View workflow job for this annotation

GitHub Actions / Check

file not found for module `tests`

Check failure on line 4 in src/ocd/mod.rs

View workflow job for this annotation

GitHub Actions / Test Suite

file not found for module `tests`

Check failure on line 4 in src/ocd/mod.rs

View workflow job for this annotation

GitHub Actions / Clippy

file not found for module `tests`
pub(crate) mod tss;

use crate::ocd::date::DateSource;
use clap::Parser;
use clap::Subcommand;
use clap::ValueEnum;
use dialoguer::Confirm;
use dialoguer::Input;
Expand All @@ -20,16 +22,52 @@ use std::path::Path;
use std::path::PathBuf;
use std::process::Command;

/// The command line interface configuration.
#[derive(Debug, Parser)]
#[clap(name = "ocd")]
#[clap(author = "Iñaki Garay <[email protected]>")]
#[clap(version = env!("VERSION_STR") )] // set in build.rs
#[clap(about = "A swiss army knife of utilities to work with files.")]
#[clap(long_about = None)]
pub struct Cli {
#[clap(subcommand)]
pub command: OcdCommand,
}

/// All OCD commands.
#[derive(Clone, Debug, Subcommand)]
pub enum OcdCommand {
#[clap(about = "Mass Re-Name")]
#[clap(name = "mrn")]
MassRename(crate::ocd::mrn::MassRenameArgs),

#[clap(about = "Time Stamp Sort")]
#[clap(name = "tss")]
TimeStampSort(crate::ocd::tss::TimeStampSortArgs),

#[clap(about = "Fix ID3 tags")]
#[clap(name = "id3")]
FixID3 {},

#[clap(about = "Run the Elephant client")]
#[clap(name = "lphc")]
ElephantClient {},

#[clap(about = "Start the Elephant server")]
#[clap(name = "lphs")]
ElephantServer {},
}

/// File processing mode, filters only regular files, only directories, or both.
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
pub(crate) enum Mode {
enum Mode {
All,
Directories,
Files,
}

#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
pub(crate) enum Verbosity {
enum Verbosity {
Silent,
Low,
Medium,
Expand Down Expand Up @@ -80,7 +118,7 @@ trait Speaker {
/// the `Move` variant which is only used in the Time Stamp Sorted utility, it
/// is inluded there.
#[derive(Debug)]
pub(crate) enum Action {
enum Action {
Move {
date_source: Option<DateSource>,
path: PathBuf,
Expand All @@ -103,7 +141,7 @@ impl Display for Action {
/// created (to include deletion instructions in an undo file), whether or not
/// git is to be used to perform actions on the filesystem, and string lengths
/// for presentation.
pub(crate) struct Plan {
struct Plan {
pub actions: BTreeMap<PathBuf, Action>,
dirs: HashSet<PathBuf>,
use_git: bool,
Expand All @@ -112,7 +150,7 @@ pub(crate) struct Plan {
}

impl Plan {
pub(crate) fn new() -> Self {
fn new() -> Self {
Plan {
dirs: HashSet::new(),
actions: BTreeMap::new(),
Expand All @@ -122,12 +160,12 @@ impl Plan {
}
}

pub(crate) fn with_git(mut self, use_git: bool) -> Self {
fn with_git(mut self, use_git: bool) -> Self {
self.use_git = use_git;
self
}

pub(crate) fn with_files(mut self, files: Vec<PathBuf>) -> Self {
fn with_files(mut self, files: Vec<PathBuf>) -> Self {
for file in files {
self.insert(file.clone(), Action::Rename { path: file.clone() });
}
Expand All @@ -136,7 +174,7 @@ impl Plan {

/// Removes all actions in plan which would result in the file being renamed
/// into itself or moved into the current directory.
pub(crate) fn clean(&mut self) {
fn clean(&mut self) {
// Retains only the elements specified by the predicate.
// In other words, remove all pairs for which the predicate returns false.
self.actions.retain(|src, action| match action {
Expand All @@ -145,7 +183,7 @@ impl Plan {
})
}

pub(crate) fn insert(&mut self, src: PathBuf, action: Action) {
fn insert(&mut self, src: PathBuf, action: Action) {
let path = match action {
Action::Move { ref path, .. } => {
// In the case of a move, the program will have created a
Expand All @@ -170,7 +208,7 @@ impl Plan {
self.actions.insert(src, action);
}

pub(crate) fn present_short(&self) {
fn present_short(&self) {
let msl = self.max_src_len;
let mdl = self.max_dst_len;
for (src, action) in &self.actions {
Expand All @@ -189,7 +227,7 @@ impl Plan {
}
}

pub(crate) fn present_long(&self) {
fn present_long(&self) {
println!("Result:");
for (src, action) in &self.actions {
match action {
Expand All @@ -208,7 +246,7 @@ impl Plan {
}
}

pub(crate) fn execute(&self) -> Result<(), Box<dyn Error>> {
fn execute(&self) -> Result<(), Box<dyn Error>> {
for (src, action) in &self.actions {
match action {
Action::Move { path, .. } => {
Expand All @@ -223,7 +261,7 @@ impl Plan {
Ok(())
}

pub(crate) fn create_undo(&self) -> io::Result<()> {
fn create_undo(&self) -> io::Result<()> {
let git = if self.use_git { "git " } else { "" };
let mut undo_file = std::fs::File::create("undo.sh")?;
for (src, action) in &self.actions {
Expand Down
Loading

0 comments on commit 083096e

Please sign in to comment.