floe_core/io/storage/
planner.rs1use 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}