pub mod mapper;
pub use mapper::LocalSourceSaveOptions;
use std::{
ffi::OsString,
fs::File,
path::{Path, PathBuf},
};
use bytesize::ByteSize;
use derive_setters::Setters;
use ignore::{Walk, WalkBuilder};
use log::warn;
use serde_with::{DisplayFromStr, serde_as};
#[cfg(not(windows))]
use std::num::TryFromIntError;
use crate::{
Excludes,
backend::{ReadSource, ReadSourceEntry, ReadSourceOpen},
error::{ErrorKind, RusticError, RusticResult},
};
#[derive(thiserror::Error, Debug, displaydoc::Display)]
pub enum IgnoreErrorKind {
#[cfg(all(not(windows), not(target_os = "openbsd")))]
ErrorXattr {
path: PathBuf,
source: std::io::Error,
},
ErrorLink {
path: PathBuf,
source: std::io::Error,
},
#[cfg(not(windows))]
CtimeConversionToTimestampFailed {
ctime: i64,
ctime_nsec: i64,
source: TryFromIntError,
},
AcquiringMetadataFailed { name: String, source: ignore::Error },
JiffError(#[from] jiff::Error),
}
pub(crate) type IgnoreResult<T> = Result<T, IgnoreErrorKind>;
#[derive(Debug)]
pub struct LocalSource {
builder: WalkBuilder,
save_opts: LocalSourceSaveOptions,
}
#[serde_as]
#[cfg_attr(feature = "clap", derive(clap::Parser))]
#[cfg_attr(feature = "merge", derive(conflate::Merge))]
#[derive(serde::Deserialize, serde::Serialize, Default, Clone, Debug, Setters)]
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
#[setters(into)]
#[non_exhaustive]
pub struct LocalSourceFilterOptions {
#[cfg_attr(feature = "clap", clap(long))]
#[cfg_attr(feature = "merge", merge(strategy = conflate::bool::overwrite_false))]
pub git_ignore: bool,
#[cfg_attr(feature = "clap", clap(long))]
#[cfg_attr(feature = "merge", merge(strategy = conflate::bool::overwrite_false))]
pub no_require_git: bool,
#[cfg_attr(
feature = "clap",
clap(long = "custom-ignorefile", value_name = "FILE")
)]
#[cfg_attr(feature = "merge", merge(strategy = conflate::vec::overwrite_empty))]
pub custom_ignorefiles: Vec<String>,
#[cfg_attr(feature = "clap", clap(long, value_name = "FILE"))]
#[cfg_attr(feature = "merge", merge(strategy = conflate::vec::overwrite_empty))]
pub exclude_if_present: Vec<String>,
#[cfg_attr(feature = "clap", clap(long, value_name = "XATTR"))]
#[cfg_attr(feature = "merge", merge(strategy = conflate::vec::overwrite_empty))]
pub exclude_if_xattr: Vec<String>,
#[cfg_attr(feature = "clap", clap(long, short = 'x'))]
#[cfg_attr(feature = "merge", merge(strategy = conflate::bool::overwrite_false))]
pub one_file_system: bool,
#[cfg_attr(feature = "clap", clap(long, value_name = "SIZE"))]
#[serde_as(as = "Option<DisplayFromStr>")]
#[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
pub exclude_larger_than: Option<ByteSize>,
}
impl LocalSource {
#[allow(clippy::too_many_lines)]
pub fn new(
save_opts: LocalSourceSaveOptions,
excludes: &Excludes,
filter_opts: &LocalSourceFilterOptions,
backup_paths: &[impl AsRef<Path>],
) -> RusticResult<Self> {
let mut walk_builder = WalkBuilder::new(&backup_paths[0]);
for path in &backup_paths[1..] {
_ = walk_builder.add(path);
}
let overrides = excludes.as_override()?;
for file in &filter_opts.custom_ignorefiles {
_ = walk_builder.add_custom_ignore_filename(file);
}
_ = walk_builder
.follow_links(false)
.hidden(false)
.ignore(false)
.git_ignore(filter_opts.git_ignore)
.git_exclude(filter_opts.git_ignore)
.require_git(!filter_opts.no_require_git)
.sort_by_file_path(Path::cmp)
.same_file_system(filter_opts.one_file_system)
.max_filesize(filter_opts.exclude_larger_than.map(|s| s.as_u64()))
.overrides(overrides);
let exclude_if_present = filter_opts.exclude_if_present.clone();
let exclude_if_xattr: Vec<OsString> = filter_opts
.exclude_if_xattr
.iter()
.map(OsString::from)
.collect();
if !exclude_if_xattr.is_empty() {
#[cfg(any(windows, target_os = "openbsd"))]
warn!("exclude-if-xattr is not supported on this platform");
#[cfg(not(any(windows, target_os = "openbsd")))]
if !xattr::SUPPORTED_PLATFORM {
warn!("exclude-if-xattr is not supported on this platform");
}
}
let needs_entry_filter = !exclude_if_present.is_empty() || !exclude_if_xattr.is_empty();
if needs_entry_filter {
_ = walk_builder.filter_entry(move |entry| {
if !exclude_if_present.is_empty()
&& let Some(tpe) = entry.file_type()
&& tpe.is_dir()
&& exclude_if_present
.iter()
.any(|file| entry.path().join(file).exists())
{
return false;
}
#[cfg(not(any(windows, target_os = "openbsd")))]
if xattr::SUPPORTED_PLATFORM && !exclude_if_xattr.is_empty() {
match xattr::list(entry.path()) {
Ok(mut attrs) => {
if attrs.any(|attr| exclude_if_xattr.contains(&attr)) {
return false;
}
}
Err(err) => {
warn!(
"Error reading xattrs for {}, not excluding: {err}",
entry.path().display()
);
}
}
}
true
});
}
let builder = walk_builder;
Ok(Self { builder, save_opts })
}
}
#[derive(Debug)]
pub struct OpenFile(PathBuf);
impl ReadSourceOpen for OpenFile {
type Reader = File;
fn open(self) -> RusticResult<Self::Reader> {
let path = self.0;
File::open(&path).map_err(|err| {
RusticError::with_source(
ErrorKind::InputOutput,
"Failed to open file at `{path}`. Please make sure the file exists and is accessible.",
err,
)
.attach_context("path", path.display().to_string())
})
}
}
impl ReadSource for LocalSource {
type Open = OpenFile;
type Iter = LocalSourceWalker;
fn size(&self) -> RusticResult<Option<u64>> {
let mut size = 0;
for entry in self.builder.build() {
if let Err(err) = entry.and_then(|e| e.metadata()).map(|m| {
size += if m.is_dir() { 0 } else { m.len() };
}) {
warn!("ignoring error {err}");
}
}
Ok(Some(size))
}
fn entries(&self) -> Self::Iter {
LocalSourceWalker {
walker: self.builder.build(),
save_opts: self.save_opts,
}
}
}
#[allow(missing_debug_implementations)]
pub struct LocalSourceWalker {
walker: Walk,
save_opts: LocalSourceSaveOptions,
}
impl Iterator for LocalSourceWalker {
type Item = RusticResult<ReadSourceEntry<OpenFile>>;
fn next(&mut self) -> Option<Self::Item> {
match self.walker.next() {
Some(Ok(entry)) if entry.depth() == 0 && entry.file_type().unwrap().is_dir() => {
self.walker.next()
}
item => item,
}
.map(|e| {
self.save_opts
.map_entry(e.map_err(|err| {
RusticError::with_source(
ErrorKind::Internal,
"Failed to get next entry from walk iterator.",
err,
)
.ask_report()
})?)
.map_err(|err| {
RusticError::with_source(
ErrorKind::Internal,
"Failed to map Directory entry to ReadSourceEntry.",
err,
)
.ask_report()
})
})
}
}