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