1use std::path::Path;
2
3use tracing::{debug, warn};
4
5use crate::error::{CoreError, Result};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum LinkKind {
9 Hardlink,
10 Reflink,
11 Copy,
12}
13
14pub async fn link_or_copy(src: &Path, dst: &Path) -> Result<LinkKind> {
19 if let Some(parent) = dst.parent() {
20 tokio::fs::create_dir_all(parent).await?;
21 }
22
23 if dst.exists() || dst.symlink_metadata().is_ok() {
24 tokio::fs::remove_file(dst).await.ok();
25 }
26
27 match tokio::fs::hard_link(src, dst).await {
28 Ok(()) => {
29 debug!(src = %src.display(), dst = %dst.display(), "created hardlink");
30 return Ok(LinkKind::Hardlink);
31 }
32 Err(e) if crate::fs::is_cross_device_error(&e) => {
33 debug!(src = %src.display(), dst = %dst.display(), "hardlink crossed filesystem");
34 }
35 Err(e) => {
36 debug!(src = %src.display(), dst = %dst.display(), "hardlink failed: {e}");
37 }
38 }
39
40 let src = src.to_path_buf();
41 let dst = dst.to_path_buf();
42 match tokio::task::spawn_blocking({
43 let src = src.clone();
44 let dst = dst.clone();
45 move || reflink_copy::reflink(&src, &dst)
46 })
47 .await
48 .map_err(|e| CoreError::Other(format!("reflink task panicked: {e}").into()))?
49 {
50 Ok(()) => {
51 debug!(src = %src.display(), dst = %dst.display(), "created reflink");
52 return Ok(LinkKind::Reflink);
53 }
54 Err(e) => {
55 warn!(
56 src = %src.display(),
57 dst = %dst.display(),
58 "reflink failed; falling back to copy: {e}"
59 );
60 if dst.exists() || dst.symlink_metadata().is_ok() {
61 tokio::fs::remove_file(&dst).await.ok();
62 }
63 }
64 }
65
66 tokio::fs::copy(&src, &dst).await.map_err(|e| {
67 CoreError::Other(
68 format!(
69 "copy fallback failed: {} -> {}: {e}",
70 src.display(),
71 dst.display()
72 )
73 .into(),
74 )
75 })?;
76 debug!(src = %src.display(), dst = %dst.display(), "copied file");
77 Ok(LinkKind::Copy)
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83
84 #[tokio::test]
85 async fn link_or_copy_replaces_existing_destination() {
86 let temp = tempfile::tempdir().unwrap();
87 let src = temp.path().join("src.bin");
88 let dst = temp.path().join("nested").join("dst.bin");
89 tokio::fs::create_dir_all(dst.parent().unwrap())
90 .await
91 .unwrap();
92 tokio::fs::write(&src, b"source").await.unwrap();
93 tokio::fs::write(&dst, b"old").await.unwrap();
94
95 let kind = link_or_copy(&src, &dst).await.unwrap();
96
97 assert!(matches!(
98 kind,
99 LinkKind::Hardlink | LinkKind::Reflink | LinkKind::Copy
100 ));
101 assert_eq!(tokio::fs::read(&dst).await.unwrap(), b"source");
102 }
103}