Skip to main content

rustic_core/backend/ignore/
mapper.rs

1#[cfg(not(windows))]
2pub mod nix_mapper;
3
4use std::{ffi::OsStr, path::Path};
5
6use derive_setters::Setters;
7use ignore::DirEntry;
8use jiff::Timestamp;
9use log::warn;
10use serde::{Deserialize, Serialize};
11use serde_with::serde_as;
12
13use super::{IgnoreErrorKind, IgnoreResult, OpenFile};
14use crate::backend::{
15    ReadSourceEntry,
16    node::{
17        ExtendedAttribute, Metadata, Node, NodeType,
18        modification::{DevIdOption, TimeOption, XattrOption},
19    },
20};
21
22#[cfg(not(windows))]
23use std::os::unix::fs::{FileTypeExt, MetadataExt};
24
25#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
26#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
27#[serde(rename_all = "kebab-case")]
28pub enum BlockdevOption {
29    #[default]
30    Special,
31    File,
32}
33
34#[serde_as]
35#[cfg_attr(feature = "clap", derive(clap::Parser))]
36#[cfg_attr(feature = "merge", derive(conflate::Merge))]
37#[derive(serde::Deserialize, serde::Serialize, Default, Clone, Copy, Debug, Setters)]
38#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
39#[setters(into)]
40#[non_exhaustive]
41/// [`LocalSourceSaveOptions`] describes how entries from a local source will be saved in the repository.
42pub struct LocalSourceSaveOptions {
43    /// Set access time [default: mtime]
44    #[cfg_attr(feature = "clap", clap(long))]
45    #[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
46    pub set_atime: Option<TimeOption>,
47
48    /// Set changed time [default: yes]
49    #[cfg_attr(feature = "clap", clap(long))]
50    #[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
51    pub set_ctime: Option<TimeOption>,
52
53    /// Set device ID [default: hardlink]
54    #[cfg_attr(feature = "clap", clap(long))]
55    #[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
56    pub set_devid: Option<DevIdOption>,
57
58    /// How block devices should be stored [default: special]
59    #[cfg_attr(feature = "clap", clap(long))]
60    #[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
61    pub set_blockdev: Option<BlockdevOption>,
62
63    /// Set extended attributes [default: yes]
64    #[cfg_attr(feature = "clap", clap(long))]
65    #[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
66    pub set_xattrs: Option<XattrOption>,
67}
68
69impl LocalSourceSaveOptions {
70    /// Maps a [`DirEntry`] to a [`ReadSourceEntry`].
71    ///
72    /// # Arguments
73    ///
74    /// * `entry` - The [`DirEntry`] to map.
75    /// * `options` - options for saving entries
76    ///
77    /// # Errors
78    ///
79    /// * If metadata could not be read.
80    /// * If the xattr of the entry could not be read.
81    pub fn map_entry(self, entry: DirEntry) -> IgnoreResult<ReadSourceEntry<OpenFile>> {
82        let name = entry.file_name();
83        let m = entry
84            .metadata()
85            .map_err(|err| IgnoreErrorKind::AcquiringMetadataFailed {
86                name: name.to_string_lossy().to_string(),
87                source: err,
88            })?;
89
90        let mtime = m.modified().ok().and_then(|t| Timestamp::try_from(t).ok());
91        let atime = || m.accessed().ok().and_then(|t| Timestamp::try_from(t).ok());
92        let atime = self
93            .set_atime
94            .unwrap_or(TimeOption::Mtime)
95            .map_or_else(atime, mtime);
96        let ctime = || Self::ctime(&m);
97        let ctime = self
98            .set_ctime
99            .unwrap_or(TimeOption::Yes)
100            .map_or_else(ctime, mtime);
101
102        let (uid, user, gid, group) = Self::user_group(&m);
103        let size = if m.is_dir() { 0 } else { m.len() };
104        let device_id = self
105            .set_devid
106            .unwrap_or_default()
107            .map_or_else(|| Self::device_id(&m), Self::hardlink(&m));
108        let xattr = || {
109            Self::xattrs(entry.path())
110                .inspect_err(|err| warn!("ignoring error obtaining xargs: {err}"))
111                .unwrap_or_default()
112        };
113        let extended_attributes = self.set_xattrs.unwrap_or_default().map_or_else(xattr);
114        let (mode, inode, links) = Self::nix_infos(&m);
115
116        let meta = Metadata {
117            mode,
118            mtime,
119            atime,
120            ctime,
121            uid,
122            gid,
123            user,
124            group,
125            inode,
126            device_id,
127            size,
128            links,
129            extended_attributes,
130        };
131
132        let node = self.to_node(&entry, &m, meta)?;
133        let path = entry.into_path();
134        let open = Some(OpenFile(path.clone()));
135        Ok(ReadSourceEntry { path, node, open })
136    }
137
138    fn to_node(
139        self,
140        entry: &DirEntry,
141        m: &std::fs::Metadata,
142        meta: Metadata,
143    ) -> IgnoreResult<Node> {
144        let name = entry.file_name();
145        let node = if m.is_dir() {
146            Node::new_node(name, NodeType::Dir, meta)
147        } else if m.is_symlink() {
148            let path = entry.path();
149            let target = std::fs::read_link(path).map_err(|err| IgnoreErrorKind::ErrorLink {
150                path: path.to_path_buf(),
151                source: err,
152            })?;
153            let node_type = NodeType::from_link(&target);
154            Node::new_node(name, node_type, meta)
155        } else {
156            self.to_node_other(name, m, meta)
157        };
158        Ok(node)
159    }
160}
161
162#[cfg(not(windows))]
163impl LocalSourceSaveOptions {
164    fn ctime(m: &std::fs::Metadata) -> Option<Timestamp> {
165        #[allow(clippy::cast_possible_truncation)]
166        Timestamp::new(m.ctime(), m.ctime_nsec() as i32).ok()
167    }
168
169    fn device_id(m: &std::fs::Metadata) -> u64 {
170        m.dev()
171    }
172
173    fn hardlink(m: &std::fs::Metadata) -> bool {
174        m.nlink() > 1 && !m.is_dir()
175    }
176
177    fn user_group(
178        m: &std::fs::Metadata,
179    ) -> (Option<u32>, Option<String>, Option<u32>, Option<String>) {
180        let uid = m.uid();
181        let gid = m.gid();
182        let user = nix_mapper::get_user_by_uid(uid);
183        let group = nix_mapper::get_group_by_gid(gid);
184        (Some(uid), user, Some(gid), group)
185    }
186
187    fn nix_infos(m: &std::fs::Metadata) -> (Option<u32>, u64, u64) {
188        let mode = nix_mapper::map_mode_to_go(m.mode());
189        let inode = m.ino();
190        let links = if m.is_dir() { 0 } else { m.nlink() };
191        (Some(mode), inode, links)
192    }
193
194    /// List [`ExtendedAttribute`] for a [`Node`] located at `path`
195    ///
196    /// # Argument
197    ///
198    /// * `path` to the [`Node`] for which to list attributes
199    ///
200    /// # Errors
201    ///
202    /// * If Xattr couldn't be listed or couldn't be read
203    #[cfg(not(target_os = "openbsd"))]
204    fn xattrs(path: &Path) -> IgnoreResult<Vec<ExtendedAttribute>> {
205        xattr::list(path)
206            .map_err(|err| IgnoreErrorKind::ErrorXattr {
207                path: path.to_path_buf(),
208                source: err,
209            })?
210            .map(|name| {
211                Ok(ExtendedAttribute {
212                    name: name.to_string_lossy().to_string(),
213                    value: xattr::get(path, name).map_err(|err| IgnoreErrorKind::ErrorXattr {
214                        path: path.to_path_buf(),
215                        source: err,
216                    })?,
217                })
218            })
219            .collect::<IgnoreResult<Vec<ExtendedAttribute>>>()
220    }
221
222    #[cfg(target_os = "openbsd")]
223    fn xattrs(_path: &Path) -> IgnoreResult<Vec<ExtendedAttribute>> {
224        Ok(Vec::new())
225    }
226
227    fn to_node_other(self, name: &OsStr, m: &std::fs::Metadata, meta: Metadata) -> Node {
228        let filetype = m.file_type();
229        if filetype.is_block_device() {
230            if matches!(self.set_blockdev.unwrap_or_default(), BlockdevOption::File) {
231                Node::new_node(name, NodeType::File, meta)
232            } else {
233                let node_type = NodeType::Dev { device: m.rdev() };
234                Node::new_node(name, node_type, meta)
235            }
236        } else if filetype.is_char_device() {
237            let node_type = NodeType::Chardev { device: m.rdev() };
238            Node::new_node(name, node_type, meta)
239        } else if filetype.is_fifo() {
240            Node::new_node(name, NodeType::Fifo, meta)
241        } else if filetype.is_socket() {
242            Node::new_node(name, NodeType::Socket, meta)
243        } else {
244            Node::new_node(name, NodeType::File, meta)
245        }
246    }
247}
248
249#[cfg(windows)]
250impl LocalSourceSaveOptions {
251    fn ctime(m: &std::fs::Metadata) -> Option<Timestamp> {
252        m.created().ok().and_then(|t| Timestamp::try_from(t).ok())
253    }
254    fn device_id(_m: &std::fs::Metadata) -> u64 {
255        0
256    }
257    fn hardlink(m: &std::fs::Metadata) -> bool {
258        false
259    }
260    fn user_group(
261        _m: &std::fs::Metadata,
262    ) -> (Option<u32>, Option<String>, Option<u32>, Option<String>) {
263        (None, None, None, None)
264    }
265
266    fn nix_infos(_m: &std::fs::Metadata) -> (Option<u32>, u64, u64) {
267        (None, 0, 0)
268    }
269
270    fn xattrs(_path: &Path) -> IgnoreResult<Vec<ExtendedAttribute>> {
271        Ok(Vec::new())
272    }
273
274    fn to_node_other(self, name: &OsStr, _m: &std::fs::Metadata, meta: Metadata) -> Node {
275        Node::new_node(name, NodeType::File, meta)
276    }
277}