]> git.proxmox.com Git - proxmox-backup.git/blame - pbs-client/src/pxar/create.rs
move pbs-tools/src/str.rs to pbs-client/src/pxar/create.rs
[proxmox-backup.git] / pbs-client / src / pxar / create.rs
CommitLineData
c443f58b 1use std::collections::{HashSet, HashMap};
c443f58b
WB
2use std::ffi::{CStr, CString, OsStr};
3use std::fmt;
a8e2940f 4use std::io::{self, Read, Write};
c443f58b
WB
5use std::os::unix::ffi::OsStrExt;
6use std::os::unix::io::{AsRawFd, FromRawFd, IntoRawFd, RawFd};
7use std::path::{Path, PathBuf};
6afb60ab 8use std::sync::{Arc, Mutex};
c443f58b
WB
9
10use anyhow::{bail, format_err, Error};
11use nix::dir::Dir;
12use nix::errno::Errno;
13use nix::fcntl::OFlag;
14use nix::sys::stat::{FileStat, Mode};
6afb60ab
SR
15use futures::future::BoxFuture;
16use futures::FutureExt;
c443f58b 17
8d841f81 18use pathpatterns::{MatchEntry, MatchFlag, MatchList, MatchType, PatternFlag};
c443f58b 19use pxar::Metadata;
6afb60ab 20use pxar::encoder::{SeqWrite, LinkOffset};
c443f58b 21
25877d05
DM
22use proxmox_sys::error::SysError;
23use proxmox_sys::fd::RawFdNum;
24use proxmox_sys::fd::Fd;
25use proxmox_sys::fs::{self, acl, xattr};
6ef1b649
WB
26use proxmox_io::vec;
27use proxmox_lang::c_str;
c443f58b 28
f323e906 29use pbs_datastore::catalog::BackupCatalogWriter;
770a36e5 30
032cd1b8 31use crate::pxar::metadata::errno_is_unsupported;
5444fa94 32use crate::pxar::Flags;
7eacdc76 33use crate::pxar::tools::assert_single_path_component;
c443f58b 34
77486a60
FG
35/// Pxar options for creating a pxar archive/stream
36#[derive(Default, Clone)]
37pub struct PxarCreateOptions {
38 /// Device/mountpoint st_dev numbers that should be included. None for no limitation.
39 pub device_set: Option<HashSet<u64>>,
40 /// Exclusion patterns
41 pub patterns: Vec<MatchEntry>,
42 /// Maximum number of entries to hold in memory
43 pub entries_max: usize,
44 /// Skip lost+found directory
45 pub skip_lost_and_found: bool,
46 /// Verbose output
47 pub verbose: bool,
48}
49
50
c443f58b
WB
51fn detect_fs_type(fd: RawFd) -> Result<i64, Error> {
52 let mut fs_stat = std::mem::MaybeUninit::uninit();
53 let res = unsafe { libc::fstatfs(fd, fs_stat.as_mut_ptr()) };
54 Errno::result(res)?;
55 let fs_stat = unsafe { fs_stat.assume_init() };
56
57 Ok(fs_stat.f_type)
58}
59
92ef0b56
DM
60fn strip_ascii_whitespace(line: &[u8]) -> &[u8] {
61 let line = match line.iter().position(|&b| !b.is_ascii_whitespace()) {
62 Some(n) => &line[n..],
63 None => return &[],
64 };
65 match line.iter().rev().position(|&b| !b.is_ascii_whitespace()) {
66 Some(n) => &line[..(line.len() - n)],
67 None => &[],
68 }
69}
70
a8e2940f 71#[rustfmt::skip]
c443f58b 72pub fn is_virtual_file_system(magic: i64) -> bool {
25877d05 73 use proxmox_sys::linux::magic::*;
c443f58b 74
a6bd6698 75 matches!(magic, BINFMTFS_MAGIC |
c443f58b
WB
76 CGROUP2_SUPER_MAGIC |
77 CGROUP_SUPER_MAGIC |
78 CONFIGFS_MAGIC |
79 DEBUGFS_MAGIC |
80 DEVPTS_SUPER_MAGIC |
81 EFIVARFS_MAGIC |
82 FUSE_CTL_SUPER_MAGIC |
83 HUGETLBFS_MAGIC |
84 MQUEUE_MAGIC |
85 NFSD_MAGIC |
86 PROC_SUPER_MAGIC |
87 PSTOREFS_MAGIC |
88 RPCAUTH_GSSMAGIC |
89 SECURITYFS_MAGIC |
90 SELINUX_MAGIC |
91 SMACK_MAGIC |
a6bd6698 92 SYSFS_MAGIC)
c443f58b
WB
93}
94
95#[derive(Debug)]
96struct ArchiveError {
97 path: PathBuf,
98 error: Error,
99}
100
101impl ArchiveError {
102 fn new(path: PathBuf, error: Error) -> Self {
103 Self { path, error }
104 }
105}
106
107impl std::error::Error for ArchiveError {}
108
109impl fmt::Display for ArchiveError {
110 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
111 write!(f, "error at {:?}: {}", self.path, self.error)
112 }
113}
114
115#[derive(Eq, PartialEq, Hash)]
116struct HardLinkInfo {
117 st_dev: u64,
118 st_ino: u64,
119}
120
47208b41
WB
121/// TODO: make a builder for the create_archive call for fewer parameters and add a method to add a
122/// logger which does not write to stderr.
123struct Logger;
124
125impl std::io::Write for Logger {
126 fn write(&mut self, data: &[u8]) -> io::Result<usize> {
127 std::io::stderr().write(data)
128 }
129
130 fn flush(&mut self) -> io::Result<()> {
131 std::io::stderr().flush()
132 }
133}
134
135/// And the error case.
3d68536f
WB
136struct ErrorReporter;
137
138impl std::io::Write for ErrorReporter {
139 fn write(&mut self, data: &[u8]) -> io::Result<usize> {
140 std::io::stderr().write(data)
141 }
142
143 fn flush(&mut self) -> io::Result<()> {
144 std::io::stderr().flush()
145 }
146}
147
6afb60ab 148struct Archiver {
5444fa94
WB
149 feature_flags: Flags,
150 fs_feature_flags: Flags,
c443f58b 151 fs_magic: i64,
3d68536f 152 patterns: Vec<MatchEntry>,
6afb60ab
SR
153 callback: Box<dyn FnMut(&Path) -> Result<(), Error> + Send>,
154 catalog: Option<Arc<Mutex<dyn BackupCatalogWriter + Send>>>,
c443f58b
WB
155 path: PathBuf,
156 entry_counter: usize,
157 entry_limit: usize,
158 current_st_dev: libc::dev_t,
159 device_set: Option<HashSet<u64>>,
160 hardlinks: HashMap<HardLinkInfo, (PathBuf, LinkOffset)>,
3d68536f 161 errors: ErrorReporter,
47208b41 162 logger: Logger,
a8e2940f 163 file_copy_buffer: Vec<u8>,
c443f58b
WB
164}
165
6afb60ab 166type Encoder<'a, T> = pxar::encoder::aio::Encoder<'a, T>;
c443f58b 167
6afb60ab 168pub async fn create_archive<T, F>(
c443f58b
WB
169 source_dir: Dir,
170 mut writer: T,
5444fa94 171 feature_flags: Flags,
6afb60ab
SR
172 callback: F,
173 catalog: Option<Arc<Mutex<dyn BackupCatalogWriter + Send>>>,
77486a60 174 options: PxarCreateOptions,
c443f58b
WB
175) -> Result<(), Error>
176where
6afb60ab
SR
177 T: SeqWrite + Send,
178 F: FnMut(&Path) -> Result<(), Error> + Send + 'static,
c443f58b
WB
179{
180 let fs_magic = detect_fs_type(source_dir.as_raw_fd())?;
181 if is_virtual_file_system(fs_magic) {
182 bail!("refusing to backup a virtual file system");
183 }
184
5667b763 185 let mut fs_feature_flags = Flags::from_magic(fs_magic);
c443f58b
WB
186
187 let stat = nix::sys::stat::fstat(source_dir.as_raw_fd())?;
188 let metadata = get_metadata(
189 source_dir.as_raw_fd(),
190 &stat,
191 feature_flags & fs_feature_flags,
192 fs_magic,
5667b763 193 &mut fs_feature_flags,
c443f58b
WB
194 )
195 .map_err(|err| format_err!("failed to get metadata for source directory: {}", err))?;
196
77486a60 197 let mut device_set = options.device_set.clone();
c443f58b
WB
198 if let Some(ref mut set) = device_set {
199 set.insert(stat.st_dev);
200 }
201
6afb60ab 202 let mut encoder = Encoder::new(&mut writer, &metadata).await?;
c443f58b 203
340c0bf9 204 let mut patterns = options.patterns;
77486a60
FG
205
206 if options.skip_lost_and_found {
239e49f9 207 patterns.push(MatchEntry::parse_pattern(
bf7e2a46 208 "lost+found",
c443f58b
WB
209 PatternFlag::PATH_NAME,
210 MatchType::Exclude,
211 )?);
212 }
213
214 let mut archiver = Archiver {
215 feature_flags,
216 fs_feature_flags,
217 fs_magic,
6afb60ab 218 callback: Box::new(callback),
3d68536f 219 patterns,
c443f58b
WB
220 catalog,
221 path: PathBuf::new(),
222 entry_counter: 0,
77486a60 223 entry_limit: options.entries_max,
c443f58b
WB
224 current_st_dev: stat.st_dev,
225 device_set,
226 hardlinks: HashMap::new(),
3d68536f 227 errors: ErrorReporter,
47208b41 228 logger: Logger,
a8e2940f 229 file_copy_buffer: vec::undefined(4 * 1024 * 1024),
c443f58b
WB
230 };
231
6afb60ab
SR
232 archiver.archive_dir_contents(&mut encoder, source_dir, true).await?;
233 encoder.finish().await?;
c443f58b
WB
234 Ok(())
235}
236
237struct FileListEntry {
238 name: CString,
239 path: PathBuf,
240 stat: FileStat,
241}
242
6afb60ab 243impl Archiver {
5444fa94
WB
244 /// Get the currently effective feature flags. (Requested flags masked by the file system
245 /// feature flags).
246 fn flags(&self) -> Flags {
c443f58b
WB
247 self.feature_flags & self.fs_feature_flags
248 }
249
250 fn wrap_err(&self, err: Error) -> Error {
251 if err.downcast_ref::<ArchiveError>().is_some() {
252 err
253 } else {
254 ArchiveError::new(self.path.clone(), err).into()
255 }
256 }
257
6afb60ab
SR
258 fn archive_dir_contents<'a, 'b, T: SeqWrite + Send>(
259 &'a mut self,
260 encoder: &'a mut Encoder<'b, T>,
30140886
WB
261 mut dir: Dir,
262 is_root: bool,
6afb60ab
SR
263 ) -> BoxFuture<'a, Result<(), Error>> {
264 async move {
265 let entry_counter = self.entry_counter;
c443f58b 266
6afb60ab
SR
267 let old_patterns_count = self.patterns.len();
268 self.read_pxar_excludes(dir.as_raw_fd())?;
3d68536f 269
6afb60ab 270 let mut file_list = self.generate_directory_file_list(&mut dir, is_root)?;
fa4bcbca 271
6afb60ab
SR
272 if is_root && old_patterns_count > 0 {
273 file_list.push(FileListEntry {
274 name: CString::new(".pxarexclude-cli").unwrap(),
275 path: PathBuf::new(),
276 stat: unsafe { std::mem::zeroed() },
277 });
278 }
c443f58b 279
6afb60ab 280 let dir_fd = dir.as_raw_fd();
c443f58b 281
6afb60ab 282 let old_path = std::mem::take(&mut self.path);
3d68536f 283
6afb60ab
SR
284 for file_entry in file_list {
285 let file_name = file_entry.name.to_bytes();
3d68536f 286
6afb60ab
SR
287 if is_root && file_name == b".pxarexclude-cli" {
288 self.encode_pxarexclude_cli(encoder, &file_entry.name, old_patterns_count).await?;
289 continue;
290 }
3d68536f 291
6afb60ab
SR
292 (self.callback)(&file_entry.path)?;
293 self.path = file_entry.path;
294 self.add_entry(encoder, dir_fd, &file_entry.name, &file_entry.stat).await
295 .map_err(|err| self.wrap_err(err))?;
296 }
297 self.path = old_path;
298 self.entry_counter = entry_counter;
299 self.patterns.truncate(old_patterns_count);
3d68536f 300
6afb60ab
SR
301 Ok(())
302 }.boxed()
3d68536f
WB
303 }
304
305 /// openat() wrapper which allows but logs `EACCES` and turns `ENOENT` into `None`.
7d0754a6
WB
306 ///
307 /// The `existed` flag is set when iterating through a directory to note that we know the file
308 /// is supposed to exist and we should warn if it doesnt'.
3d68536f
WB
309 fn open_file(
310 &mut self,
311 parent: RawFd,
312 file_name: &CStr,
313 oflags: OFlag,
7d0754a6 314 existed: bool,
3d68536f 315 ) -> Result<Option<Fd>, Error> {
e51be338
WB
316 // common flags we always want to use:
317 let oflags = oflags | OFlag::O_CLOEXEC | OFlag::O_NOCTTY;
318
30c3c5d6
WB
319 let mut noatime = OFlag::O_NOATIME;
320 loop {
321 return match Fd::openat(
322 &unsafe { RawFdNum::from_raw_fd(parent) },
323 file_name,
324 oflags | noatime,
325 Mode::empty(),
326 ) {
327 Ok(fd) => Ok(Some(fd)),
328 Err(nix::Error::Sys(Errno::ENOENT)) => {
329 if existed {
330 self.report_vanished_file()?;
331 }
332 Ok(None)
7d0754a6 333 }
30c3c5d6
WB
334 Err(nix::Error::Sys(Errno::EACCES)) => {
335 writeln!(self.errors, "failed to open file: {:?}: access denied", file_name)?;
336 Ok(None)
337 }
338 Err(nix::Error::Sys(Errno::EPERM)) if !noatime.is_empty() => {
339 // Retry without O_NOATIME:
340 noatime = OFlag::empty();
341 continue;
342 }
343 Err(other) => Err(Error::from(other)),
3d68536f 344 }
3d68536f
WB
345 }
346 }
347
348 fn read_pxar_excludes(&mut self, parent: RawFd) -> Result<(), Error> {
32a4695c
WB
349 let fd = match self.open_file(parent, c_str!(".pxarexclude"), OFlag::O_RDONLY, false)? {
350 Some(fd) => fd,
351 None => return Ok(()),
352 };
3d68536f
WB
353
354 let old_pattern_count = self.patterns.len();
355
b25deec0
WB
356 let path_bytes = self.path.as_os_str().as_bytes();
357
32a4695c
WB
358 let file = unsafe { std::fs::File::from_raw_fd(fd.into_raw_fd()) };
359
360 use io::BufRead;
361 for line in io::BufReader::new(file).split(b'\n') {
362 let line = match line {
363 Ok(line) => line,
364 Err(err) => {
365 let _ = writeln!(
366 self.errors,
367 "ignoring .pxarexclude after read error in {:?}: {}",
368 self.path,
369 err,
370 );
371 self.patterns.truncate(old_pattern_count);
372 return Ok(());
373 }
374 };
3d68536f 375
2b7f8dd5 376 let line = strip_ascii_whitespace(&line);
3d68536f 377
32a4695c
WB
378 if line.is_empty() || line[0] == b'#' {
379 continue;
380 }
3d68536f 381
32a4695c 382 let mut buf;
8d841f81 383 let (line, mode, anchored) = if line[0] == b'/' {
32a4695c
WB
384 buf = Vec::with_capacity(path_bytes.len() + 1 + line.len());
385 buf.extend(path_bytes);
386 buf.extend(line);
8d841f81 387 (&buf[..], MatchType::Exclude, true)
32a4695c
WB
388 } else if line.starts_with(b"!/") {
389 // inverted case with absolute path
390 buf = Vec::with_capacity(path_bytes.len() + line.len());
391 buf.extend(path_bytes);
392 buf.extend(&line[1..]); // without the '!'
8d841f81 393 (&buf[..], MatchType::Include, true)
32a4695c 394 } else if line.starts_with(b"!") {
8d841f81 395 (&line[1..], MatchType::Include, false)
32a4695c 396 } else {
8d841f81 397 (line, MatchType::Exclude, false)
32a4695c
WB
398 };
399
400 match MatchEntry::parse_pattern(line, PatternFlag::PATH_NAME, mode) {
8d841f81
WB
401 Ok(pattern) => {
402 if anchored {
403 self.patterns.push(pattern.add_flags(MatchFlag::ANCHORED));
404 } else {
405 self.patterns.push(pattern);
406 }
407 }
32a4695c
WB
408 Err(err) => {
409 let _ = writeln!(self.errors, "bad pattern in {:?}: {}", self.path, err);
3d68536f
WB
410 }
411 }
412 }
c443f58b
WB
413
414 Ok(())
415 }
416
6afb60ab 417 async fn encode_pxarexclude_cli<T: SeqWrite + Send>(
30140886 418 &mut self,
6afb60ab 419 encoder: &mut Encoder<'_, T>,
30140886 420 file_name: &CStr,
c9097ff8 421 patterns_count: usize,
30140886 422 ) -> Result<(), Error> {
c9097ff8 423 let content = generate_pxar_excludes_cli(&self.patterns[..patterns_count]);
6afb60ab
SR
424 if let Some(ref catalog) = self.catalog {
425 catalog.lock().unwrap().add_file(file_name, content.len() as u64, 0)?;
30140886
WB
426 }
427
428 let mut metadata = Metadata::default();
429 metadata.stat.mode = pxar::format::mode::IFREG | 0o600;
430
6afb60ab
SR
431 let mut file = encoder.create_file(&metadata, ".pxarexclude-cli", content.len() as u64).await?;
432 file.write_all(&content).await?;
30140886
WB
433
434 Ok(())
435 }
436
437 fn generate_directory_file_list(
438 &mut self,
439 dir: &mut Dir,
440 is_root: bool,
441 ) -> Result<Vec<FileListEntry>, Error> {
c443f58b
WB
442 let dir_fd = dir.as_raw_fd();
443
444 let mut file_list = Vec::new();
445
446 for file in dir.iter() {
447 let file = file?;
448
449 let file_name = file.file_name().to_owned();
450 let file_name_bytes = file_name.to_bytes();
451 if file_name_bytes == b"." || file_name_bytes == b".." {
452 continue;
453 }
454
30140886
WB
455 if is_root && file_name_bytes == b".pxarexclude-cli" {
456 continue;
457 }
458
c443f58b 459 let os_file_name = OsStr::from_bytes(file_name_bytes);
7eacdc76 460 assert_single_path_component(os_file_name)?;
c443f58b
WB
461 let full_path = self.path.join(os_file_name);
462
463 let stat = match nix::sys::stat::fstatat(
464 dir_fd,
465 file_name.as_c_str(),
466 nix::fcntl::AtFlags::AT_SYMLINK_NOFOLLOW,
467 ) {
468 Ok(stat) => stat,
469 Err(ref err) if err.not_found() => continue,
470 Err(err) => bail!("stat failed on {:?}: {}", full_path, err),
471 };
472
874bd545 473 let match_path = PathBuf::from("/").join(full_path.clone());
c443f58b 474 if self
239e49f9 475 .patterns
874bd545 476 .matches(match_path.as_os_str().as_bytes(), Some(stat.st_mode as u32))
c443f58b
WB
477 == Some(MatchType::Exclude)
478 {
479 continue;
480 }
481
482 self.entry_counter += 1;
483 if self.entry_counter > self.entry_limit {
484 bail!("exceeded allowed number of file entries (> {})",self.entry_limit);
485 }
486
487 file_list.push(FileListEntry {
488 name: file_name,
489 path: full_path,
490 stat
491 });
492 }
493
494 file_list.sort_unstable_by(|a, b| a.name.cmp(&b.name));
495
496 Ok(file_list)
497 }
498
3d68536f 499 fn report_vanished_file(&mut self) -> Result<(), Error> {
5afa0755 500 writeln!(self.errors, "warning: file vanished while reading: {:?}", self.path)?;
3d68536f
WB
501 Ok(())
502 }
503
a8e2940f 504 fn report_file_shrunk_while_reading(&mut self) -> Result<(), Error> {
5afa0755 505 writeln!(
a8e2940f
WB
506 self.errors,
507 "warning: file size shrunk while reading: {:?}, file will be padded with zeros!",
508 self.path,
509 )?;
510 Ok(())
511 }
512
513 fn report_file_grew_while_reading(&mut self) -> Result<(), Error> {
5afa0755 514 writeln!(
a8e2940f
WB
515 self.errors,
516 "warning: file size increased while reading: {:?}, file will be truncated!",
517 self.path,
518 )?;
519 Ok(())
520 }
521
6afb60ab 522 async fn add_entry<T: SeqWrite + Send>(
c443f58b 523 &mut self,
6afb60ab 524 encoder: &mut Encoder<'_, T>,
c443f58b
WB
525 parent: RawFd,
526 c_file_name: &CStr,
527 stat: &FileStat,
528 ) -> Result<(), Error> {
529 use pxar::format::mode;
530
531 let file_mode = stat.st_mode & libc::S_IFMT;
1008a69a 532 let open_mode = if file_mode == libc::S_IFREG || file_mode == libc::S_IFDIR {
c443f58b 533 OFlag::empty()
1008a69a
WB
534 } else {
535 OFlag::O_PATH
c443f58b
WB
536 };
537
3d68536f
WB
538 let fd = self.open_file(
539 parent,
c443f58b 540 c_file_name,
e51be338 541 open_mode | OFlag::O_RDONLY | OFlag::O_NOFOLLOW,
7d0754a6 542 true,
c443f58b
WB
543 )?;
544
3d68536f
WB
545 let fd = match fd {
546 Some(fd) => fd,
7d0754a6 547 None => return Ok(()),
3d68536f
WB
548 };
549
5667b763 550 let metadata = get_metadata(fd.as_raw_fd(), &stat, self.flags(), self.fs_magic, &mut self.fs_feature_flags)?;
c443f58b
WB
551
552 if self
239e49f9 553 .patterns
c443f58b
WB
554 .matches(self.path.as_os_str().as_bytes(), Some(stat.st_mode as u32))
555 == Some(MatchType::Exclude)
556 {
557 return Ok(());
558 }
559
560 let file_name: &Path = OsStr::from_bytes(c_file_name.to_bytes()).as_ref();
561 match metadata.file_type() {
562 mode::IFREG => {
563 let link_info = HardLinkInfo {
564 st_dev: stat.st_dev,
565 st_ino: stat.st_ino,
566 };
567
568 if stat.st_nlink > 1 {
569 if let Some((path, offset)) = self.hardlinks.get(&link_info) {
6afb60ab
SR
570 if let Some(ref catalog) = self.catalog {
571 catalog.lock().unwrap().add_hardlink(c_file_name)?;
c443f58b
WB
572 }
573
6afb60ab 574 encoder.add_hardlink(file_name, path, *offset).await?;
c443f58b
WB
575
576 return Ok(());
577 }
578 }
579
580 let file_size = stat.st_size as u64;
6afb60ab
SR
581 if let Some(ref catalog) = self.catalog {
582 catalog.lock().unwrap().add_file(c_file_name, file_size, stat.st_mtime)?;
c443f58b
WB
583 }
584
585 let offset: LinkOffset =
6afb60ab 586 self.add_regular_file(encoder, fd, file_name, &metadata, file_size).await?;
c443f58b
WB
587
588 if stat.st_nlink > 1 {
589 self.hardlinks.insert(link_info, (self.path.clone(), offset));
590 }
591
592 Ok(())
593 }
594 mode::IFDIR => {
595 let dir = Dir::from_fd(fd.into_raw_fd())?;
9321bbd1 596
6afb60ab
SR
597 if let Some(ref catalog) = self.catalog {
598 catalog.lock().unwrap().start_directory(c_file_name)?;
9321bbd1 599 }
6afb60ab
SR
600 let result = self.add_directory(encoder, dir, c_file_name, &metadata, stat).await;
601 if let Some(ref catalog) = self.catalog {
602 catalog.lock().unwrap().end_directory()?;
9321bbd1
WB
603 }
604 result
c443f58b
WB
605 }
606 mode::IFSOCK => {
6afb60ab
SR
607 if let Some(ref catalog) = self.catalog {
608 catalog.lock().unwrap().add_socket(c_file_name)?;
c443f58b
WB
609 }
610
6afb60ab 611 Ok(encoder.add_socket(&metadata, file_name).await?)
c443f58b
WB
612 }
613 mode::IFIFO => {
6afb60ab
SR
614 if let Some(ref catalog) = self.catalog {
615 catalog.lock().unwrap().add_fifo(c_file_name)?;
c443f58b
WB
616 }
617
6afb60ab 618 Ok(encoder.add_fifo(&metadata, file_name).await?)
c443f58b
WB
619 }
620 mode::IFLNK => {
6afb60ab
SR
621 if let Some(ref catalog) = self.catalog {
622 catalog.lock().unwrap().add_symlink(c_file_name)?;
c443f58b
WB
623 }
624
6afb60ab 625 self.add_symlink(encoder, fd, file_name, &metadata).await
c443f58b
WB
626 }
627 mode::IFBLK => {
6afb60ab
SR
628 if let Some(ref catalog) = self.catalog {
629 catalog.lock().unwrap().add_block_device(c_file_name)?;
c443f58b
WB
630 }
631
6afb60ab 632 self.add_device(encoder, file_name, &metadata, &stat).await
c443f58b
WB
633 }
634 mode::IFCHR => {
6afb60ab
SR
635 if let Some(ref catalog) = self.catalog {
636 catalog.lock().unwrap().add_char_device(c_file_name)?;
c443f58b
WB
637 }
638
6afb60ab 639 self.add_device(encoder, file_name, &metadata, &stat).await
c443f58b
WB
640 }
641 other => bail!(
642 "encountered unknown file type: 0x{:x} (0o{:o})",
643 other,
644 other
645 ),
646 }
647 }
648
6afb60ab 649 async fn add_directory<T: SeqWrite + Send>(
c443f58b 650 &mut self,
6afb60ab 651 encoder: &mut Encoder<'_, T>,
c443f58b
WB
652 dir: Dir,
653 dir_name: &CStr,
654 metadata: &Metadata,
655 stat: &FileStat,
656 ) -> Result<(), Error> {
657 let dir_name = OsStr::from_bytes(dir_name.to_bytes());
658
6afb60ab 659 let mut encoder = encoder.create_directory(dir_name, &metadata).await?;
c443f58b
WB
660
661 let old_fs_magic = self.fs_magic;
662 let old_fs_feature_flags = self.fs_feature_flags;
663 let old_st_dev = self.current_st_dev;
664
665 let mut skip_contents = false;
666 if old_st_dev != stat.st_dev {
667 self.fs_magic = detect_fs_type(dir.as_raw_fd())?;
5444fa94 668 self.fs_feature_flags = Flags::from_magic(self.fs_magic);
c443f58b
WB
669 self.current_st_dev = stat.st_dev;
670
671 if is_virtual_file_system(self.fs_magic) {
672 skip_contents = true;
673 } else if let Some(set) = &self.device_set {
674 skip_contents = !set.contains(&stat.st_dev);
675 }
676 }
677
678 let result = if skip_contents {
47208b41 679 writeln!(self.logger, "skipping mount point: {:?}", self.path)?;
c443f58b
WB
680 Ok(())
681 } else {
6afb60ab 682 self.archive_dir_contents(&mut encoder, dir, false).await
c443f58b
WB
683 };
684
685 self.fs_magic = old_fs_magic;
686 self.fs_feature_flags = old_fs_feature_flags;
687 self.current_st_dev = old_st_dev;
688
6afb60ab 689 encoder.finish().await?;
c443f58b
WB
690 result
691 }
692
6afb60ab 693 async fn add_regular_file<T: SeqWrite + Send>(
c443f58b 694 &mut self,
6afb60ab 695 encoder: &mut Encoder<'_, T>,
c443f58b
WB
696 fd: Fd,
697 file_name: &Path,
698 metadata: &Metadata,
699 file_size: u64,
700 ) -> Result<LinkOffset, Error> {
701 let mut file = unsafe { std::fs::File::from_raw_fd(fd.into_raw_fd()) };
a8e2940f 702 let mut remaining = file_size;
6afb60ab 703 let mut out = encoder.create_file(metadata, file_name, file_size).await?;
a8e2940f 704 while remaining != 0 {
3c2dd8ad 705 let mut got = match file.read(&mut self.file_copy_buffer[..]) {
e144810d 706 Ok(0) => break,
3c2dd8ad
DC
707 Ok(got) => got,
708 Err(err) if err.kind() == std::io::ErrorKind::Interrupted => continue,
709 Err(err) => bail!(err),
710 };
a8e2940f
WB
711 if got as u64 > remaining {
712 self.report_file_grew_while_reading()?;
713 got = remaining as usize;
714 }
6afb60ab 715 out.write_all(&self.file_copy_buffer[..got]).await?;
a8e2940f
WB
716 remaining -= got as u64;
717 }
718 if remaining > 0 {
719 self.report_file_shrunk_while_reading()?;
720 let to_zero = remaining.min(self.file_copy_buffer.len() as u64) as usize;
721 vec::clear(&mut self.file_copy_buffer[..to_zero]);
722 while remaining != 0 {
723 let fill = remaining.min(self.file_copy_buffer.len() as u64) as usize;
6afb60ab 724 out.write_all(&self.file_copy_buffer[..fill]).await?;
a8e2940f
WB
725 remaining -= fill as u64;
726 }
727 }
728
729 Ok(out.file_offset())
c443f58b
WB
730 }
731
6afb60ab 732 async fn add_symlink<T: SeqWrite + Send>(
c443f58b 733 &mut self,
6afb60ab 734 encoder: &mut Encoder<'_, T>,
c443f58b
WB
735 fd: Fd,
736 file_name: &Path,
737 metadata: &Metadata,
738 ) -> Result<(), Error> {
739 let dest = nix::fcntl::readlinkat(fd.as_raw_fd(), &b""[..])?;
6afb60ab 740 encoder.add_symlink(metadata, file_name, dest).await?;
c443f58b
WB
741 Ok(())
742 }
743
6afb60ab 744 async fn add_device<T: SeqWrite + Send>(
c443f58b 745 &mut self,
6afb60ab 746 encoder: &mut Encoder<'_, T>,
c443f58b
WB
747 file_name: &Path,
748 metadata: &Metadata,
749 stat: &FileStat,
750 ) -> Result<(), Error> {
751 Ok(encoder.add_device(
752 metadata,
753 file_name,
754 pxar::format::Device::from_dev_t(stat.st_rdev),
6afb60ab 755 ).await?)
c443f58b
WB
756 }
757}
758
5667b763 759fn get_metadata(fd: RawFd, stat: &FileStat, flags: Flags, fs_magic: i64, fs_feature_flags: &mut Flags) -> Result<Metadata, Error> {
c443f58b
WB
760 // required for some of these
761 let proc_path = Path::new("/proc/self/fd/").join(fd.to_string());
762
c443f58b
WB
763 let mut meta = Metadata {
764 stat: pxar::Stat {
765 mode: u64::from(stat.st_mode),
766 flags: 0,
767 uid: stat.st_uid,
768 gid: stat.st_gid,
b31cdec2 769 mtime: pxar::format::StatxTimestamp::new(stat.st_mtime, stat.st_mtime_nsec as u32),
c443f58b
WB
770 },
771 ..Default::default()
772 };
773
5667b763 774 get_xattr_fcaps_acl(&mut meta, fd, &proc_path, flags, fs_feature_flags)?;
c443f58b
WB
775 get_chattr(&mut meta, fd)?;
776 get_fat_attr(&mut meta, fd, fs_magic)?;
777 get_quota_project_id(&mut meta, fd, flags, fs_magic)?;
778 Ok(meta)
779}
780
5667b763 781fn get_fcaps(meta: &mut Metadata, fd: RawFd, flags: Flags, fs_feature_flags: &mut Flags) -> Result<(), Error> {
591b120d 782 if !flags.contains(Flags::WITH_FCAPS) {
c443f58b
WB
783 return Ok(());
784 }
785
786 match xattr::fgetxattr(fd, xattr::xattr_name_fcaps()) {
787 Ok(data) => {
788 meta.fcaps = Some(pxar::format::FCaps { data });
789 Ok(())
790 }
791 Err(Errno::ENODATA) => Ok(()),
5667b763
DC
792 Err(Errno::EOPNOTSUPP) => {
793 fs_feature_flags.remove(Flags::WITH_FCAPS);
794 Ok(())
795 }
c443f58b
WB
796 Err(Errno::EBADF) => Ok(()), // symlinks
797 Err(err) => bail!("failed to read file capabilities: {}", err),
798 }
799}
800
801fn get_xattr_fcaps_acl(
802 meta: &mut Metadata,
803 fd: RawFd,
804 proc_path: &Path,
5444fa94 805 flags: Flags,
5667b763 806 fs_feature_flags: &mut Flags,
c443f58b 807) -> Result<(), Error> {
591b120d 808 if !flags.contains(Flags::WITH_XATTRS) {
c443f58b
WB
809 return Ok(());
810 }
811
812 let xattrs = match xattr::flistxattr(fd) {
813 Ok(names) => names,
5667b763
DC
814 Err(Errno::EOPNOTSUPP) => {
815 fs_feature_flags.remove(Flags::WITH_XATTRS);
816 return Ok(());
817 },
c443f58b
WB
818 Err(Errno::EBADF) => return Ok(()), // symlinks
819 Err(err) => bail!("failed to read xattrs: {}", err),
820 };
821
822 for attr in &xattrs {
823 if xattr::is_security_capability(&attr) {
5667b763 824 get_fcaps(meta, fd, flags, fs_feature_flags)?;
c443f58b
WB
825 continue;
826 }
827
828 if xattr::is_acl(&attr) {
5667b763 829 get_acl(meta, proc_path, flags, fs_feature_flags)?;
c443f58b
WB
830 continue;
831 }
832
833 if !xattr::is_valid_xattr_name(&attr) {
834 continue;
835 }
836
837 match xattr::fgetxattr(fd, attr) {
838 Ok(data) => meta
839 .xattrs
840 .push(pxar::format::XAttr::new(attr.to_bytes(), data)),
841 Err(Errno::ENODATA) => (), // it got removed while we were iterating...
842 Err(Errno::EOPNOTSUPP) => (), // shouldn't be possible so just ignore this
843 Err(Errno::EBADF) => (), // symlinks, shouldn't be able to reach this either
844 Err(err) => bail!("error reading extended attribute {:?}: {}", attr, err),
845 }
846 }
847
848 Ok(())
849}
850
851fn get_chattr(metadata: &mut Metadata, fd: RawFd) -> Result<(), Error> {
032cd1b8 852 let mut attr: libc::c_long = 0;
c443f58b
WB
853
854 match unsafe { fs::read_attr_fd(fd, &mut attr) } {
855 Ok(_) => (),
856 Err(nix::Error::Sys(errno)) if errno_is_unsupported(errno) => {
857 return Ok(());
858 }
859 Err(err) => bail!("failed to read file attributes: {}", err),
860 }
861
032cd1b8 862 metadata.stat.flags |= Flags::from_chattr(attr).bits();
c443f58b
WB
863
864 Ok(())
865}
866
867fn get_fat_attr(metadata: &mut Metadata, fd: RawFd, fs_magic: i64) -> Result<(), Error> {
25877d05 868 use proxmox_sys::linux::magic::*;
c443f58b
WB
869
870 if fs_magic != MSDOS_SUPER_MAGIC && fs_magic != FUSE_SUPER_MAGIC {
871 return Ok(());
872 }
873
874 let mut attr: u32 = 0;
875
876 match unsafe { fs::read_fat_attr_fd(fd, &mut attr) } {
877 Ok(_) => (),
878 Err(nix::Error::Sys(errno)) if errno_is_unsupported(errno) => {
879 return Ok(());
880 }
881 Err(err) => bail!("failed to read fat attributes: {}", err),
882 }
883
5444fa94 884 metadata.stat.flags |= Flags::from_fat_attr(attr).bits();
c443f58b
WB
885
886 Ok(())
887}
888
889/// Read the quota project id for an inode, supported on ext4/XFS/FUSE/ZFS filesystems
890fn get_quota_project_id(
891 metadata: &mut Metadata,
892 fd: RawFd,
5444fa94 893 flags: Flags,
c443f58b
WB
894 magic: i64,
895) -> Result<(), Error> {
896 if !(metadata.is_dir() || metadata.is_regular_file()) {
897 return Ok(());
898 }
899
591b120d 900 if !flags.contains(Flags::WITH_QUOTA_PROJID) {
c443f58b
WB
901 return Ok(());
902 }
903
25877d05 904 use proxmox_sys::linux::magic::*;
c443f58b
WB
905
906 match magic {
907 EXT4_SUPER_MAGIC | XFS_SUPER_MAGIC | FUSE_SUPER_MAGIC | ZFS_SUPER_MAGIC => (),
908 _ => return Ok(()),
909 }
910
911 let mut fsxattr = fs::FSXAttr::default();
912 let res = unsafe { fs::fs_ioc_fsgetxattr(fd, &mut fsxattr) };
913
914 // On some FUSE filesystems it can happen that ioctl is not supported.
915 // For these cases projid is set to 0 while the error is ignored.
916 if let Err(err) = res {
917 let errno = err
918 .as_errno()
919 .ok_or_else(|| format_err!("error while reading quota project id"))?;
920 if errno_is_unsupported(errno) {
921 return Ok(());
922 } else {
923 bail!("error while reading quota project id ({})", errno);
924 }
925 }
926
927 let projid = fsxattr.fsx_projid as u64;
928 if projid != 0 {
929 metadata.quota_project_id = Some(pxar::format::QuotaProjectId { projid });
930 }
931 Ok(())
932}
933
5667b763 934fn get_acl(metadata: &mut Metadata, proc_path: &Path, flags: Flags, fs_feature_flags: &mut Flags) -> Result<(), Error> {
591b120d 935 if !flags.contains(Flags::WITH_ACL) {
c443f58b
WB
936 return Ok(());
937 }
938
939 if metadata.is_symlink() {
940 return Ok(());
941 }
942
5667b763 943 get_acl_do(metadata, proc_path, acl::ACL_TYPE_ACCESS, fs_feature_flags)?;
c443f58b
WB
944
945 if metadata.is_dir() {
5667b763 946 get_acl_do(metadata, proc_path, acl::ACL_TYPE_DEFAULT, fs_feature_flags)?;
c443f58b
WB
947 }
948
949 Ok(())
950}
951
952fn get_acl_do(
953 metadata: &mut Metadata,
954 proc_path: &Path,
955 acl_type: acl::ACLType,
5667b763 956 fs_feature_flags: &mut Flags,
c443f58b
WB
957) -> Result<(), Error> {
958 // In order to be able to get ACLs with type ACL_TYPE_DEFAULT, we have
959 // to create a path for acl_get_file(). acl_get_fd() only allows to get
960 // ACL_TYPE_ACCESS attributes.
961 let acl = match acl::ACL::get_file(&proc_path, acl_type) {
962 Ok(acl) => acl,
963 // Don't bail if underlying endpoint does not support acls
5667b763
DC
964 Err(Errno::EOPNOTSUPP) => {
965 fs_feature_flags.remove(Flags::WITH_ACL);
966 return Ok(());
967 }
c443f58b
WB
968 // Don't bail if the endpoint cannot carry acls
969 Err(Errno::EBADF) => return Ok(()),
970 // Don't bail if there is no data
971 Err(Errno::ENODATA) => return Ok(()),
972 Err(err) => bail!("error while reading ACL - {}", err),
973 };
974
975 process_acl(metadata, acl, acl_type)
976}
977
978fn process_acl(
979 metadata: &mut Metadata,
980 acl: acl::ACL,
981 acl_type: acl::ACLType,
982) -> Result<(), Error> {
983 use pxar::format::acl as pxar_acl;
984 use pxar::format::acl::{Group, GroupObject, Permissions, User};
985
986 let mut acl_user = Vec::new();
987 let mut acl_group = Vec::new();
988 let mut acl_group_obj = None;
989 let mut acl_default = None;
990 let mut user_obj_permissions = None;
991 let mut group_obj_permissions = None;
992 let mut other_permissions = None;
993 let mut mask_permissions = None;
994
995 for entry in &mut acl.entries() {
996 let tag = entry.get_tag_type()?;
997 let permissions = entry.get_permissions()?;
998 match tag {
999 acl::ACL_USER_OBJ => user_obj_permissions = Some(Permissions(permissions)),
1000 acl::ACL_GROUP_OBJ => group_obj_permissions = Some(Permissions(permissions)),
1001 acl::ACL_OTHER => other_permissions = Some(Permissions(permissions)),
1002 acl::ACL_MASK => mask_permissions = Some(Permissions(permissions)),
1003 acl::ACL_USER => {
1004 acl_user.push(User {
1005 uid: entry.get_qualifier()?,
1006 permissions: Permissions(permissions),
1007 });
1008 }
1009 acl::ACL_GROUP => {
1010 acl_group.push(Group {
1011 gid: entry.get_qualifier()?,
1012 permissions: Permissions(permissions),
1013 });
1014 }
1015 _ => bail!("Unexpected ACL tag encountered!"),
1016 }
1017 }
1018
1019 acl_user.sort();
1020 acl_group.sort();
1021
1022 match acl_type {
1023 acl::ACL_TYPE_ACCESS => {
1024 // The mask permissions are mapped to the stat group permissions
1025 // in case that the ACL group permissions were set.
1026 // Only in that case we need to store the group permissions,
1027 // in the other cases they are identical to the stat group permissions.
1028 if let (Some(gop), true) = (group_obj_permissions, mask_permissions.is_some()) {
1029 acl_group_obj = Some(GroupObject { permissions: gop });
1030 }
1031
1032 metadata.acl.users = acl_user;
1033 metadata.acl.groups = acl_group;
9f40e09d 1034 metadata.acl.group_obj = acl_group_obj;
c443f58b
WB
1035 }
1036 acl::ACL_TYPE_DEFAULT => {
1037 if user_obj_permissions != None
1038 || group_obj_permissions != None
1039 || other_permissions != None
1040 || mask_permissions != None
1041 {
1042 acl_default = Some(pxar_acl::Default {
1043 // The value is set to UINT64_MAX as placeholder if one
1044 // of the permissions is not set
1045 user_obj_permissions: user_obj_permissions.unwrap_or(Permissions::NO_MASK),
1046 group_obj_permissions: group_obj_permissions.unwrap_or(Permissions::NO_MASK),
1047 other_permissions: other_permissions.unwrap_or(Permissions::NO_MASK),
1048 mask_permissions: mask_permissions.unwrap_or(Permissions::NO_MASK),
1049 });
1050 }
1051
1052 metadata.acl.default_users = acl_user;
1053 metadata.acl.default_groups = acl_group;
9f40e09d 1054 metadata.acl.default = acl_default;
c443f58b
WB
1055 }
1056 _ => bail!("Unexpected ACL type encountered"),
1057 }
1058
c443f58b
WB
1059 Ok(())
1060}
239e49f9
WB
1061
1062/// Note that our pattern lists are "positive". `MatchType::Include` means the file is included.
1063/// Since we are generating an *exclude* list, we need to invert this, so includes get a `'!'`
1064/// prefix.
1065fn generate_pxar_excludes_cli(patterns: &[MatchEntry]) -> Vec<u8> {
8d841f81 1066 use pathpatterns::MatchPattern;
239e49f9
WB
1067
1068 let mut content = Vec::new();
1069
1070 for pattern in patterns {
1071 match pattern.match_type() {
1072 MatchType::Include => content.push(b'!'),
1073 MatchType::Exclude => (),
1074 }
1075
1076 match pattern.pattern() {
1077 MatchPattern::Literal(lit) => content.extend(lit),
1078 MatchPattern::Pattern(pat) => content.extend(pat.pattern().to_bytes()),
1079 }
1080
1081 if pattern.match_flags() == MatchFlag::MATCH_DIRECTORIES && content.last() != Some(&b'/') {
1082 content.push(b'/');
1083 }
1084
1085 content.push(b'\n');
1086 }
1087
1088 content
1089}