1use super::disk_adaptor::{DirectDiskAdaptor, DiskAdaptor};
2use crate::error::{Aria2Error, FatalError, Result};
3use crate::filesystem::disk_space::check_disk_space;
4use std::path::Path;
5
6#[cfg_attr(target_os = "linux", allow(dead_code))]
13static SECURE_FALLOC_WARN_ONCE: std::sync::Once = std::sync::Once::new();
14
15#[derive(Debug, Clone, Copy, PartialEq, Default)]
16pub enum AllocationStrategy {
17 #[default]
18 None,
19 Prealloc,
20 Falloc,
21 Trunc,
22 Mmap,
27}
28
29impl AllocationStrategy {
30 #[allow(clippy::should_implement_trait)]
33 pub fn from_str(s: &str) -> Self {
34 match s {
35 "prealloc" => AllocationStrategy::Prealloc,
36 "falloc" => AllocationStrategy::Falloc,
37 "trunc" => AllocationStrategy::Trunc,
38 "mmap" => AllocationStrategy::Mmap,
39 _ => AllocationStrategy::None,
40 }
41 }
42}
43
44pub async fn allocate_file<D: DiskAdaptor>(
59 adaptor: &mut D,
60 _path: &Path,
61 length: u64,
62 strategy: AllocationStrategy,
63 secure: bool,
64) -> Result<()> {
65 match strategy {
66 AllocationStrategy::None => Ok(()),
67 AllocationStrategy::Prealloc => preallocate(adaptor, length).await,
68 AllocationStrategy::Falloc => fallocate(adaptor, length, secure).await,
69 AllocationStrategy::Trunc => truncate(adaptor, length).await,
70 AllocationStrategy::Mmap => fallocate(adaptor, length, secure).await,
73 }
74}
75
76pub async fn preallocate_file(
77 path: &Path,
78 length: u64,
79 strategy: &str,
80 secure: bool,
81) -> Result<()> {
82 preallocate_file_with_progress(path, length, strategy, None::<&fn(u64, u64)>, secure).await
83}
84
85pub async fn preallocate_file_with_progress<F>(
90 path: &Path,
91 length: u64,
92 strategy: &str,
93 on_progress: Option<&F>,
94 secure: bool,
95) -> Result<()>
96where
97 F: Fn(u64, u64) + Send + Sync,
98{
99 let alloc_strategy = AllocationStrategy::from_str(strategy);
100
101 if length == 0 || alloc_strategy == AllocationStrategy::None {
102 return Ok(());
103 }
104
105 if let Err(_e) = check_disk_space(path, length) {
110 return Err(Aria2Error::Fatal(FatalError::DiskSpaceExhausted));
111 }
112
113 if let Some(parent) = path.parent() {
114 let parent: &Path = parent;
115 if !parent.exists() {
116 tokio::fs::create_dir_all(parent)
117 .await
118 .map_err(|e: std::io::Error| Aria2Error::Io(e.to_string()))?;
119 }
120 }
121
122 const PROGRESS_THRESHOLD: u64 = 100 * 1024 * 1024; if let Some(cb) = on_progress
125 && length >= PROGRESS_THRESHOLD
126 {
127 cb(0, length);
128 }
129
130 let mut adaptor = DirectDiskAdaptor::new();
131 adaptor.open(path).await?;
132 allocate_file(&mut adaptor, path, length, alloc_strategy, secure).await?;
133
134 if let Some(cb) = on_progress
135 && length >= PROGRESS_THRESHOLD
136 {
137 cb(length, length);
138 }
139
140 adaptor.close().await
141}
142
143async fn preallocate<D: DiskAdaptor>(adaptor: &mut D, length: u64) -> Result<()> {
152 adaptor.truncate(length).await
153}
154
155async fn async_zero_fill<D: DiskAdaptor>(adaptor: &mut D, length: u64) -> Result<()> {
164 const CHUNK_SIZE: usize = 1024 * 1024; let zero_chunk = vec![0u8; CHUNK_SIZE];
166 let mut remaining = length;
167 let mut offset: u64 = 0;
168
169 while remaining > 0 {
170 let write_len = remaining.min(CHUNK_SIZE as u64) as usize;
171 adaptor.write(offset, &zero_chunk[..write_len]).await?;
172 offset += write_len as u64;
173 remaining -= write_len as u64;
174
175 tokio::task::yield_now().await;
177 }
178
179 Ok(())
180}
181
182#[cfg_attr(target_os = "linux", allow(unused_variables))]
211async fn fallocate<D: DiskAdaptor>(adaptor: &mut D, length: u64, secure: bool) -> Result<()> {
212 #[cfg(target_os = "linux")]
213 {
214 if let Some(fd) = adaptor.unix_raw_fd() {
215 let ret = unsafe {
224 libc::fallocate(
225 fd,
226 0 as libc::c_int,
230 0,
231 length as libc::off_t,
232 )
233 };
234 if ret == 0 {
235 return Ok(());
237 }
238 let errno = unsafe { *libc::__errno_location() };
239 if errno == libc::EOPNOTSUPP {
240 tracing::warn!(
241 length,
242 "fallocate(2) not supported by filesystem; \
243 falling back to async zero-fill"
244 );
245 adaptor.truncate(length).await?;
247 return async_zero_fill(adaptor, length).await;
248 }
249 Err(Aria2Error::Io(
251 std::io::Error::from_raw_os_error(errno).to_string(),
252 ))
253 } else {
254 adaptor.truncate(length).await
256 }
257 }
258
259 #[cfg(all(unix, not(target_os = "linux")))]
260 {
261 #[cfg(target_os = "macos")]
265 {
266 match adaptor.unix_raw_fd() {
267 Some(fd) => {
268 adaptor.truncate(length).await?;
272 let fstore = libc::fstore_t {
275 fst_flags: libc::F_ALLOCATEALL as libc::c_uint,
276 fst_posmode: libc::F_PEOFPOSMODE,
277 fst_offset: 0,
278 fst_length: length as libc::off_t,
279 fst_bytesalloc: 0,
280 };
281 let ret = unsafe { libc::fcntl(fd, libc::F_PREALLOCATE, &fstore) };
282 if ret != 0 {
283 tracing::warn!(
286 length,
287 "F_PREALLOCATE failed on macOS, file remains sparse"
288 );
289 return Ok(());
290 }
291 if secure {
295 async_zero_fill(adaptor, length).await
296 } else {
297 SECURE_FALLOC_WARN_ONCE.call_once(|| {
298 tracing::warn!(
299 "F_PREALLOCATE succeeded but does not zero-fill; \
300 residual disk data may be exposed. \
301 Enable --secure-falloc=true to zero-fill (at a \
302 performance cost). This warning is logged once."
303 );
304 });
305 Ok(())
306 }
307 }
308 None => adaptor.truncate(length).await,
309 }
310 }
311
312 #[cfg(not(target_os = "macos"))]
313 {
314 adaptor.truncate(length).await
317 }
318 }
319
320 #[cfg(not(unix))]
321 {
322 adaptor.truncate(length).await?;
335 let valid_data_succeeded: bool = if let Some(handle) = adaptor.windows_raw_handle() {
336 let ok = unsafe {
339 windows_sys::Win32::Storage::FileSystem::SetFileValidData(handle, length as i64)
340 };
341 if ok == 0 {
342 let err = unsafe { windows_sys::Win32::Foundation::GetLastError() };
343 if err == windows_sys::Win32::Foundation::ERROR_PRIVILEGE_NOT_HELD {
344 tracing::warn!(
348 length,
349 "SetFileValidData requires SE_MANAGE_VOLUME_PRIVILEGE; \
350 falling back to sparse file. This causes ~2x I/O on \
351 first write because each block must be zeroed by the \
352 writer instead of being pre-allocated."
353 );
354 } else {
355 tracing::warn!(length, err, "SetFileValidData failed; file remains sparse");
356 }
357 false
358 } else {
359 true
360 }
361 } else {
362 false
363 };
364
365 if valid_data_succeeded {
371 if secure {
372 async_zero_fill(adaptor, length).await
373 } else {
374 SECURE_FALLOC_WARN_ONCE.call_once(|| {
375 tracing::warn!(
376 "SetFileValidData succeeded but does not zero-fill; \
377 residual disk data may be exposed. \
378 Enable --secure-falloc=true to zero-fill (at a \
379 performance cost). This warning is logged once."
380 );
381 });
382 Ok(())
383 }
384 } else {
385 Ok(())
386 }
387 }
388}
389
390async fn truncate<D: DiskAdaptor>(adaptor: &mut D, length: u64) -> Result<()> {
396 adaptor.truncate(length).await
397}
398
399pub async fn get_available_space(path: &Path) -> Result<u64> {
400 let parent = path.parent().unwrap_or_else(|| Path::new("."));
401
402 #[cfg(target_os = "linux")]
403 {
404 let _metadata = tokio::fs::metadata(parent)
405 .await
406 .map_err(|e| Aria2Error::Io(e.to_string()))?;
407
408 let statvfs_result = unsafe {
409 let mut stat: libc::statvfs64 = std::mem::zeroed();
410 let ret = libc::statvfs64(
411 parent.to_str().unwrap_or(".").as_ptr() as *const i8,
412 &mut stat,
413 );
414 (ret, stat)
415 };
416
417 if statvfs_result.0 == 0 {
418 let stat = statvfs_result.1;
419 Ok(stat.f_bavail as u64 * stat.f_frsize as u64)
420 } else {
421 Err(Aria2Error::Io("Failed to get disk space".to_string()))
422 }
423 }
424
425 #[cfg(all(unix, not(target_os = "linux")))]
426 {
427 let _metadata = tokio::fs::metadata(parent)
430 .await
431 .map_err(|e| Aria2Error::Io(e.to_string()))?;
432
433 let statvfs_result = unsafe {
434 let mut stat: libc::statvfs = std::mem::zeroed();
435 let ret = libc::statvfs(
436 parent.to_str().unwrap_or(".").as_ptr() as *const i8,
437 &mut stat,
438 );
439 (ret, stat)
440 };
441
442 if statvfs_result.0 == 0 {
443 let stat = statvfs_result.1;
444 Ok(stat.f_bavail as u64 * stat.f_frsize as u64)
445 } else {
446 Err(Aria2Error::Io("Failed to get disk space".to_string()))
447 }
448 }
449
450 #[cfg(windows)]
451 {
452 let metadata = tokio::fs::metadata(parent)
453 .await
454 .map_err(|e| Aria2Error::Io(e.to_string()))?;
455
456 let free = metadata.len();
457 if free > 0 { Ok(free) } else { Ok(u64::MAX / 2) }
458 }
459
460 #[cfg(all(not(unix), not(windows)))]
461 {
462 Ok(u64::MAX)
463 }
464}
465
466#[cfg(test)]
467mod tests {
468 use super::*;
469
470 #[test]
471 fn test_allocation_strategy_from_str() {
472 assert_eq!(
473 AllocationStrategy::from_str("none"),
474 AllocationStrategy::None
475 );
476 assert_eq!(
477 AllocationStrategy::from_str("prealloc"),
478 AllocationStrategy::Prealloc
479 );
480 assert_eq!(
481 AllocationStrategy::from_str("falloc"),
482 AllocationStrategy::Falloc
483 );
484 assert_eq!(
485 AllocationStrategy::from_str("trunc"),
486 AllocationStrategy::Trunc
487 );
488 assert_eq!(
489 AllocationStrategy::from_str("mmap"),
490 AllocationStrategy::Mmap
491 );
492 assert_eq!(
493 AllocationStrategy::from_str("invalid"),
494 AllocationStrategy::None
495 );
496 assert_eq!(AllocationStrategy::from_str(""), AllocationStrategy::None);
497 }
498
499 #[tokio::test]
500 async fn test_preallocate_file_none() {
501 let dir = tempfile::tempdir().unwrap();
502 let path = dir.path().join("test_none.bin");
503 preallocate_file(&path, 1024, "none", false).await.unwrap();
504 assert!(!path.exists());
505 }
506
507 #[tokio::test]
508 async fn test_preallocate_file_trunc() {
509 let dir = tempfile::tempdir().unwrap();
510 let path = dir.path().join("test_trunc.bin");
511 preallocate_file(&path, 4096, "trunc", false).await.unwrap();
512
513 let metadata = tokio::fs::metadata(&path).await.unwrap();
514 assert_eq!(metadata.len(), 4096);
515 }
516
517 #[tokio::test]
518 async fn test_preallocate_file_prealloc() {
519 let dir = tempfile::tempdir().unwrap();
520 let path = dir.path().join("test_prealloc.bin");
521 preallocate_file(&path, 1024 * 1024, "prealloc", false)
522 .await
523 .unwrap();
524
525 let metadata = tokio::fs::metadata(&path).await.unwrap();
526 assert_eq!(metadata.len(), 1024 * 1024);
527 }
528
529 #[tokio::test]
530 async fn test_preallocate_zero_length() {
531 let dir = tempfile::tempdir().unwrap();
532 let path = dir.path().join("test_zero.bin");
533 preallocate_file(&path, 0, "trunc", false).await.unwrap();
534 assert!(!path.exists());
535 }
536
537 #[tokio::test]
538 async fn test_preallocate_creates_parent_dirs() {
539 let dir = tempfile::tempdir().unwrap();
540 let path = dir.path().join("sub1").join("sub2").join("test_nested.bin");
541 preallocate_file(&path, 100, "trunc", false).await.unwrap();
542
543 assert!(path.exists());
544 let metadata = tokio::fs::metadata(&path).await.unwrap();
545 assert_eq!(metadata.len(), 100);
546 }
547
548 #[tokio::test]
549 async fn test_get_available_space_returns_value() {
550 let dir = tempfile::tempdir().unwrap();
551 let space = get_available_space(dir.path()).await;
552 assert!(space.is_ok());
553 let val = space.unwrap();
554 assert!(val > 0);
555 }
556
557 #[tokio::test]
558 async fn test_preallocate_overwrite_existing() {
559 let dir = tempfile::tempdir().unwrap();
560 let path = dir.path().join("test_overwrite.bin");
561
562 tokio::fs::write(&path, b"original data").await.unwrap();
563 preallocate_file(&path, 2048, "trunc", false).await.unwrap();
564
565 let metadata = tokio::fs::metadata(&path).await.unwrap();
566 assert_eq!(metadata.len(), 2048);
567 }
568
569 #[tokio::test]
572 async fn test_allocate_file_cross_platform_prealloc() {
573 let dir = tempfile::tempdir().unwrap();
574 let path = dir.path().join("test_alloc_prealloc.bin");
575
576 tokio::fs::write(&path, b"hello").await.unwrap();
578
579 preallocate_file(&path, 10 * 1024 * 1024, "prealloc", false)
581 .await
582 .unwrap();
583
584 let metadata = tokio::fs::metadata(&path).await.unwrap();
586 assert_eq!(metadata.len(), 10 * 1024 * 1024);
587 }
588
589 #[tokio::test]
592 async fn test_allocate_file_cross_platform_falloc() {
593 let dir = tempfile::tempdir().unwrap();
594 let path = dir.path().join("test_alloc_falloc.bin");
595
596 tokio::fs::write(&path, b"initial data").await.unwrap();
598
599 preallocate_file(&path, 5 * 1024 * 1024, "falloc", false)
601 .await
602 .unwrap();
603
604 let metadata = tokio::fs::metadata(&path).await.unwrap();
606 assert_eq!(metadata.len(), 5 * 1024 * 1024);
607 }
608
609 #[tokio::test]
617 async fn test_fallocate_creates_sparse_file_of_correct_size() {
618 let dir = tempfile::tempdir().unwrap();
619 let path = dir.path().join("test_falloc_sparse.bin");
620
621 preallocate_file(&path, 1024 * 1024, "falloc", false)
622 .await
623 .unwrap();
624
625 let metadata = tokio::fs::metadata(&path).await.unwrap();
626 assert_eq!(metadata.len(), 1024 * 1024);
627 }
628
629 #[tokio::test]
632 async fn test_allocate_file_cross_platform_trunc() {
633 let dir = tempfile::tempdir().unwrap();
634 let path = dir.path().join("test_alloc_trunc.bin");
635
636 tokio::fs::write(&path, b"some initial content here")
638 .await
639 .unwrap();
640
641 preallocate_file(&path, 1024 * 1024, "trunc", false)
643 .await
644 .unwrap();
645
646 let metadata = tokio::fs::metadata(&path).await.unwrap();
648 assert_eq!(metadata.len(), 1024 * 1024);
649 }
650
651 #[tokio::test]
653 async fn test_allocate_file_cross_platform_none() {
654 let dir = tempfile::tempdir().unwrap();
655 let path = dir.path().join("test_alloc_none.bin");
656
657 preallocate_file(&path, 1024 * 1024, "none", false)
659 .await
660 .unwrap();
661
662 assert!(!path.exists());
664 }
665
666 #[tokio::test]
668 async fn test_allocate_large_file() {
669 let dir = tempfile::tempdir().unwrap();
670 let path = dir.path().join("test_large_alloc.bin");
671
672 preallocate_file(&path, 50 * 1024 * 1024, "falloc", false)
674 .await
675 .unwrap();
676
677 let metadata = tokio::fs::metadata(&path).await.unwrap();
679 assert_eq!(metadata.len(), 50 * 1024 * 1024);
680
681 use tokio::io::{AsyncSeekExt, AsyncWriteExt};
683 let mut file = tokio::fs::OpenOptions::new()
684 .write(true)
685 .open(&path)
686 .await
687 .unwrap();
688
689 file.seek(std::io::SeekFrom::Start(49 * 1024 * 1024))
691 .await
692 .unwrap();
693 file.write_all(b"end marker").await.unwrap();
694 file.flush().await.unwrap();
695 drop(file);
696
697 let metadata = tokio::fs::metadata(&path).await.unwrap();
699 assert_eq!(metadata.len(), 50 * 1024 * 1024);
700 }
701
702 #[tokio::test]
704 async fn test_all_strategies_same_result() {
705 let dir = tempfile::tempdir().unwrap();
706 let test_size: u64 = 1024 * 100; let strategies = ["prealloc", "falloc", "trunc"];
709
710 for (i, strategy) in strategies.iter().enumerate() {
711 let path = dir.path().join(format!("test_strategy_{}.bin", i));
712
713 preallocate_file(&path, test_size, strategy, false)
714 .await
715 .unwrap();
716
717 let metadata = tokio::fs::metadata(&path).await.unwrap();
718 assert_eq!(
719 metadata.len(),
720 test_size,
721 "Strategy {} produced wrong size",
722 strategy
723 );
724 }
725 }
726
727 #[tokio::test]
729 async fn test_preallocate_with_progress_callback() {
730 use std::sync::{Arc, Mutex};
731
732 let dir = tempfile::tempdir().unwrap();
733 let path = dir.path().join("test_progress.bin");
734
735 let progress_calls: Arc<Mutex<Vec<(u64, u64)>>> = Arc::new(Mutex::new(Vec::new()));
736 let pc = progress_calls.clone();
737
738 preallocate_file_with_progress(
739 &path,
740 150 * 1024 * 1024, "prealloc",
742 Some(&|allocated, total| {
743 pc.lock().unwrap().push((allocated, total));
744 }),
745 false,
746 )
747 .await
748 .unwrap();
749
750 {
751 let calls = progress_calls.lock().unwrap();
753 assert!(
755 calls.len() >= 2,
756 "expected at least 2 progress calls, got {}",
757 calls.len()
758 );
759 assert_eq!(calls.first().unwrap().0, 0); assert_eq!(
761 calls.last().unwrap().0,
762 150 * 1024 * 1024 );
764 assert_eq!(calls.last().unwrap().1, 150 * 1024 * 1024);
765 } let metadata = tokio::fs::metadata(&path).await.unwrap();
769 assert_eq!(metadata.len(), 150 * 1024 * 1024);
770 }
771
772 #[tokio::test]
774 async fn test_preallocate_small_file_no_progress_callback() {
775 use std::sync::{Arc, Mutex};
776
777 let dir = tempfile::tempdir().unwrap();
778 let path = dir.path().join("test_small_progress.bin");
779
780 let progress_calls: Arc<Mutex<Vec<(u64, u64)>>> = Arc::new(Mutex::new(Vec::new()));
781 let pc = progress_calls.clone();
782
783 preallocate_file_with_progress(
784 &path,
785 1024, "trunc",
787 Some(&|allocated, total| {
788 pc.lock().unwrap().push((allocated, total));
789 }),
790 false,
791 )
792 .await
793 .unwrap();
794
795 let calls = progress_calls.lock().unwrap();
796 assert!(
798 calls.is_empty(),
799 "small file should not trigger progress, got {} calls",
800 calls.len()
801 );
802 }
803
804 #[test]
807 fn test_default_allocation_is_falloc() {
808 use crate::constants;
809 assert_eq!(constants::DEFAULT_FILE_ALLOCATION, "falloc");
810 assert_eq!(
811 AllocationStrategy::from_str(constants::DEFAULT_FILE_ALLOCATION),
812 AllocationStrategy::Falloc
813 );
814 }
815
816 #[tokio::test]
819 async fn test_async_zero_fill() {
820 let dir = tempfile::tempdir().unwrap();
821 let path = dir.path().join("test_zero_fill.bin");
822
823 let mut adaptor = DirectDiskAdaptor::new();
824 adaptor.open(&path).await.unwrap();
825 adaptor.truncate(5 * 1024 * 1024).await.unwrap(); async_zero_fill(&mut adaptor, 5 * 1024 * 1024)
827 .await
828 .unwrap();
829 adaptor.close().await.unwrap();
830
831 let metadata = tokio::fs::metadata(&path).await.unwrap();
833 assert_eq!(metadata.len(), 5 * 1024 * 1024);
834
835 let content = tokio::fs::read(&path).await.unwrap();
837 assert!(content.iter().all(|&b| b == 0), "File should be all zeros");
838 }
839
840 #[test]
842 fn test_secure_falloc_default() {
843 use crate::request::request_group::DownloadOptions;
844 let opts = DownloadOptions::default();
845 assert!(!opts.secure_falloc, "secure_falloc should default to false");
846 }
847}