Skip to main content

floe_core/io/write/
parts.rs

1use 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}