Skip to main content

floe_core/io/storage/
planner.rs

1use std::collections::hash_map::DefaultHasher;
2use std::hash::{Hash, Hasher};
3use std::path::Path;
4use std::path::PathBuf;
5
6use crate::FloeResult;
7
8#[derive(Debug, Clone)]
9pub struct ObjectRef {
10    pub uri: String,
11    pub key: String,
12    pub last_modified: Option<String>,
13    pub size: Option<u64>,
14}
15
16pub fn join_prefix(prefix: &str, name: &str) -> String {
17    let left = prefix.trim_matches('/');
18    let right = name.trim_matches('/');
19    if left.is_empty() {
20        right.to_string()
21    } else if right.is_empty() {
22        left.to_string()
23    } else {
24        format!("{left}/{right}")
25    }
26}
27
28pub fn normalize_separators(value: &str) -> String {
29    value.replace('\\', "/").trim_matches('/').to_string()
30}
31
32pub fn stable_sort_refs(mut refs: Vec<ObjectRef>) -> Vec<ObjectRef> {
33    refs.sort_by(|a, b| a.uri.cmp(&b.uri));
34    refs
35}
36
37pub fn filter_by_suffixes(refs: Vec<ObjectRef>, suffixes: &[String]) -> Vec<ObjectRef> {
38    let suffixes = suffixes
39        .iter()
40        .map(|suffix| suffix.to_ascii_lowercase())
41        .collect::<Vec<_>>();
42    refs.into_iter()
43        .filter(|obj| {
44            let lower = obj.uri.to_ascii_lowercase();
45            !lower.ends_with('/') && suffixes.iter().any(|suffix| lower.ends_with(suffix))
46        })
47        .collect()
48}
49
50pub fn ensure_parent_dir(path: &Path) -> FloeResult<()> {
51    if let Some(parent) = path.parent() {
52        std::fs::create_dir_all(parent)?;
53    }
54    Ok(())
55}
56
57pub fn temp_path_for_key(temp_dir: &Path, key: &str) -> PathBuf {
58    let mut hasher = DefaultHasher::new();
59    key.hash(&mut hasher);
60    let hash = hasher.finish();
61    let name = Path::new(key)
62        .file_name()
63        .and_then(|name| name.to_str())
64        .unwrap_or("object");
65    let sanitized = sanitize_filename(name);
66    temp_dir.join(format!("{hash:016x}_{sanitized}"))
67}
68
69fn sanitize_filename(name: &str) -> String {
70    name.chars()
71        .map(|ch| {
72            if ch.is_ascii_alphanumeric() || matches!(ch, '.' | '-' | '_') {
73                ch
74            } else {
75                '_'
76            }
77        })
78        .collect()
79}