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