1use std::{
2 cmp::max,
3 fs::{rename, DirEntry},
4 io,
5 path::Path,
6};
7
8use anyhow::{anyhow, bail, Result};
9
10pub fn get_sorted_files_in_dir(directory_path: &Path) -> Result<Vec<DirEntry>> {
11 if directory_path.is_file() {
12 bail!(
13 "Invalid directory path ('{}' is a file)!",
14 directory_path.to_string_lossy()
15 );
16 }
17
18 let mut files: Vec<DirEntry> = directory_path
19 .read_dir()?
20 .into_iter()
21 .filter(|entry| {
22 if let Ok(entry) = entry {
23 is_valid_name(&entry.file_name().to_string_lossy())
24 } else {
25 false
26 }
27 })
28 .collect::<Result<Vec<DirEntry>, io::Error>>()?;
29
30 files.sort_by(|f1, f2| {
31 num_prefix(&entry_to_file_name(f1))
32 .expect("Not valid but already checked is_valid")
33 .cmp(
34 &num_prefix(&entry_to_file_name(f2))
35 .expect("Not valid but already checked is_valid"),
36 )
37 });
38
39 Ok(files)
40}
41
42pub fn is_valid_name(file_name: &str) -> bool {
43 is_numeric(&file_prefix(file_name))
44}
45
46pub fn file_prefix(file_name: &str) -> String {
47 file_name
48 .to_string()
49 .split_once('-')
50 .map_or(file_name.to_string(), |(start, _)| start.to_string())
51}
52
53pub fn is_numeric(file_prefix: &str) -> bool {
54 file_prefix.chars().all(|c| c.is_ascii_digit())
55}
56
57pub fn num_prefix(file_name: &str) -> Result<usize> {
58 let prefix = file_prefix(file_name);
59 if is_numeric(&prefix) {
60 Ok(prefix.parse()?)
61 } else {
62 Err(anyhow!("'{}' is an invalid file name!", file_name))
63 }
64}
65
66pub fn entry_to_file_name(entry: &DirEntry) -> String {
67 entry.file_name().to_string_lossy().to_string()
68}
69
70pub fn replaced_index_name_unchecked(name: &str, new_index: usize, padding: usize) -> String {
71 let mut new_name = {
72 let mut index = format!("{:0width$}", new_index, width = padding);
73 index.push('-');
74 index
75 };
76 let (_, old_end): (&str, &str) = name
77 .split_once('-')
78 .expect(&*format!("'{}' is an invalid file name!", name));
79 new_name.push_str(old_end);
80
81 new_name
82}
83
84pub fn rename_files_from_index(
85 mut entries: Vec<DirEntry>,
86 start_index: Option<usize>,
87 parent_dir: &Path,
88 padding: usize,
89) -> Result<usize> {
90 let mut new_names: Vec<String> = Vec::new();
91 for (current_index, entry) in entries.iter().enumerate() {
92 let file_name = entry_to_file_name(entry);
93 let expected_index = if let Some(start_index) = &start_index {
94 current_index + *start_index + 1
95 } else {
96 current_index
97 };
98
99 let new_name = if num_prefix(&file_name)? != expected_index {
100 replaced_index_name_unchecked(&file_name, expected_index, padding)
101 } else {
102 file_name
103 };
104
105 new_names.push(new_name);
106 }
107
108 entries.reverse();
109 new_names.reverse();
110
111 let mut rename_count = 0;
112 entries
113 .into_iter()
114 .enumerate()
115 .try_for_each(|(index, entry)| {
116 let entry_name = &entry_to_file_name(&entry);
117 let new_name = &new_names[index];
118 if entry_name != new_name {
119 let new_path = parent_dir.join(new_name);
120 rename_count += 1;
121 rename(entry.path(), new_path).map_err(|err| {
122 anyhow!(
123 "Error renaming file '{}' to '{}' - {}",
124 entry_name,
125 new_name,
126 err
127 )
128 })
129 } else {
130 Ok(())
131 }
132 })?;
133 Ok(rename_count)
134}
135
136pub fn get_padding_digits(all_files: &[DirEntry]) -> (usize, usize) {
137 let digits_needed = max((all_files.len() as f64).log10().ceil() as usize, 2);
138 let digits_used = if let Some(file) = all_files.get(0) {
139 file_prefix(&entry_to_file_name(file)).len()
140 } else {
141 0
142 };
143 (digits_used, digits_needed)
144}