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