rust_webx_core/form/
spool.rs1use std::path::PathBuf;
4use std::sync::RwLock;
5
6pub const SPOOL_FILE_PREFIX: &str = "webx-upload-";
8
9const MAX_FILE_NAME_LEN: usize = 200;
11
12static SPOOL_ROOT: RwLock<Option<PathBuf>> = RwLock::new(None);
13
14pub fn set_spool_root(path: impl Into<PathBuf>) {
19 let mut guard = SPOOL_ROOT
20 .write()
21 .unwrap_or_else(|poisoned| poisoned.into_inner());
22 *guard = Some(path.into());
23}
24
25pub fn spool_root() -> PathBuf {
27 let guard = SPOOL_ROOT
28 .read()
29 .unwrap_or_else(|poisoned| poisoned.into_inner());
30 if let Some(root) = guard.as_ref() {
31 return root.clone();
32 }
33 std::env::temp_dir().join("webx-uploads")
34}
35
36pub fn sanitize_file_name(raw: &str) -> String {
44 let base = raw.rsplit(['/', '\\']).next().unwrap_or("");
46
47 let mut cleaned = String::with_capacity(base.len());
48 for ch in base.chars() {
49 if ch.is_control() {
50 continue;
51 }
52 match ch {
53 '<' | '>' | ':' | '"' | '|' | '?' | '*' => cleaned.push('_'),
54 _ => cleaned.push(ch),
55 }
56 }
57
58 let trimmed = cleaned.trim().trim_end_matches(['.', ' ']);
60 let candidate = if trimmed.is_empty() { "file" } else { trimmed };
61
62 truncate_preserving_extension(candidate, MAX_FILE_NAME_LEN)
63}
64
65fn truncate_preserving_extension(name: &str, max: usize) -> String {
66 if name.len() <= max {
67 return name.to_string();
68 }
69 match name.rfind('.') {
71 Some(idx) if idx > 0 && name.len() - idx <= 16 => {
72 let ext = &name[idx..];
73 let stem = truncate_chars(&name[..idx], max.saturating_sub(ext.len()));
74 format!("{stem}{ext}")
75 }
76 _ => truncate_chars(name, max),
77 }
78}
79
80fn truncate_chars(s: &str, max: usize) -> String {
81 if s.len() <= max {
82 return s.to_string();
83 }
84 let mut end = max;
85 while end > 0 && !s.is_char_boundary(end) {
86 end -= 1;
87 }
88 s[..end].to_string()
89}