]> git.proxmox.com Git - proxmox-backup.git/blobdiff - src/pxar/create.rs
pxar: factor out PxarCreateOptions
[proxmox-backup.git] / src / pxar / create.rs
index 2de579a35f7031ba16783a7b51fe2f93ccfa26a4..229e3f597fd44129dfc44842b25a7db97970ac40 100644 (file)
@@ -1,5 +1,4 @@
 use std::collections::{HashSet, HashMap};
-use std::convert::TryFrom;
 use std::ffi::{CStr, CString, OsStr};
 use std::fmt;
 use std::io::{self, Read, Write};
@@ -13,7 +12,7 @@ use nix::errno::Errno;
 use nix::fcntl::OFlag;
 use nix::sys::stat::{FileStat, Mode};
 
-use pathpatterns::{MatchEntry, MatchList, MatchType, PatternFlag};
+use pathpatterns::{MatchEntry, MatchFlag, MatchList, MatchType, PatternFlag};
 use pxar::Metadata;
 use pxar::encoder::LinkOffset;
 
@@ -23,10 +22,27 @@ use proxmox::tools::fd::RawFdNum;
 use proxmox::tools::vec;
 
 use crate::pxar::catalog::BackupCatalogWriter;
+use crate::pxar::metadata::errno_is_unsupported;
 use crate::pxar::Flags;
 use crate::pxar::tools::assert_single_path_component;
 use crate::tools::{acl, fs, xattr, Fd};
 
+/// Pxar options for creating a pxar archive/stream
+#[derive(Default, Clone)]
+pub struct PxarCreateOptions {
+    /// Device/mountpoint st_dev numbers that should be included. None for no limitation.
+    pub device_set: Option<HashSet<u64>>,
+    /// Exclusion patterns
+    pub patterns: Vec<MatchEntry>,
+    /// Maximum number of entries to hold in memory
+    pub entries_max: usize,
+    /// Skip lost+found directory
+    pub skip_lost_and_found: bool,
+    /// Verbose output
+    pub verbose: bool,
+}
+
+
 fn detect_fs_type(fd: RawFd) -> Result<i64, Error> {
     let mut fs_stat = std::mem::MaybeUninit::uninit();
     let res = unsafe { libc::fstatfs(fd, fs_stat.as_mut_ptr()) };
@@ -40,8 +56,7 @@ fn detect_fs_type(fd: RawFd) -> Result<i64, Error> {
 pub fn is_virtual_file_system(magic: i64) -> bool {
     use proxmox::sys::linux::magic::*;
 
-    match magic {
-        BINFMTFS_MAGIC |
+    matches!(magic, BINFMTFS_MAGIC |
         CGROUP2_SUPER_MAGIC |
         CGROUP_SUPER_MAGIC |
         CONFIGFS_MAGIC |
@@ -58,9 +73,7 @@ pub fn is_virtual_file_system(magic: i64) -> bool {
         SECURITYFS_MAGIC |
         SELINUX_MAGIC |
         SMACK_MAGIC |
-        SYSFS_MAGIC => true,
-        _ => false
-    }
+        SYSFS_MAGIC)
 }
 
 #[derive(Debug)]
@@ -89,7 +102,21 @@ struct HardLinkInfo {
     st_ino: u64,
 }
 
-/// In case we want to collect them or redirect them we can just add this here:
+/// TODO: make a builder for the create_archive call for fewer parameters and add a method to add a
+/// logger which does not write to stderr.
+struct Logger;
+
+impl std::io::Write for Logger {
+    fn write(&mut self, data: &[u8]) -> io::Result<usize> {
+        std::io::stderr().write(data)
+    }
+
+    fn flush(&mut self) -> io::Result<()> {
+        std::io::stderr().flush()
+    }
+}
+
+/// And the error case.
 struct ErrorReporter;
 
 impl std::io::Write for ErrorReporter {
@@ -116,6 +143,7 @@ struct Archiver<'a, 'b> {
     device_set: Option<HashSet<u64>>,
     hardlinks: HashMap<HardLinkInfo, (PathBuf, LinkOffset)>,
     errors: ErrorReporter,
+    logger: Logger,
     file_copy_buffer: Vec<u8>,
 }
 
@@ -124,13 +152,10 @@ type Encoder<'a, 'b> = pxar::encoder::Encoder<'a, &'b mut dyn pxar::encoder::Seq
 pub fn create_archive<T, F>(
     source_dir: Dir,
     mut writer: T,
-    mut patterns: Vec<MatchEntry>,
     feature_flags: Flags,
-    mut device_set: Option<HashSet<u64>>,
-    skip_lost_and_found: bool,
     mut callback: F,
-    entry_limit: usize,
     catalog: Option<&mut dyn BackupCatalogWriter>,
+    options: PxarCreateOptions,
 ) -> Result<(), Error>
 where
     T: pxar::encoder::SeqWrite,
@@ -152,6 +177,7 @@ where
     )
     .map_err(|err| format_err!("failed to get metadata for source directory: {}", err))?;
 
+    let mut device_set = options.device_set.clone();
     if let Some(ref mut set) = device_set {
         set.insert(stat.st_dev);
     }
@@ -159,7 +185,9 @@ where
     let writer = &mut writer as &mut dyn pxar::encoder::SeqWrite;
     let mut encoder = Encoder::new(writer, &metadata)?;
 
-    if skip_lost_and_found {
+    let mut patterns = options.patterns.clone();
+
+    if options.skip_lost_and_found {
         patterns.push(MatchEntry::parse_pattern(
             "lost+found",
             PatternFlag::PATH_NAME,
@@ -176,11 +204,12 @@ where
         catalog,
         path: PathBuf::new(),
         entry_counter: 0,
-        entry_limit,
+        entry_limit: options.entries_max,
         current_st_dev: stat.st_dev,
         device_set,
         hardlinks: HashMap::new(),
         errors: ErrorReporter,
+        logger: Logger,
         file_copy_buffer: vec::undefined(4 * 1024 * 1024),
     };
 
@@ -221,7 +250,15 @@ impl<'a, 'b> Archiver<'a, 'b> {
         let old_patterns_count = self.patterns.len();
         self.read_pxar_excludes(dir.as_raw_fd())?;
 
-        let file_list = self.generate_directory_file_list(&mut dir, is_root)?;
+        let mut file_list = self.generate_directory_file_list(&mut dir, is_root)?;
+
+        if is_root && old_patterns_count > 0 {
+            file_list.push(FileListEntry {
+                name: CString::new(".pxarexclude-cli").unwrap(),
+                path: PathBuf::new(),
+                stat: unsafe { std::mem::zeroed() },
+            });
+        }
 
         let dir_fd = dir.as_raw_fd();
 
@@ -231,11 +268,11 @@ impl<'a, 'b> Archiver<'a, 'b> {
             let file_name = file_entry.name.to_bytes();
 
             if is_root && file_name == b".pxarexclude-cli" {
-                self.encode_pxarexclude_cli(encoder, &file_entry.name)?;
+                self.encode_pxarexclude_cli(encoder, &file_entry.name, old_patterns_count)?;
                 continue;
             }
 
-            (self.callback)(Path::new(OsStr::from_bytes(file_name)))?;
+            (self.callback)(&file_entry.path)?;
             self.path = file_entry.path;
             self.add_entry(encoder, dir_fd, &file_entry.name, &file_entry.stat)
                 .map_err(|err| self.wrap_err(err))?;
@@ -258,68 +295,101 @@ impl<'a, 'b> Archiver<'a, 'b> {
         oflags: OFlag,
         existed: bool,
     ) -> Result<Option<Fd>, Error> {
-        match Fd::openat(
-            &unsafe { RawFdNum::from_raw_fd(parent) },
-            file_name,
-            oflags,
-            Mode::empty(),
-        ) {
-            Ok(fd) => Ok(Some(fd)),
-            Err(nix::Error::Sys(Errno::ENOENT)) => {
-                if existed {
-                    self.report_vanished_file()?;
+        // common flags we always want to use:
+        let oflags = oflags | OFlag::O_CLOEXEC | OFlag::O_NOCTTY;
+
+        let mut noatime = OFlag::O_NOATIME;
+        loop {
+            return match Fd::openat(
+                &unsafe { RawFdNum::from_raw_fd(parent) },
+                file_name,
+                oflags | noatime,
+                Mode::empty(),
+            ) {
+                Ok(fd) => Ok(Some(fd)),
+                Err(nix::Error::Sys(Errno::ENOENT)) => {
+                    if existed {
+                        self.report_vanished_file()?;
+                    }
+                    Ok(None)
                 }
-                Ok(None)
-            }
-            Err(nix::Error::Sys(Errno::EACCES)) => {
-                writeln!(self.errors, "failed to open file: {:?}: access denied", file_name)?;
-                Ok(None)
+                Err(nix::Error::Sys(Errno::EACCES)) => {
+                    writeln!(self.errors, "failed to open file: {:?}: access denied", file_name)?;
+                    Ok(None)
+                }
+                Err(nix::Error::Sys(Errno::EPERM)) if !noatime.is_empty() => {
+                    // Retry without O_NOATIME:
+                    noatime = OFlag::empty();
+                    continue;
+                }
+                Err(other) => Err(Error::from(other)),
             }
-            Err(other) => Err(Error::from(other)),
         }
     }
 
     fn read_pxar_excludes(&mut self, parent: RawFd) -> Result<(), Error> {
-        let fd = self.open_file(
-            parent,
-            c_str!(".pxarexclude"),
-            OFlag::O_RDONLY | OFlag::O_CLOEXEC | OFlag::O_NOCTTY,
-            false,
-        )?;
+        let fd = match self.open_file(parent, c_str!(".pxarexclude"), OFlag::O_RDONLY, false)? {
+            Some(fd) => fd,
+            None => return Ok(()),
+        };
 
         let old_pattern_count = self.patterns.len();
 
-        if let Some(fd) = fd {
-            let file = unsafe { std::fs::File::from_raw_fd(fd.into_raw_fd()) };
-
-            use io::BufRead;
-            for line in io::BufReader::new(file).lines() {
-                let line = match line {
-                    Ok(line) => line,
-                    Err(err) => {
-                        let _ = writeln!(
-                            self.errors,
-                            "ignoring .pxarexclude after read error in {:?}: {}",
-                            self.path,
-                            err,
-                        );
-                        self.patterns.truncate(old_pattern_count);
-                        return Ok(());
-                    }
-                };
+        let path_bytes = self.path.as_os_str().as_bytes();
+
+        let file = unsafe { std::fs::File::from_raw_fd(fd.into_raw_fd()) };
+
+        use io::BufRead;
+        for line in io::BufReader::new(file).split(b'\n') {
+            let line = match line {
+                Ok(line) => line,
+                Err(err) => {
+                    let _ = writeln!(
+                        self.errors,
+                        "ignoring .pxarexclude after read error in {:?}: {}",
+                        self.path,
+                        err,
+                    );
+                    self.patterns.truncate(old_pattern_count);
+                    return Ok(());
+                }
+            };
 
-                let line = line.trim();
+            let line = crate::tools::strip_ascii_whitespace(&line);
 
-                if line.is_empty() || line.starts_with('#') {
-                    continue;
-                }
+            if line.is_empty() || line[0] == b'#' {
+                continue;
+            }
+
+            let mut buf;
+            let (line, mode, anchored) = if line[0] == b'/' {
+                buf = Vec::with_capacity(path_bytes.len() + 1 + line.len());
+                buf.extend(path_bytes);
+                buf.extend(line);
+                (&buf[..], MatchType::Exclude, true)
+            } else if line.starts_with(b"!/") {
+                // inverted case with absolute path
+                buf = Vec::with_capacity(path_bytes.len() + line.len());
+                buf.extend(path_bytes);
+                buf.extend(&line[1..]); // without the '!'
+                (&buf[..], MatchType::Include, true)
+            } else if line.starts_with(b"!") {
+                (&line[1..], MatchType::Include, false)
+            } else {
+                (line, MatchType::Exclude, false)
+            };
 
-                match MatchEntry::parse_pattern(line, PatternFlag::PATH_NAME, MatchType::Exclude) {
-                    Ok(pattern) => self.patterns.push(pattern),
-                    Err(err) => {
-                        let _ = writeln!(self.errors, "bad pattern in {:?}: {}", self.path, err);
+            match MatchEntry::parse_pattern(line, PatternFlag::PATH_NAME, mode) {
+                Ok(pattern) => {
+                    if anchored {
+                        self.patterns.push(pattern.add_flags(MatchFlag::ANCHORED));
+                    } else {
+                        self.patterns.push(pattern);
                     }
                 }
+                Err(err) => {
+                    let _ = writeln!(self.errors, "bad pattern in {:?}: {}", self.path, err);
+                }
             }
         }
 
@@ -330,8 +400,9 @@ impl<'a, 'b> Archiver<'a, 'b> {
         &mut self,
         encoder: &mut Encoder,
         file_name: &CStr,
+        patterns_count: usize,
     ) -> Result<(), Error> {
-        let content = generate_pxar_excludes_cli(&self.patterns);
+        let content = generate_pxar_excludes_cli(&self.patterns[..patterns_count]);
 
         if let Some(ref mut catalog) = self.catalog {
             catalog.add_file(file_name, content.len() as u64, 0)?;
@@ -355,14 +426,6 @@ impl<'a, 'b> Archiver<'a, 'b> {
 
         let mut file_list = Vec::new();
 
-        if is_root && !self.patterns.is_empty() {
-            file_list.push(FileListEntry {
-                name: CString::new(".pxarexclude-cli").unwrap(),
-                path: PathBuf::new(),
-                stat: unsafe { std::mem::zeroed() },
-            });
-        }
-
         for file in dir.iter() {
             let file = file?;
 
@@ -376,10 +439,6 @@ impl<'a, 'b> Archiver<'a, 'b> {
                 continue;
             }
 
-            if file_name_bytes == b".pxarexclude" {
-                continue;
-            }
-
             let os_file_name = OsStr::from_bytes(file_name_bytes);
             assert_single_path_component(os_file_name)?;
             let full_path = self.path.join(os_file_name);
@@ -394,9 +453,10 @@ impl<'a, 'b> Archiver<'a, 'b> {
                 Err(err) => bail!("stat failed on {:?}: {}", full_path, err),
             };
 
+            let match_path = PathBuf::from("/").join(full_path.clone());
             if self
                 .patterns
-                .matches(full_path.as_os_str().as_bytes(), Some(stat.st_mode as u32))
+                .matches(match_path.as_os_str().as_bytes(), Some(stat.st_mode as u32))
                 == Some(MatchType::Exclude)
             {
                 continue;
@@ -461,7 +521,7 @@ impl<'a, 'b> Archiver<'a, 'b> {
         let fd = self.open_file(
             parent,
             c_file_name,
-            open_mode | OFlag::O_RDONLY | OFlag::O_NOFOLLOW | OFlag::O_CLOEXEC | OFlag::O_NOCTTY,
+            open_mode | OFlag::O_RDONLY | OFlag::O_NOFOLLOW,
             true,
         )?;
 
@@ -502,7 +562,7 @@ impl<'a, 'b> Archiver<'a, 'b> {
 
                 let file_size = stat.st_size as u64;
                 if let Some(ref mut catalog) = self.catalog {
-                    catalog.add_file(c_file_name, file_size, stat.st_mtime as u64)?;
+                    catalog.add_file(c_file_name, file_size, stat.st_mtime)?;
                 }
 
                 let offset: LinkOffset =
@@ -599,6 +659,7 @@ impl<'a, 'b> Archiver<'a, 'b> {
         }
 
         let result = if skip_contents {
+            writeln!(self.logger, "skipping mount point: {:?}", self.path)?;
             Ok(())
         } else {
             self.archive_dir_contents(&mut encoder, dir, false)
@@ -624,7 +685,12 @@ impl<'a, 'b> Archiver<'a, 'b> {
         let mut remaining = file_size;
         let mut out = encoder.create_file(metadata, file_name, file_size)?;
         while remaining != 0 {
-            let mut got = file.read(&mut self.file_copy_buffer[..])?;
+            let mut got = match file.read(&mut self.file_copy_buffer[..]) {
+                Ok(0) => break,
+                Ok(got) => got,
+                Err(err) if err.kind() == std::io::ErrorKind::Interrupted => continue,
+                Err(err) => bail!(err),
+            };
             if got as u64 > remaining {
                 self.report_file_grew_while_reading()?;
                 got = remaining as usize;
@@ -677,16 +743,16 @@ fn get_metadata(fd: RawFd, stat: &FileStat, flags: Flags, fs_magic: i64) -> Resu
     // required for some of these
     let proc_path = Path::new("/proc/self/fd/").join(fd.to_string());
 
-    let mtime = u64::try_from(stat.st_mtime * 1_000_000_000 + stat.st_mtime_nsec)
-        .map_err(|_| format_err!("file with negative mtime"))?;
-
     let mut meta = Metadata {
         stat: pxar::Stat {
             mode: u64::from(stat.st_mode),
             flags: 0,
             uid: stat.st_uid,
             gid: stat.st_gid,
-            mtime,
+            mtime: pxar::format::StatxTimestamp {
+                secs: stat.st_mtime,
+                nanos: stat.st_mtime_nsec as u32,
+            },
         },
         ..Default::default()
     };
@@ -698,13 +764,6 @@ fn get_metadata(fd: RawFd, stat: &FileStat, flags: Flags, fs_magic: i64) -> Resu
     Ok(meta)
 }
 
-fn errno_is_unsupported(errno: Errno) -> bool {
-    match errno {
-        Errno::ENOTTY | Errno::ENOSYS | Errno::EBADF | Errno::EOPNOTSUPP | Errno::EINVAL => true,
-        _ => false,
-    }
-}
-
 fn get_fcaps(meta: &mut Metadata, fd: RawFd, flags: Flags) -> Result<(), Error> {
     if flags.contains(Flags::WITH_FCAPS) {
         return Ok(());
@@ -769,7 +828,7 @@ fn get_xattr_fcaps_acl(
 }
 
 fn get_chattr(metadata: &mut Metadata, fd: RawFd) -> Result<(), Error> {
-    let mut attr: usize = 0;
+    let mut attr: libc::c_long = 0;
 
     match unsafe { fs::read_attr_fd(fd, &mut attr) } {
         Ok(_) => (),
@@ -779,7 +838,7 @@ fn get_chattr(metadata: &mut Metadata, fd: RawFd) -> Result<(), Error> {
         Err(err) => bail!("failed to read file attributes: {}", err),
     }
 
-    metadata.stat.flags |= Flags::from_chattr(attr as u32).bits();
+    metadata.stat.flags |= Flags::from_chattr(attr).bits();
 
     Ok(())
 }
@@ -980,7 +1039,7 @@ fn process_acl(
 /// Since we are generating an *exclude* list, we need to invert this, so includes get a `'!'`
 /// prefix.
 fn generate_pxar_excludes_cli(patterns: &[MatchEntry]) -> Vec<u8> {
-    use pathpatterns::{MatchFlag, MatchPattern};
+    use pathpatterns::MatchPattern;
 
     let mut content = Vec::new();