1use crate::Result;
4use std::path::{Path, PathBuf};
5
6pub struct FileUtils;
8
9impl FileUtils {
10 pub fn calculate_hash(path: &Path) -> Result<String> {
16 let _path = path;
18 Ok("placeholder_hash".to_string())
19 }
20
21 #[must_use]
23 pub fn format_file_size(bytes: u64) -> String {
24 const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB", "PB"];
25 let mut size = bytes as f64;
26 let mut unit_index = 0;
27
28 while size >= 1024.0 && unit_index < UNITS.len() - 1 {
29 size /= 1024.0;
30 unit_index += 1;
31 }
32
33 format!("{:.2} {}", size, UNITS[unit_index])
34 }
35
36 pub fn get_modification_time(path: &Path) -> Result<i64> {
42 let metadata = std::fs::metadata(path)?;
43 let modified = metadata.modified()?;
44 let duration = modified
45 .duration_since(std::time::UNIX_EPOCH)
46 .map_err(|e| {
47 crate::ProxyError::IoError(std::io::Error::new(std::io::ErrorKind::Other, e))
48 })?;
49
50 Ok(duration.as_secs() as i64)
51 }
52
53 pub fn copy_with_progress<F>(
55 source: &Path,
56 destination: &Path,
57 mut progress_callback: F,
58 ) -> Result<u64>
59 where
60 F: FnMut(u64, u64),
61 {
62 let total_size = std::fs::metadata(source)?.len();
63 let mut copied = 0u64;
64
65 progress_callback(copied, total_size);
67
68 std::fs::copy(source, destination)?;
69 copied = total_size;
70 progress_callback(copied, total_size);
71
72 Ok(copied)
73 }
74
75 pub fn ensure_directory(path: &Path) -> Result<()> {
77 if !path.exists() {
78 std::fs::create_dir_all(path)?;
79 }
80 Ok(())
81 }
82
83 pub fn get_available_space(_path: &Path) -> Result<u64> {
89 Ok(0)
91 }
92
93 #[must_use]
95 pub fn is_video_file(path: &Path) -> bool {
96 const VIDEO_EXTENSIONS: &[&str] = &[
97 "mp4", "mov", "avi", "mkv", "wmv", "flv", "webm", "m4v", "mpg", "mpeg", "3gp", "3g2",
98 "mxf", "ts", "m2ts", "mts",
99 ];
100
101 if let Some(ext) = path.extension() {
102 if let Some(ext_str) = ext.to_str() {
103 return VIDEO_EXTENSIONS.contains(&ext_str.to_lowercase().as_str());
104 }
105 }
106
107 false
108 }
109
110 #[must_use]
112 pub fn guess_codec_from_extension(path: &Path) -> Option<String> {
113 if let Some(ext) = path.extension() {
114 match ext.to_str()?.to_lowercase().as_str() {
115 "mp4" | "m4v" => Some("h264".to_string()),
116 "webm" => Some("vp9".to_string()),
117 "mkv" => Some("h264".to_string()),
118 "mov" => Some("prores".to_string()),
119 "avi" => Some("h264".to_string()),
120 _ => None,
121 }
122 } else {
123 None
124 }
125 }
126}
127
128pub struct PathUtils;
130
131impl PathUtils {
132 #[must_use]
134 pub fn normalize_path(path: &Path) -> PathBuf {
135 if let Ok(canonical) = path.canonicalize() {
137 canonical
138 } else {
139 path.to_path_buf()
140 }
141 }
142
143 pub fn get_relative_path(base: &Path, target: &Path) -> Result<PathBuf> {
149 target
150 .strip_prefix(base)
151 .map(|p| p.to_path_buf())
152 .map_err(|e| crate::ProxyError::InvalidInput(e.to_string()))
153 }
154
155 #[must_use]
157 pub fn make_absolute(path: &Path) -> PathBuf {
158 if path.is_absolute() {
159 path.to_path_buf()
160 } else if let Ok(current_dir) = std::env::current_dir() {
161 current_dir.join(path)
162 } else {
163 path.to_path_buf()
164 }
165 }
166
167 #[must_use]
169 pub fn get_stem(path: &Path) -> String {
170 path.file_stem()
171 .and_then(|s| s.to_str())
172 .unwrap_or("")
173 .to_string()
174 }
175
176 #[must_use]
178 pub fn change_extension(path: &Path, new_ext: &str) -> PathBuf {
179 let mut new_path = path.to_path_buf();
180 new_path.set_extension(new_ext);
181 new_path
182 }
183
184 pub fn make_unique_filename(path: &Path) -> PathBuf {
186 if !path.exists() {
187 return path.to_path_buf();
188 }
189
190 let stem = Self::get_stem(path);
191 let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
192 let parent = path.parent().unwrap_or_else(|| Path::new("."));
193
194 for i in 1..1000 {
195 let new_filename = if ext.is_empty() {
196 format!("{}_{}", stem, i)
197 } else {
198 format!("{}_{}.{}", stem, i, ext)
199 };
200
201 let new_path = parent.join(new_filename);
202 if !new_path.exists() {
203 return new_path;
204 }
205 }
206
207 path.to_path_buf()
208 }
209
210 #[must_use]
212 pub fn are_same_file(path1: &Path, path2: &Path) -> bool {
213 if let (Ok(canon1), Ok(canon2)) = (path1.canonicalize(), path2.canonicalize()) {
214 canon1 == canon2
215 } else {
216 path1 == path2
217 }
218 }
219}
220
221pub struct TimeUtils;
223
224impl TimeUtils {
225 #[must_use]
227 pub fn format_duration(seconds: f64) -> String {
228 let hours = (seconds / 3600.0).floor() as u64;
229 let minutes = ((seconds % 3600.0) / 60.0).floor() as u64;
230 let secs = (seconds % 60.0).floor() as u64;
231 let ms = ((seconds % 1.0) * 1000.0).floor() as u64;
232
233 if hours > 0 {
234 format!("{}:{:02}:{:02}.{:03}", hours, minutes, secs, ms)
235 } else if minutes > 0 {
236 format!("{}:{:02}.{:03}", minutes, secs, ms)
237 } else {
238 format!("{}.{:03}s", secs, ms)
239 }
240 }
241
242 pub fn parse_duration(duration_str: &str) -> Result<f64> {
248 let parts: Vec<&str> = duration_str.split(':').collect();
250
251 match parts.len() {
252 3 => {
253 let hours: f64 = parts[0].parse().map_err(|e| {
255 crate::ProxyError::InvalidInput(format!("Invalid hours: {}", e))
256 })?;
257 let minutes: f64 = parts[1].parse().map_err(|e| {
258 crate::ProxyError::InvalidInput(format!("Invalid minutes: {}", e))
259 })?;
260 let seconds: f64 = parts[2].parse().map_err(|e| {
261 crate::ProxyError::InvalidInput(format!("Invalid seconds: {}", e))
262 })?;
263
264 Ok(hours * 3600.0 + minutes * 60.0 + seconds)
265 }
266 2 => {
267 let minutes: f64 = parts[0].parse().map_err(|e| {
269 crate::ProxyError::InvalidInput(format!("Invalid minutes: {}", e))
270 })?;
271 let seconds: f64 = parts[1].parse().map_err(|e| {
272 crate::ProxyError::InvalidInput(format!("Invalid seconds: {}", e))
273 })?;
274
275 Ok(minutes * 60.0 + seconds)
276 }
277 1 => {
278 parts[0].parse().map_err(|e| {
280 crate::ProxyError::InvalidInput(format!("Invalid duration: {}", e))
281 })
282 }
283 _ => Err(crate::ProxyError::InvalidInput(
284 "Invalid duration format".to_string(),
285 )),
286 }
287 }
288
289 #[must_use]
291 pub fn current_timestamp() -> i64 {
292 std::time::SystemTime::now()
293 .duration_since(std::time::UNIX_EPOCH)
294 .expect("infallible: system clock is always after UNIX_EPOCH")
295 .as_secs() as i64
296 }
297
298 #[must_use]
300 pub fn format_timestamp(_timestamp: i64) -> String {
301 "2024-01-01T00:00:00Z".to_string()
303 }
304}
305
306pub struct StringUtils;
308
309impl StringUtils {
310 #[must_use]
312 pub fn sanitize_filename(filename: &str) -> String {
313 const INVALID_CHARS: &[char] = &['/', '\\', ':', '*', '?', '"', '<', '>', '|'];
314
315 filename
316 .chars()
317 .map(|c| if INVALID_CHARS.contains(&c) { '_' } else { c })
318 .collect()
319 }
320
321 #[must_use]
323 pub fn generate_id() -> String {
324 use std::time::SystemTime;
325
326 let now = SystemTime::now()
327 .duration_since(std::time::UNIX_EPOCH)
328 .expect("infallible: system clock is always after UNIX_EPOCH");
329
330 format!("{:x}", now.as_nanos())
331 }
332
333 #[must_use]
335 pub fn truncate(s: &str, max_len: usize) -> String {
336 if s.len() <= max_len {
337 s.to_string()
338 } else {
339 format!("{}...", &s[..max_len.saturating_sub(3)])
340 }
341 }
342
343 #[must_use]
345 pub fn bytes_to_hex(bytes: &[u8]) -> String {
346 bytes.iter().map(|b| format!("{:02x}", b)).collect()
347 }
348
349 pub fn hex_to_bytes(hex: &str) -> Result<Vec<u8>> {
351 let mut bytes = Vec::new();
352 let mut chars = hex.chars();
353
354 while let (Some(high), Some(low)) = (chars.next(), chars.next()) {
355 let byte = u8::from_str_radix(&format!("{}{}", high, low), 16)
356 .map_err(|e| crate::ProxyError::InvalidInput(e.to_string()))?;
357 bytes.push(byte);
358 }
359
360 Ok(bytes)
361 }
362}
363
364#[cfg(test)]
365mod tests {
366 use super::*;
367
368 #[test]
369 fn test_format_file_size() {
370 assert_eq!(FileUtils::format_file_size(500), "500.00 B");
371 assert_eq!(FileUtils::format_file_size(1024), "1.00 KB");
372 assert_eq!(FileUtils::format_file_size(1_048_576), "1.00 MB");
373 assert_eq!(FileUtils::format_file_size(1_073_741_824), "1.00 GB");
374 }
375
376 #[test]
377 fn test_is_video_file() {
378 assert!(FileUtils::is_video_file(Path::new("test.mp4")));
379 assert!(FileUtils::is_video_file(Path::new("test.mov")));
380 assert!(FileUtils::is_video_file(Path::new("test.mkv")));
381 assert!(!FileUtils::is_video_file(Path::new("test.txt")));
382 }
383
384 #[test]
385 fn test_guess_codec() {
386 assert_eq!(
387 FileUtils::guess_codec_from_extension(Path::new("test.mp4")),
388 Some("h264".to_string())
389 );
390 assert_eq!(
391 FileUtils::guess_codec_from_extension(Path::new("test.webm")),
392 Some("vp9".to_string())
393 );
394 }
395
396 #[test]
397 fn test_get_stem() {
398 assert_eq!(PathUtils::get_stem(Path::new("test.mp4")), "test");
399 assert_eq!(PathUtils::get_stem(Path::new("/path/to/file.ext")), "file");
400 }
401
402 #[test]
403 fn test_change_extension() {
404 let path = Path::new("test.mp4");
405 let new_path = PathUtils::change_extension(path, "mov");
406 assert_eq!(new_path, PathBuf::from("test.mov"));
407 }
408
409 #[test]
410 fn test_format_duration() {
411 assert_eq!(TimeUtils::format_duration(30.5), "30.500s");
412 assert_eq!(TimeUtils::format_duration(90.0), "1:30.000");
413 assert_eq!(TimeUtils::format_duration(3665.0), "1:01:05.000");
414 }
415
416 #[test]
417 fn test_parse_duration() {
418 assert_eq!(
419 TimeUtils::parse_duration("30").expect("should succeed in test"),
420 30.0
421 );
422 assert_eq!(
423 TimeUtils::parse_duration("1:30").expect("should succeed in test"),
424 90.0
425 );
426 assert_eq!(
427 TimeUtils::parse_duration("1:01:05").expect("should succeed in test"),
428 3665.0
429 );
430 }
431
432 #[test]
433 fn test_sanitize_filename() {
434 assert_eq!(
435 StringUtils::sanitize_filename("test/file:name.mp4"),
436 "test_file_name.mp4"
437 );
438 }
439
440 #[test]
441 fn test_truncate() {
442 assert_eq!(StringUtils::truncate("hello", 10), "hello");
443 assert_eq!(StringUtils::truncate("hello world", 8), "hello...");
444 }
445
446 #[test]
447 fn test_bytes_to_hex() {
448 assert_eq!(StringUtils::bytes_to_hex(&[0, 1, 255]), "0001ff");
449 }
450
451 #[test]
452 fn test_hex_to_bytes() {
453 let bytes = StringUtils::hex_to_bytes("0001ff").expect("should succeed in test");
454 assert_eq!(bytes, vec![0, 1, 255]);
455 }
456
457 #[test]
458 fn test_current_timestamp() {
459 let ts = TimeUtils::current_timestamp();
460 assert!(ts > 0);
461 }
462}