1mod conflict;
6mod sort;
7mod walker;
8
9pub use sort::sort_bottom_up;
10
11use crate::{
12 plan_types::ItemKind, slugify::slugify_camel_iso_with_year, FrenError, PlanOpts, RenamePlan,
13 SlugOpts,
14};
15use chrono::{Datelike, Local};
16use std::ffi::OsString;
17use std::path::Path;
18use uuid::Uuid;
19
20pub fn plan(
33 roots: &[&Path],
34 slug_opts: &SlugOpts,
35 plan_opts: &PlanOpts,
36) -> Result<Vec<RenamePlan>, FrenError> {
37 let current_year = Local::now().year();
38 plan_with_year(roots, slug_opts, plan_opts, current_year)
39}
40
41pub fn plan_with_year(
43 roots: &[&Path],
44 slug_opts: &SlugOpts,
45 plan_opts: &PlanOpts,
46 current_year: i32,
47) -> Result<Vec<RenamePlan>, FrenError> {
48 let batch_id = Uuid::now_v7();
49 let mut plans = Vec::new();
50
51 for root in roots {
52 let items = walker::walk(root, plan_opts)?;
53 for item in items {
54 if item.path == *root {
57 continue;
58 }
59 let new_name = compute_new_name(&item, slug_opts, current_year);
60 let old_name = item
61 .path
62 .file_name()
63 .map(OsString::from)
64 .unwrap_or_default();
65 if Some(new_name.as_os_str()) == Some(old_name.as_os_str()) {
66 continue;
68 }
69 let parent = item
70 .path
71 .parent()
72 .map(Path::to_path_buf)
73 .unwrap_or_default();
74 plans.push(RenamePlan {
75 original_path: item.path.clone(),
76 parent,
77 old_name,
78 new_name,
79 depth: item.depth,
80 kind: item.kind,
81 detected_date: None,
82 batch_id,
83 });
84 }
85 }
86
87 sort_bottom_up(&mut plans);
88 conflict::check_within_batch(&plans)?;
89 if plan_opts.on_conflict == crate::ConflictPolicy::Abort {
90 conflict::check_preexisting(&plans)?;
91 }
92 Ok(plans)
93}
94
95fn compute_new_name(
96 item: &walker::DiscoveredItem,
97 slug_opts: &SlugOpts,
98 current_year: i32,
99) -> OsString {
100 let raw_name = item
101 .path
102 .file_name()
103 .map(|n| n.to_string_lossy().into_owned())
104 .unwrap_or_default();
105 match item.kind {
106 ItemKind::Dir => slugify_camel_iso_with_year(&raw_name, slug_opts, current_year).into(),
107 ItemKind::File | ItemKind::Symlink => {
108 let path = std::path::Path::new(&raw_name);
109 let stem = path
110 .file_stem()
111 .map(|s| s.to_string_lossy().into_owned())
112 .unwrap_or_default();
113 let ext = path
114 .extension()
115 .map(|e| e.to_string_lossy().into_owned())
116 .unwrap_or_default();
117 let new_stem = slugify_camel_iso_with_year(&stem, slug_opts, current_year);
118 if ext.is_empty() {
119 new_stem.into()
120 } else {
121 format!("{}.{}", new_stem, ext.to_lowercase()).into()
122 }
123 }
124 }
125}