common/
preserve.rs

1use anyhow::{Context, Result};
2use serde::{Deserialize, Serialize};
3use std::os::unix::fs::MetadataExt;
4use std::os::unix::prelude::PermissionsExt;
5use tracing::instrument;
6
7pub trait Metadata {
8    fn uid(&self) -> u32;
9    fn gid(&self) -> u32;
10    fn atime(&self) -> i64;
11    fn atime_nsec(&self) -> i64;
12    fn mtime(&self) -> i64;
13    fn mtime_nsec(&self) -> i64;
14    fn permissions(&self) -> std::fs::Permissions;
15    // ctime cannot be set manually, but we include it for comparison purposes
16    // default implementation returns 0 to indicate ctime is not available (e.g., in protocol::Metadata)
17    fn ctime(&self) -> i64 {
18        0
19    }
20    fn ctime_nsec(&self) -> i64 {
21        0
22    }
23    // size is not preserved (cannot be set), but included for comparison purposes
24    // default implementation returns 0 to indicate size is not available or not applicable
25    fn size(&self) -> u64 {
26        0
27    }
28}
29
30impl Metadata for std::fs::Metadata {
31    fn uid(&self) -> u32 {
32        MetadataExt::uid(self)
33    }
34    fn gid(&self) -> u32 {
35        MetadataExt::gid(self)
36    }
37    fn atime(&self) -> i64 {
38        MetadataExt::atime(self)
39    }
40    fn atime_nsec(&self) -> i64 {
41        MetadataExt::atime_nsec(self)
42    }
43    fn mtime(&self) -> i64 {
44        MetadataExt::mtime(self)
45    }
46    fn mtime_nsec(&self) -> i64 {
47        MetadataExt::mtime_nsec(self)
48    }
49    fn permissions(&self) -> std::fs::Permissions {
50        self.permissions()
51    }
52    fn ctime(&self) -> i64 {
53        MetadataExt::ctime(self)
54    }
55    fn ctime_nsec(&self) -> i64 {
56        MetadataExt::ctime_nsec(self)
57    }
58    fn size(&self) -> u64 {
59        self.len()
60    }
61}
62
63#[derive(Copy, Clone, Debug, Default, Deserialize, Serialize)]
64pub struct UserAndTimeSettings {
65    pub uid: bool,
66    pub gid: bool,
67    pub time: bool,
68}
69
70impl UserAndTimeSettings {
71    #[must_use]
72    pub fn any(&self) -> bool {
73        self.uid || self.gid || self.time
74    }
75}
76
77pub type ModeMask = u32;
78
79#[derive(Copy, Clone, Debug, Deserialize, Serialize)]
80pub struct FileSettings {
81    pub user_and_time: UserAndTimeSettings,
82    pub mode_mask: ModeMask,
83}
84
85impl Default for FileSettings {
86    fn default() -> Self {
87        Self {
88            user_and_time: UserAndTimeSettings::default(),
89            mode_mask: 0o0777, // remove sticky bit, setuid and setgid to mimic "cp" tool
90        }
91    }
92}
93
94#[derive(Copy, Clone, Debug, Deserialize, Serialize)]
95pub struct DirSettings {
96    pub user_and_time: UserAndTimeSettings,
97    pub mode_mask: ModeMask,
98}
99
100impl Default for DirSettings {
101    fn default() -> Self {
102        Self {
103            user_and_time: UserAndTimeSettings::default(),
104            mode_mask: 0o0777,
105        }
106    }
107}
108
109#[derive(Copy, Clone, Debug, Default, Deserialize, Serialize)]
110pub struct SymlinkSettings {
111    pub user_and_time: UserAndTimeSettings,
112}
113
114impl SymlinkSettings {
115    #[must_use]
116    pub fn any(&self) -> bool {
117        self.user_and_time.any()
118    }
119}
120
121#[derive(Copy, Clone, Debug, Default, Deserialize, Serialize)]
122pub struct Settings {
123    pub file: FileSettings,
124    pub dir: DirSettings,
125    pub symlink: SymlinkSettings,
126}
127
128#[instrument]
129async fn set_owner_and_time<Meta: Metadata + std::fmt::Debug>(
130    settings: &UserAndTimeSettings,
131    path: &std::path::Path,
132    metadata: &Meta,
133) -> Result<()> {
134    let settings = settings.to_owned();
135    let dst = path.to_owned();
136    let uid = metadata.uid();
137    let gid = metadata.gid();
138    let atime = metadata.atime();
139    let atime_nsec = metadata.atime_nsec();
140    let mtime = metadata.mtime();
141    let mtime_nsec = metadata.mtime_nsec();
142    tokio::task::spawn_blocking(move || -> Result<()> {
143        if settings.uid || settings.gid {
144            // set user and group
145            tracing::debug!("setting uid ang gid");
146            let uid_val = if settings.uid { Some(uid.into()) } else { None };
147            let gid_val = if settings.gid { Some(gid.into()) } else { None };
148            nix::unistd::fchownat(
149                None,
150                &dst,
151                uid_val,
152                gid_val,
153                nix::fcntl::AtFlags::AT_SYMLINK_NOFOLLOW,
154            )
155            .with_context(|| {
156                format!(
157                    "cannot set {:?} owner to {:?} and/or group id to {:?}",
158                    &dst, &uid_val, &gid_val
159                )
160            })?;
161        }
162        // set timestamps last - modifying other file metadata can change them
163        if settings.time {
164            tracing::debug!("setting timestamps");
165            let atime_spec = nix::sys::time::TimeSpec::new(atime, atime_nsec);
166            let mtime_spec = nix::sys::time::TimeSpec::new(mtime, mtime_nsec);
167            nix::sys::stat::utimensat(
168                None,
169                &dst,
170                &atime_spec,
171                &mtime_spec,
172                nix::sys::stat::UtimensatFlags::NoFollowSymlink,
173            )
174            .with_context(|| format!("failed setting timestamps for {:?}", &dst))?;
175        }
176        Ok(())
177    })
178    .await?
179}
180
181pub async fn set_file_metadata<Meta: Metadata + std::fmt::Debug>(
182    settings: &Settings,
183    metadata: &Meta,
184    path: &std::path::Path,
185) -> Result<()> {
186    let permissions = if settings.file.mode_mask == 0o7777 {
187        // special case for default preserve
188        metadata.permissions()
189    } else {
190        std::fs::Permissions::from_mode(metadata.permissions().mode() & settings.file.mode_mask)
191    };
192    let file = tokio::fs::File::open(path).await?;
193    file.set_permissions(permissions.clone())
194        .await
195        .with_context(|| format!("cannot set {:?} permissions to {:?}", &path, &permissions))?;
196    // close the file we don't accidentally race and have permissions applied after the timestamps, which would modify them!
197    drop(file);
198    set_owner_and_time(&settings.file.user_and_time, path, metadata).await?;
199    Ok(())
200}
201
202pub async fn set_dir_metadata<Meta: Metadata + std::fmt::Debug>(
203    settings: &Settings,
204    metadata: &Meta,
205    path: &std::path::Path,
206) -> Result<()> {
207    let permissions = if settings.dir.mode_mask == 0o7777 {
208        // special case for default preserve
209        metadata.permissions()
210    } else {
211        std::fs::Permissions::from_mode(metadata.permissions().mode() & settings.dir.mode_mask)
212    };
213    tokio::fs::set_permissions(path, permissions.clone())
214        .await
215        .with_context(|| format!("cannot set {:?} permissions to {:?}", &path, &permissions))?;
216    set_owner_and_time(&settings.dir.user_and_time, path, metadata).await?;
217    Ok(())
218}
219
220pub async fn set_symlink_metadata<Meta: Metadata + std::fmt::Debug>(
221    settings: &Settings,
222    metadata: &Meta,
223    path: &std::path::Path,
224) -> Result<()> {
225    // we don't set permissions for symlinks, only owner and time
226    set_owner_and_time(&settings.file.user_and_time, path, metadata).await?;
227    Ok(())
228}
229
230#[must_use]
231pub fn preserve_all() -> Settings {
232    let user_and_time = UserAndTimeSettings {
233        uid: true,
234        gid: true,
235        time: true,
236    };
237
238    Settings {
239        file: FileSettings {
240            user_and_time,
241            mode_mask: 0o7777,
242        },
243        dir: DirSettings {
244            user_and_time,
245            mode_mask: 0o7777,
246        },
247        symlink: SymlinkSettings { user_and_time },
248    }
249}
250
251#[must_use]
252pub fn preserve_default() -> Settings {
253    Settings::default()
254}