floe_core/io/write/
parts.rs1use std::path::{Path, PathBuf};
2
3use crate::{io, FloeResult};
4use uuid::Uuid;
5
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct PartFile {
8 pub path: PathBuf,
9 pub file_name: String,
10 pub index: usize,
11}
12
13#[derive(Debug, Clone)]
14pub struct PartNameAllocator {
15 next_index: Option<usize>,
16 extension: String,
17}
18
19impl PartNameAllocator {
20 pub fn from_local_path(base_path: &Path, extension: &str) -> FloeResult<Self> {
21 Ok(Self {
22 next_index: Some(next_local_part_index(base_path, extension)?),
23 extension: normalize_extension(extension),
24 })
25 }
26
27 pub fn from_next_index(next_index: usize, extension: &str) -> Self {
28 Self {
29 next_index: Some(next_index),
30 extension: normalize_extension(extension),
31 }
32 }
33
34 pub fn unique(extension: &str) -> Self {
35 Self {
36 next_index: None,
37 extension: normalize_extension(extension),
38 }
39 }
40
41 pub fn allocate_next(&mut self) -> String {
42 match self.next_index {
43 Some(index) => {
44 let file_name = part_filename(index, &self.extension);
45 self.next_index = Some(index.saturating_add(1));
46 file_name
47 }
48 None => append_part_filename(&self.extension),
49 }
50 }
51}
52
53pub fn part_filename(index: usize, extension: &str) -> String {
54 let extension = normalize_extension(extension);
55 let stem = io::storage::paths::build_part_stem(index);
56 io::storage::paths::build_output_filename(&stem, "", &extension)
57}
58
59pub fn append_part_filename(extension: &str) -> String {
60 let extension = normalize_extension(extension);
61 let id = Uuid::new_v4();
62 format!("part-{id}.{extension}")
63}
64
65pub fn is_part_filename(file_name: &str, extension: &str) -> bool {
66 let extension = normalize_extension(extension);
67 let path = Path::new(file_name);
68 if path.extension().and_then(|ext| ext.to_str()) != Some(extension.as_str()) {
69 return false;
70 }
71 let stem = match path.file_stem().and_then(|stem| stem.to_str()) {
72 Some(stem) => stem,
73 None => return false,
74 };
75 match stem.strip_prefix("part-") {
76 Some(rest) => !rest.is_empty(),
77 None => false,
78 }
79}
80
81pub fn is_part_key(key: &str, extension: &str) -> bool {
82 let file_name = match Path::new(key).file_name().and_then(|name| name.to_str()) {
83 Some(name) => name,
84 None => return false,
85 };
86 is_part_filename(file_name, extension)
87}
88
89pub fn list_local_part_files(base_path: &Path, extension: &str) -> FloeResult<Vec<PartFile>> {
90 if base_path.as_os_str().is_empty() || !base_path.exists() || base_path.is_file() {
91 return Ok(Vec::new());
92 }
93
94 let extension = normalize_extension(extension);
95 let mut parts = Vec::new();
96 for entry in std::fs::read_dir(base_path)? {
97 let entry = entry?;
98 if !entry.file_type()?.is_file() {
99 continue;
100 }
101 let file_name = entry.file_name();
102 let Some(file_name) = file_name.to_str() else {
103 continue;
104 };
105 let Some(index) = parse_part_index(file_name, &extension) else {
106 continue;
107 };
108 parts.push(PartFile {
109 path: entry.path(),
110 file_name: file_name.to_string(),
111 index,
112 });
113 }
114 parts.sort_by(|left, right| {
115 left.index
116 .cmp(&right.index)
117 .then_with(|| left.file_name.cmp(&right.file_name))
118 });
119 Ok(parts)
120}
121
122pub fn list_local_part_paths(base_path: &Path, extension: &str) -> FloeResult<Vec<PathBuf>> {
123 if base_path.as_os_str().is_empty() || !base_path.exists() || base_path.is_file() {
124 return Ok(Vec::new());
125 }
126
127 let extension = normalize_extension(extension);
128 let mut entries = Vec::new();
129 for entry in std::fs::read_dir(base_path)? {
130 let entry = entry?;
131 if !entry.file_type()?.is_file() {
132 continue;
133 }
134 let file_name = entry.file_name();
135 let Some(file_name) = file_name.to_str() else {
136 continue;
137 };
138 if !is_part_filename(file_name, &extension) {
139 continue;
140 }
141 entries.push((file_name.to_string(), entry.path()));
142 }
143 entries.sort_by(|left, right| left.0.cmp(&right.0));
144 Ok(entries.into_iter().map(|(_, path)| path).collect())
145}
146
147pub fn next_local_part_index(base_path: &Path, extension: &str) -> FloeResult<usize> {
148 let part_files = list_local_part_files(base_path, extension)?;
149 Ok(part_files.last().map(|part| part.index + 1).unwrap_or(0))
150}
151
152pub fn next_local_part_filename(base_path: &Path, extension: &str) -> FloeResult<String> {
153 let next_index = next_local_part_index(base_path, extension)?;
154 Ok(part_filename(next_index, extension))
155}
156
157pub fn clear_local_part_files(base_path: &Path, extension: &str) -> FloeResult<usize> {
158 if base_path.as_os_str().is_empty() || !base_path.exists() {
159 return Ok(0);
160 }
161 if base_path.is_file() {
162 std::fs::remove_file(base_path)?;
163 std::fs::create_dir_all(base_path)?;
164 return Ok(0);
165 }
166
167 let mut removed = 0usize;
168 for entry in std::fs::read_dir(base_path)? {
169 let entry = entry?;
170 if !entry.file_type()?.is_file() {
171 continue;
172 }
173 let file_name = entry.file_name();
174 let Some(file_name) = file_name.to_str() else {
175 continue;
176 };
177 if is_part_filename(file_name, extension) {
178 std::fs::remove_file(entry.path())?;
179 removed += 1;
180 }
181 }
182 Ok(removed)
183}
184
185fn parse_part_index(file_name: &str, extension: &str) -> Option<usize> {
186 let path = Path::new(file_name);
187 if path.extension()?.to_str()? != extension {
188 return None;
189 }
190 let stem = path.file_stem()?.to_str()?;
191 let digits = stem.strip_prefix("part-")?;
192 if digits.len() < 5 || !digits.bytes().all(|value| value.is_ascii_digit()) {
193 return None;
194 }
195 digits.parse::<usize>().ok()
196}
197
198fn normalize_extension(extension: &str) -> String {
199 extension.trim_start_matches('.').to_string()
200}