1use crate::error::{Aria2Error, FatalError, Result};
2use std::fmt;
3use std::path::Path;
4
5const DEFAULT_MARGIN_MB: u64 = 100;
6
7#[cfg(unix)]
17fn path_to_cstring(path: &Path) -> Option<std::ffi::CString> {
18 use std::os::unix::ffi::OsStrExt;
19 std::ffi::CString::new(path.as_os_str().as_bytes()).ok()
20}
21
22#[derive(Debug, Clone)]
47pub enum DiskError {
48 InsufficientSpace {
50 required: u64,
52 available: Option<u64>,
54 },
55 IoError(String),
57 PermissionDenied(String),
59}
60
61impl fmt::Display for DiskError {
62 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63 match self {
64 DiskError::InsufficientSpace {
65 required,
66 available,
67 } => {
68 write!(
69 f,
70 "Not enough disk space: need {}, have {}",
71 format_bytes(*required),
72 available.map_or_else(|| "unknown".into(), format_bytes)
73 )
74 }
75 DiskError::IoError(msg) => write!(f, "Disk I/O error: {}", msg),
76 DiskError::PermissionDenied(path) => write!(f, "Permission denied: {}", path),
77 }
78 }
79}
80
81impl std::error::Error for DiskError {}
82
83fn format_bytes(bytes: u64) -> String {
96 if bytes >= 1024 * 1024 * 1024 {
97 format!("{:.2} GiB", bytes as f64 / (1024.0 * 1024.0 * 1024.0))
98 } else if bytes >= 1024 * 1024 {
99 format!("{:.2} MiB", bytes as f64 / (1024.0 * 1024.0))
100 } else if bytes >= 1024 {
101 format!("{:.2} KiB", bytes as f64 / 1024.0)
102 } else {
103 format!("{} B", bytes)
104 }
105}
106
107pub fn check_disk_space(path: &Path, required_bytes: u64) -> std::result::Result<(), String> {
149 #[cfg(unix)]
150 {
151 let check_path = if path.as_os_str().is_empty() {
153 Path::new(".")
154 } else {
155 path
156 };
157
158 let Some(c_path) = path_to_cstring(check_path) else {
161 tracing::warn!(
162 path = %check_path.display(),
163 required = required_bytes,
164 "path contains embedded null byte, skipping disk space check"
165 );
166 return Ok(());
167 };
168
169 let mut stat: libc::statvfs = unsafe { std::mem::zeroed() };
170 let ret = unsafe { libc::statvfs(c_path.as_ptr(), &mut stat) };
171
172 if ret != 0 {
173 tracing::warn!(
176 path = %check_path.display(),
177 required = required_bytes,
178 "statvfs failed, skipping disk space check: {}",
179 std::io::Error::last_os_error()
180 );
181 return Ok(());
182 }
183
184 let available = stat.f_bavail as u64 * stat.f_frsize as u64;
186
187 let needed_with_headroom = required_bytes.saturating_add(required_bytes / 10);
189
190 if available < needed_with_headroom {
191 let available_gb = available as f64 / (1024.0 * 1024.0 * 1024.0);
192 let required_gb = required_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
193 return Err(format!(
194 "Insufficient disk space: need {:.2} GiB but only {:.2} GiB available on '{}'",
195 required_gb,
196 available_gb,
197 check_path.display()
198 ));
199 }
200
201 Ok(())
202 }
203
204 #[cfg(windows)]
205 {
206 use std::os::windows::ffi::OsStrExt;
207
208 let check_path = if path.as_os_str().is_empty() {
210 Path::new(".")
211 } else {
212 path
213 };
214
215 let abs_path = match std::fs::canonicalize(check_path) {
217 Ok(p) => p,
218 Err(_) => {
219 std::env::current_dir().unwrap_or_else(|_| Path::new(".").to_path_buf())
221 }
222 };
223
224 let mut wide_path: Vec<u16> = abs_path.as_os_str().encode_wide().collect();
226 wide_path.push(0);
227
228 let mut free_bytes_available: u64 = 0;
229 let mut total_number_of_bytes: u64 = 0;
230 let mut total_number_of_free_bytes: u64 = 0;
231
232 let result = unsafe {
233 windows_sys::Win32::Storage::FileSystem::GetDiskFreeSpaceExW(
234 wide_path.as_ptr(),
235 &mut free_bytes_available as *mut u64 as *mut _,
236 &mut total_number_of_bytes as *mut u64 as *mut _,
237 &mut total_number_of_free_bytes as *mut u64 as *mut _,
238 )
239 };
240
241 if result == 0 {
242 tracing::warn!(
244 path = %check_path.display(),
245 required = required_bytes,
246 "GetDiskFreeSpaceEx failed, skipping disk space check"
247 );
248 return Ok(());
249 }
250
251 let needed_with_headroom = required_bytes.saturating_add(required_bytes / 10);
253
254 if free_bytes_available < needed_with_headroom {
255 let available_gb = free_bytes_available as f64 / (1024.0 * 1024.0 * 1024.0);
256 let required_gb = required_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
257 return Err(format!(
258 "Insufficient disk space: need {:.2} GiB but only {:.2} GiB available on '{}'",
259 required_gb,
260 available_gb,
261 check_path.display()
262 ));
263 }
264
265 Ok(())
266 }
267
268 #[cfg(not(any(unix, windows)))]
269 {
270 tracing::warn!(
272 path = %path.display(),
273 required = required_bytes,
274 "Disk space check skipped on unsupported platform"
275 );
276 Ok(())
277 }
278}
279
280pub fn check_disk_space_typed(
295 path: &Path,
296 required_bytes: u64,
297) -> std::result::Result<(), DiskError> {
298 #[cfg(unix)]
299 {
300 let check_path = if path.as_os_str().is_empty() {
301 Path::new(".")
302 } else {
303 path
304 };
305
306 let Some(c_path) = path_to_cstring(check_path) else {
309 tracing::warn!(
310 path = %check_path.display(),
311 required = required_bytes,
312 "path contains embedded null byte, skipping disk space check"
313 );
314 return Ok(());
315 };
316
317 let mut stat: libc::statvfs = unsafe { std::mem::zeroed() };
318 let ret = unsafe { libc::statvfs(c_path.as_ptr(), &mut stat) };
319
320 if ret != 0 {
321 tracing::warn!(
324 path = %check_path.display(),
325 required = required_bytes,
326 "statvfs failed, skipping disk space check: {}",
327 std::io::Error::last_os_error()
328 );
329 return Ok(());
330 }
331
332 let available = stat.f_bavail as u64 * stat.f_frsize as u64;
333 let needed_with_headroom = required_bytes.saturating_add(required_bytes / 10);
334
335 if available < needed_with_headroom {
336 return Err(DiskError::InsufficientSpace {
337 required: needed_with_headroom,
338 available: Some(available),
339 });
340 }
341
342 Ok(())
343 }
344
345 #[cfg(windows)]
346 {
347 use std::os::windows::ffi::OsStrExt;
348
349 let check_path = if path.as_os_str().is_empty() {
350 Path::new(".")
351 } else {
352 path
353 };
354
355 let abs_path = match std::fs::canonicalize(check_path) {
357 Ok(p) => p,
358 Err(_) => std::env::current_dir().unwrap_or_else(|_| Path::new(".").to_path_buf()),
359 };
360
361 let mut wide_path: Vec<u16> = abs_path.as_os_str().encode_wide().collect();
363 wide_path.push(0);
364
365 let mut free_bytes_available: u64 = 0;
366 let mut total_number_of_bytes: u64 = 0;
367 let mut total_number_of_free_bytes: u64 = 0;
368
369 let result = unsafe {
370 windows_sys::Win32::Storage::FileSystem::GetDiskFreeSpaceExW(
371 wide_path.as_ptr(),
372 &mut free_bytes_available as *mut u64 as *mut _,
373 &mut total_number_of_bytes as *mut u64 as *mut _,
374 &mut total_number_of_free_bytes as *mut u64 as *mut _,
375 )
376 };
377
378 if result == 0 {
379 return Ok(());
381 }
382
383 let needed_with_headroom = required_bytes.saturating_add(required_bytes / 10);
384
385 if free_bytes_available < needed_with_headroom {
386 return Err(DiskError::InsufficientSpace {
387 required: needed_with_headroom,
388 available: Some(free_bytes_available),
389 });
390 }
391
392 Ok(())
393 }
394
395 #[cfg(not(any(unix, windows)))]
396 {
397 let _ = path;
398 let _ = required_bytes;
399 Ok(())
400 }
401}
402
403pub fn available_space(path: &Path) -> Result<u64> {
404 let path = if path.as_os_str().is_empty() {
405 Path::new(".")
406 } else {
407 path
408 };
409
410 #[cfg(unix)]
411 {
412 let c_path = path_to_cstring(path).ok_or_else(|| {
415 Aria2Error::Fatal(FatalError::Config(format!(
416 "path contains embedded null byte: {}",
417 path.display()
418 )))
419 })?;
420 let mut stat: libc::statvfs = unsafe { std::mem::zeroed() };
421 let ret = unsafe { libc::statvfs(c_path.as_ptr(), &mut stat) };
422 if ret != 0 {
423 return Err(Aria2Error::Fatal(FatalError::Config(format!(
424 "statvfs failed: {}",
425 std::io::Error::last_os_error()
426 ))));
427 }
428 Ok(stat.f_bavail as u64 * stat.f_frsize as u64)
429 }
430
431 #[cfg(windows)]
432 {
433 use std::os::windows::ffi::OsStrExt;
434
435 let abs_path = match std::fs::canonicalize(path) {
437 Ok(p) => p,
438 Err(_) => std::env::current_dir().unwrap_or_else(|_| Path::new(".").to_path_buf()),
439 };
440
441 let mut wide_path: Vec<u16> = abs_path.as_os_str().encode_wide().collect();
443 wide_path.push(0);
444
445 let mut free_bytes_available: u64 = 0;
446 let mut total_number_of_bytes: u64 = 0;
447 let mut total_number_of_free_bytes: u64 = 0;
448
449 let result = unsafe {
450 windows_sys::Win32::Storage::FileSystem::GetDiskFreeSpaceExW(
451 wide_path.as_ptr(),
452 &mut free_bytes_available as *mut u64 as *mut _,
453 &mut total_number_of_bytes as *mut u64 as *mut _,
454 &mut total_number_of_free_bytes as *mut u64 as *mut _,
455 )
456 };
457
458 if result == 0 {
459 return Err(Aria2Error::Fatal(FatalError::Config(
460 "GetDiskFreeSpaceExW failed".to_string(),
461 )));
462 }
463
464 Ok(free_bytes_available)
465 }
466
467 #[cfg(not(any(unix, windows)))]
468 {
469 let _ = path;
470 Ok(u64::MAX)
471 }
472}
473
474pub fn has_enough_space(path: &Path, required: u64) -> bool {
475 available_space(path).is_ok_and(|avail| avail >= required)
476}
477
478pub fn check_with_margin(path: &Path, required: u64, margin_mb: Option<u64>) -> Result<()> {
479 let margin = margin_mb.unwrap_or(DEFAULT_MARGIN_MB) * 1024 * 1024;
480 let total_needed = required.saturating_add(margin);
481 let avail = match available_space(path) {
486 Ok(v) => v,
487 Err(err) => {
488 tracing::warn!(
489 path = %path.display(),
490 required = total_needed,
491 "available_space failed, skipping disk space check: {}",
492 err
493 );
494 return Ok(());
495 }
496 };
497 if avail < total_needed {
498 Err(Aria2Error::Fatal(FatalError::DiskSpaceExhausted))
499 } else {
500 Ok(())
501 }
502}
503
504pub fn total_space(path: &Path) -> Result<u64> {
505 let path = if path.as_os_str().is_empty() {
506 Path::new(".")
507 } else {
508 path
509 };
510
511 #[cfg(unix)]
512 {
513 let c_path = path_to_cstring(path).ok_or_else(|| {
516 Aria2Error::Fatal(FatalError::Config(format!(
517 "path contains embedded null byte: {}",
518 path.display()
519 )))
520 })?;
521 let mut stat: libc::statvfs = unsafe { std::mem::zeroed() };
522 let ret = unsafe { libc::statvfs(c_path.as_ptr(), &mut stat) };
523 if ret != 0 {
524 return Err(Aria2Error::Fatal(FatalError::Config(format!(
525 "statvfs failed: {}",
526 std::io::Error::last_os_error()
527 ))));
528 }
529 Ok(stat.f_blocks as u64 * stat.f_frsize as u64)
530 }
531
532 #[cfg(windows)]
533 {
534 use std::os::windows::ffi::OsStrExt;
535
536 let abs_path = match std::fs::canonicalize(path) {
538 Ok(p) => p,
539 Err(_) => std::env::current_dir().unwrap_or_else(|_| Path::new(".").to_path_buf()),
540 };
541
542 let mut wide_path: Vec<u16> = abs_path.as_os_str().encode_wide().collect();
544 wide_path.push(0);
545
546 let mut free_bytes_available: u64 = 0;
547 let mut total_number_of_bytes: u64 = 0;
548 let mut total_number_of_free_bytes: u64 = 0;
549
550 let result = unsafe {
551 windows_sys::Win32::Storage::FileSystem::GetDiskFreeSpaceExW(
552 wide_path.as_ptr(),
553 &mut free_bytes_available as *mut u64 as *mut _,
554 &mut total_number_of_bytes as *mut u64 as *mut _,
555 &mut total_number_of_free_bytes as *mut u64 as *mut _,
556 )
557 };
558
559 if result == 0 {
560 return Err(Aria2Error::Fatal(FatalError::Config(
561 "GetDiskFreeSpaceExW failed".to_string(),
562 )));
563 }
564
565 Ok(total_number_of_bytes)
566 }
567
568 #[cfg(not(any(unix, windows)))]
569 {
570 let _ = path;
571 Ok(u64::MAX)
572 }
573}
574
575#[cfg(test)]
580mod tests {
581 use super::*;
582
583 #[test]
588 fn test_sufficient_space_ok() {
589 let required = 100 * 1024 * 1024; let result = check_disk_space(Path::new("."), required);
592
593 assert!(
596 result.is_ok() || result.is_err(),
597 "Should return Ok or Err without panicking"
598 );
599 }
600
601 #[test]
606 fn test_insufficient_space_err() {
607 let required = u64::MAX / 2; let result = check_disk_space(Path::new("."), required);
610
611 if let Err(error_msg) = result {
614 assert!(
615 error_msg.to_lowercase().contains("insufficient")
616 || error_msg.to_lowercase().contains("space"),
617 "Error message should mention space: {}",
618 error_msg
619 );
620 }
621 }
623
624 #[test]
629 fn test_disk_error_display() {
630 let err = DiskError::InsufficientSpace {
632 required: 1024 * 1024 * 1024, available: Some(512 * 1024 * 1024), };
635 let display_str = format!("{}", err);
636 assert!(
637 display_str.contains("Not enough disk space"),
638 "Should mention insufficient space"
639 );
640 assert!(
641 display_str.contains("1.00 GiB") || display_str.contains("GiB"),
642 "Should show required size in GiB"
643 );
644 assert!(
645 display_str.contains("512.00 MiB") || display_str.contains("MiB"),
646 "Should show available size in MiB"
647 );
648
649 let io_err = DiskError::IoError("Failed to write block".to_string());
651 let io_display = format!("{}", io_err);
652 assert!(
653 io_display.contains("Disk I/O error"),
654 "Should mention I/O error"
655 );
656 assert!(
657 io_display.contains("Failed to write block"),
658 "Should include original message"
659 );
660
661 let perm_err = DiskError::PermissionDenied("/root/secret".to_string());
663 let perm_display = format!("{}", perm_err);
664 assert!(
665 perm_display.contains("Permission denied"),
666 "Should mention permission denied"
667 );
668 assert!(perm_display.contains("/root/secret"), "Should include path");
669
670 assert_eq!(format_bytes(0), "0 B", "Zero bytes");
672 assert_eq!(format_bytes(500), "500 B", "Small bytes");
673 assert_eq!(format_bytes(1024), "1.00 KiB", "Exactly 1 KiB");
674 assert_eq!(format_bytes(1024 * 1024), "1.00 MiB", "Exactly 1 MiB");
675 assert_eq!(
676 format_bytes(1024 * 1024 * 1024),
677 "1.00 GiB",
678 "Exactly 1 GiB"
679 );
680 assert_eq!(
681 format_bytes(1536 * 1024 * 1024),
682 "1.50 GiB",
683 "1.5 GiB with decimal"
684 );
685 }
686
687 #[test]
689 fn test_check_disk_space_typed_returns_structured_errors() {
690 let huge_request = u64::MAX / 2;
691
692 match check_disk_space_typed(Path::new("."), huge_request) {
693 Ok(()) => {
694 }
696 Err(DiskError::InsufficientSpace {
697 required,
698 available,
699 }) => {
700 assert!(required > 0, "Required should be positive");
702 if let Some(avail) = available {
704 let _ = avail;
706 }
707 }
708 Err(DiskError::IoError(msg)) => {
709 assert!(!msg.is_empty(), "Error message should not be empty");
711 }
712 Err(DiskError::PermissionDenied(_)) => {
713 }
715 }
716 }
717
718 #[test]
720 fn test_check_disk_space_empty_path() {
721 let result = check_disk_space(Path::new(""), 1024);
722 assert!(
724 result.is_ok() || result.is_err(),
725 "Empty path should be handled gracefully"
726 );
727 }
728
729 #[test]
731 fn test_check_disk_space_zero_bytes() {
732 let result = check_disk_space(Path::new("."), 0);
733 assert!(
734 result.is_ok(),
735 "Requesting zero bytes should always succeed"
736 );
737 }
738}