use std::fmt::{Display, Formatter, Write};
use std::io::BufRead;
use std::num::NonZeroU16;
use std::process::Output;
use log::{debug, error, info, trace, warn};
use serde::{Deserialize, Serialize};
use crate::errors::{
CompactError, CreateError, ExtractError, InitError, ListError, MountError, PruneError,
};
use crate::output::create::Create;
use crate::output::list::ListRepository;
use crate::output::logging::{LevelName, LoggingMessage, MessageId};
use crate::utils::shell_escape;
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum PatternInstruction {
Root(String),
Include(Pattern),
Exclude(Pattern),
ExcludeNoRecurse(Pattern),
}
impl Display for PatternInstruction {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
PatternInstruction::Root(path) => write!(f, "P {path}"),
PatternInstruction::Include(pattern) => write!(f, "+ {pattern}"),
PatternInstruction::Exclude(pattern) => write!(f, "- {pattern}"),
PatternInstruction::ExcludeNoRecurse(pattern) => write!(f, "! {pattern}"),
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum Pattern {
FnMatch(String),
Shell(String),
Regex(String),
PathPrefix(String),
PathFullMatch(String),
}
impl Display for Pattern {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Pattern::FnMatch(x) => write!(f, "fm:{x}"),
Pattern::Shell(x) => write!(f, "sh:{x}"),
Pattern::Regex(x) => write!(f, "re:{x}"),
Pattern::PathPrefix(x) => write!(f, "pp:{x}"),
Pattern::PathFullMatch(x) => write!(f, "pf:{x}"),
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
pub enum CompressionMode {
None,
Lz4,
Zstd(u8),
Zlib(u8),
Lzma(u8),
}
impl Display for CompressionMode {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
CompressionMode::None => write!(f, "none"),
CompressionMode::Lz4 => write!(f, "lz4"),
CompressionMode::Zstd(x) => write!(f, "zstd,{x}"),
CompressionMode::Zlib(x) => write!(f, "zlib,{x}"),
CompressionMode::Lzma(x) => write!(f, "lzma,{x}"),
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum EncryptionMode {
None,
Authenticated(String),
AuthenticatedBlake2(String),
Repokey(String),
Keyfile(String),
RepokeyBlake2(String),
KeyfileBlake2(String),
}
impl EncryptionMode {
pub(crate) fn get_passphrase(&self) -> Option<String> {
match self {
EncryptionMode::None => None,
EncryptionMode::Authenticated(x) => Some(x.clone()),
EncryptionMode::AuthenticatedBlake2(x) => Some(x.clone()),
EncryptionMode::Repokey(x) => Some(x.clone()),
EncryptionMode::Keyfile(x) => Some(x.clone()),
EncryptionMode::RepokeyBlake2(x) => Some(x.clone()),
EncryptionMode::KeyfileBlake2(x) => Some(x.clone()),
}
}
}
impl Display for EncryptionMode {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
EncryptionMode::None => write!(f, "none"),
EncryptionMode::Authenticated(_) => write!(f, "authenticated"),
EncryptionMode::AuthenticatedBlake2(_) => write!(f, "authenticated-blake2"),
EncryptionMode::Repokey(_) => write!(f, "repokey"),
EncryptionMode::Keyfile(_) => write!(f, "keyfile"),
EncryptionMode::RepokeyBlake2(_) => write!(f, "repokey-blake2"),
EncryptionMode::KeyfileBlake2(_) => write!(f, "keyfile-blake2"),
}
}
}
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
pub struct CommonOptions {
pub local_path: Option<String>,
pub remote_path: Option<String>,
pub upload_ratelimit: Option<u64>,
pub rsh: Option<String>,
}
impl From<&CommonOptions> for String {
fn from(value: &CommonOptions) -> Self {
let mut s = String::new();
if let Some(rsh) = &value.rsh {
s = format!("{s} --rsh {} ", shell_escape(rsh));
}
if let Some(remote_path) = &value.remote_path {
s = format!("{s} --remote-path {} ", shell_escape(remote_path));
}
if let Some(upload_ratelimit) = &value.upload_ratelimit {
s = format!("{s} --upload-ratelimit {upload_ratelimit} ");
}
s
}
}
#[derive(Serialize, Deserialize, Copy, Clone, Debug)]
pub enum PruneWithinTime {
Hour,
Day,
Week,
Month,
Year,
}
impl Display for PruneWithinTime {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
PruneWithinTime::Hour => write!(f, "H"),
PruneWithinTime::Day => write!(f, "d"),
PruneWithinTime::Week => write!(f, "w"),
PruneWithinTime::Month => write!(f, "m"),
PruneWithinTime::Year => write!(f, "y"),
}
}
}
#[derive(Serialize, Deserialize, Copy, Clone, Debug)]
pub struct PruneWithin {
pub quantifier: NonZeroU16,
pub time: PruneWithinTime,
}
impl Display for PruneWithin {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}{}", self.quantifier, self.time)
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct PruneOptions {
pub repository: String,
pub passphrase: Option<String>,
pub keep_within: Option<PruneWithin>,
pub keep_secondly: Option<NonZeroU16>,
pub keep_minutely: Option<NonZeroU16>,
pub keep_hourly: Option<NonZeroU16>,
pub keep_daily: Option<NonZeroU16>,
pub keep_weekly: Option<NonZeroU16>,
pub keep_monthly: Option<NonZeroU16>,
pub keep_yearly: Option<NonZeroU16>,
pub checkpoint_interval: Option<NonZeroU16>,
pub glob_archives: Option<String>,
}
impl PruneOptions {
pub fn new(repository: String) -> Self {
Self {
repository,
passphrase: None,
keep_within: None,
keep_secondly: None,
keep_minutely: None,
keep_hourly: None,
keep_daily: None,
keep_weekly: None,
keep_monthly: None,
keep_yearly: None,
checkpoint_interval: None,
glob_archives: None,
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum MountSource {
Repository {
name: String,
first_n_archives: Option<NonZeroU16>,
last_n_archives: Option<NonZeroU16>,
glob_archives: Option<String>,
},
Archive {
archive_name: String,
},
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct MountOptions {
pub mount_source: MountSource,
pub mountpoint: String,
pub passphrase: Option<String>,
pub select_paths: Vec<Pattern>,
}
impl MountOptions {
pub fn new(mount_source: MountSource, mountpoint: String) -> Self {
Self {
mount_source,
mountpoint,
passphrase: None,
select_paths: vec![],
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct CompactOptions {
pub repository: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct CreateOptions {
pub repository: String,
pub archive: String,
pub passphrase: Option<String>,
pub comment: Option<String>,
pub compression: Option<CompressionMode>,
pub paths: Vec<String>,
pub exclude_caches: bool,
pub patterns: Vec<PatternInstruction>,
pub pattern_file: Option<String>,
pub excludes: Vec<Pattern>,
pub exclude_file: Option<String>,
pub numeric_ids: bool,
pub sparse: bool,
pub read_special: bool,
pub no_xattrs: bool,
pub no_acls: bool,
pub no_flags: bool,
}
impl CreateOptions {
pub fn new(
repository: String,
archive: String,
paths: Vec<String>,
patterns: Vec<PatternInstruction>,
) -> Self {
Self {
repository,
archive,
passphrase: None,
comment: None,
compression: None,
paths,
exclude_caches: false,
patterns,
pattern_file: None,
excludes: vec![],
exclude_file: None,
numeric_ids: false,
sparse: false,
read_special: false,
no_xattrs: false,
no_acls: false,
no_flags: false,
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct InitOptions {
pub repository: String,
pub encryption_mode: EncryptionMode,
pub append_only: bool,
pub make_parent_dirs: bool,
pub storage_quota: Option<String>,
}
impl InitOptions {
pub fn new(repository: String, encryption_mode: EncryptionMode) -> Self {
Self {
repository,
encryption_mode,
append_only: false,
make_parent_dirs: false,
storage_quota: None,
}
}
}
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct ListOptions {
pub repository: String,
pub passphrase: Option<String>,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct ExtractOptions {
pub repository: String,
pub archive: String,
pub destination: String,
pub passphrase: Option<String>,
pub strip_components: Option<u16>,
}
impl ExtractOptions {
pub fn new(repository: String, archive: String, destination: String) -> Self {
Self {
repository,
archive,
destination,
passphrase: None,
strip_components: None,
}
}
}
pub(crate) fn init_fmt_args(options: &InitOptions, common_options: &CommonOptions) -> String {
format!(
"--log-json {common_options}init -e {e}{append_only}{make_parent_dirs}{storage_quota} {repository}",
common_options = String::from(common_options),
e = options.encryption_mode,
append_only = if options.append_only {
" --append-only"
} else {
""
},
make_parent_dirs = if options.make_parent_dirs {
" --make-parent-dirs"
} else {
""
},
storage_quota = options
.storage_quota
.as_ref()
.map_or("".to_string(), |x| format!(
" --storage-quota {quota}",
quota = shell_escape(x)
)),
repository = shell_escape(&options.repository),
)
}
fn log_message(level_name: LevelName, time: f64, name: String, message: String) {
match level_name {
LevelName::Debug => debug!("{time} {name}: {message}"),
LevelName::Info => info!("{time} {name}: {message}"),
LevelName::Warning => warn!("{time} {name}: {message}"),
LevelName::Error => error!("{time} {name}: {message}"),
LevelName::Critical => error!("{time} {name}: {message}"),
}
}
pub(crate) fn init_parse_result(res: Output) -> Result<(), InitError> {
let Some(exit_code) = res.status.code() else {
warn!("borg process was terminated by signal");
return Err(InitError::TerminatedBySignal);
};
let mut output = String::new();
for line in res.stderr.lines() {
let line = line.map_err(InitError::InvalidBorgOutput)?;
writeln!(output, "{line}").unwrap();
trace!("borg output: {line}");
let log_msg = LoggingMessage::from_str(&line)?;
if let LoggingMessage::LogMessage {
name,
message,
level_name,
time,
msg_id,
} = log_msg
{
log_message(level_name, time, name, message);
if let Some(msg_id) = msg_id {
match msg_id {
MessageId::RepositoryAlreadyExists => {
return Err(InitError::RepositoryAlreadyExists)
}
_ => {
if exit_code > 1 {
return Err(InitError::UnexpectedMessageId(msg_id));
}
}
}
}
if let Some(MessageId::RepositoryAlreadyExists) = msg_id {
return Err(InitError::RepositoryAlreadyExists);
} else if let Some(msg_id) = msg_id {
return Err(InitError::UnexpectedMessageId(msg_id));
}
}
}
if exit_code > 1 {
return Err(InitError::Unknown(output));
}
Ok(())
}
pub(crate) fn prune_fmt_args(options: &PruneOptions, common_options: &CommonOptions) -> String {
format!(
"--log-json {common_options} prune{keep_within}{keep_secondly}{keep_minutely}{keep_hourly}{keep_daily}{keep_weekly}{keep_monthly}{keep_yearly} {repository}",
common_options = String::from(common_options),
keep_within = options.keep_within.as_ref().map_or("".to_string(), |x| format!(" --keep-within {x}")),
keep_secondly = options.keep_secondly.as_ref().map_or("".to_string(), |x| format!(" --keep-secondly {x}")),
keep_minutely = options.keep_minutely.map_or("".to_string(), |x| format!(" --keep-minutely {x}")),
keep_hourly = options.keep_hourly.map_or("".to_string(), |x| format!(" --keep-hourly {x}")),
keep_daily = options.keep_daily.map_or("".to_string(), |x| format!(" --keep-daily {x}")),
keep_weekly = options.keep_weekly.map_or("".to_string(), |x| format!(" --keep-weekly {x}")),
keep_monthly = options.keep_monthly.map_or("".to_string(), |x| format!(" --keep-monthly {x}")),
keep_yearly = options.keep_yearly.map_or("".to_string(), |x| format!(" --keep-yearly {x}")),
repository = shell_escape(&options.repository)
)
}
pub(crate) fn prune_parse_output(res: Output) -> Result<(), PruneError> {
let Some(exit_code) = res.status.code() else {
warn!("borg process was terminated by signal");
return Err(PruneError::TerminatedBySignal);
};
let mut output = String::new();
for line in BufRead::lines(res.stderr.as_slice()) {
let line = line.map_err(PruneError::InvalidBorgOutput)?;
writeln!(output, "{line}").unwrap();
trace!("borg output: {line}");
let log_msg = LoggingMessage::from_str(&line)?;
if let LoggingMessage::LogMessage {
name,
message,
level_name,
time,
msg_id,
} = log_msg
{
log_message(level_name, time, name, message);
if let Some(msg_id) = msg_id {
if exit_code > 1 {
return Err(PruneError::UnexpectedMessageId(msg_id));
}
}
}
}
if exit_code > 1 {
return Err(PruneError::Unknown(output));
}
Ok(())
}
pub(crate) fn mount_fmt_args(options: &MountOptions, common_options: &CommonOptions) -> String {
let mount_source_formatted = match &options.mount_source {
MountSource::Repository {
name,
first_n_archives,
last_n_archives,
glob_archives,
} => {
format!(
"{name}{first_n_archives}{last_n_archives}{glob_archives}",
name = name.clone(),
first_n_archives = first_n_archives
.map(|first_n| format!(" --first {}", first_n))
.unwrap_or_default(),
last_n_archives = last_n_archives
.map(|last_n| format!(" --last {}", last_n))
.unwrap_or_default(),
glob_archives = glob_archives
.as_ref()
.map(|glob| format!(" --glob-archives {}", glob))
.unwrap_or_default(),
)
}
MountSource::Archive { archive_name } => archive_name.clone(),
};
format!(
"--log-json {common_options} mount {mount_source} {mountpoint} {select_paths}",
common_options = String::from(common_options),
mount_source = mount_source_formatted,
mountpoint = options.mountpoint,
select_paths = options
.select_paths
.iter()
.map(|x| format!("--pattern={}", shell_escape(&x.to_string())))
.collect::<Vec<String>>()
.join(" "),
)
.trim()
.to_string()
}
pub(crate) fn mount_parse_output(res: Output) -> Result<(), MountError> {
let Some(exit_code) = res.status.code() else {
warn!("borg process was terminated by signal");
return Err(MountError::TerminatedBySignal);
};
let mut output = String::new();
for line in BufRead::lines(res.stderr.as_slice()) {
let line = line.map_err(MountError::InvalidBorgOutput)?;
writeln!(output, "{line}").unwrap();
trace!("borg output: {line}");
let log_msg = LoggingMessage::from_str(&line)?;
if let LoggingMessage::UMountError(message) = log_msg {
return Err(MountError::UMountError(message));
};
if let LoggingMessage::LogMessage {
name,
message,
level_name,
time,
msg_id,
} = log_msg
{
log_message(level_name, time, name, message);
if let Some(msg_id) = msg_id {
if exit_code > 1 {
return Err(MountError::UnexpectedMessageId(msg_id));
}
}
}
}
if exit_code > 1 {
return Err(MountError::Unknown(output));
}
Ok(())
}
pub(crate) fn list_fmt_args(options: &ListOptions, common_options: &CommonOptions) -> String {
format!(
"--log-json {common_options} list --json {repository}",
common_options = String::from(common_options),
repository = shell_escape(&options.repository)
)
}
pub(crate) fn list_parse_output(res: Output) -> Result<ListRepository, ListError> {
let Some(exit_code) = res.status.code() else {
warn!("borg process was terminated by signal");
return Err(ListError::TerminatedBySignal);
};
let mut output = String::new();
for line in BufRead::lines(res.stderr.as_slice()) {
let line = line.map_err(ListError::InvalidBorgOutput)?;
writeln!(output, "{line}").unwrap();
trace!("borg output: {line}");
let log_msg = LoggingMessage::from_str(&line)?;
if let LoggingMessage::LogMessage {
name,
message,
level_name,
time,
msg_id,
} = log_msg
{
log_message(level_name, time, name, message);
if let Some(msg_id) = msg_id {
match msg_id {
MessageId::RepositoryDoesNotExist => {
return Err(ListError::RepositoryDoesNotExist);
}
MessageId::PassphraseWrong => {
return Err(ListError::PassphraseWrong);
}
_ => {
if exit_code > 1 {
return Err(ListError::UnexpectedMessageId(msg_id));
}
}
}
}
}
}
if exit_code > 1 {
return Err(ListError::Unknown(output));
}
trace!("Parsing output");
let list_repo: ListRepository = serde_json::from_slice(&res.stdout)?;
Ok(list_repo)
}
pub(crate) fn extract_fmt_args(options: &ExtractOptions, common_options: &CommonOptions) -> String {
format!(
"--log-json {common_options} extract{strip_components} {repo}::{archive}",
common_options = String::from(common_options),
strip_components = options
.strip_components
.map(|n| format!(" --strip-components {}", n))
.unwrap_or_default(),
repo = shell_escape(&options.repository),
archive = shell_escape(&options.archive),
)
}
pub(crate) fn extract_parse_output(res: Output) -> Result<(), ExtractError> {
let Some(exit_code) = res.status.code() else {
warn!("borg process was terminated by signal");
return Err(ExtractError::TerminatedBySignal);
};
let mut output = String::new();
for line in BufRead::lines(res.stderr.as_slice()) {
let line = line.map_err(ExtractError::InvalidBorgOutput)?;
writeln!(output, "{line}").unwrap();
trace!("borg output: {line}");
let log_msg = LoggingMessage::from_str(&line)?;
if let LoggingMessage::LogMessage {
name,
message,
level_name,
time,
msg_id,
} = log_msg
{
log_message(level_name, time, name, message);
if let Some(msg_id) = msg_id {
if exit_code > 1 {
return Err(ExtractError::UnexpectedMessageId(msg_id));
}
}
}
}
if exit_code > 1 {
return Err(ExtractError::Unknown(output));
}
Ok(())
}
pub(crate) fn create_fmt_args(
options: &CreateOptions,
common_options: &CommonOptions,
progress: bool,
) -> String {
format!(
"--log-json{p} {common_options}create --json{comment}{compression}{num_ids}{sparse}{read_special}{no_xattr}{no_acls}{no_flags}{ex_caches}{patterns}{excludes}{pattern_file}{exclude_file} {repo}::{archive} {paths}",
common_options = String::from(common_options),
p = if progress { " --progress" } else { "" },
comment = options.comment.as_ref().map_or("".to_string(), |x| format!(
" --comment {}",
shell_escape(x)
)),
compression = options.compression.as_ref().map_or("".to_string(), |x| format!(" --compression {x}")),
num_ids = if options.numeric_ids { " --numeric-ids" } else { "" },
sparse = if options.sparse { " --sparse" } else { "" },
read_special = if options.read_special { " --read-special" } else { "" },
no_xattr = if options.no_xattrs { " --noxattrs" } else { "" },
no_acls = if options.no_acls { " --noacls" } else { "" },
no_flags = if options.no_flags { " --noflags" } else { "" },
ex_caches = if options.exclude_caches { " --exclude-caches" } else {""},
patterns = options.patterns.iter().map(|x| format!(
" --pattern={}",
shell_escape(&x.to_string()),
)).collect::<Vec<String>>().join(" "),
excludes = options.excludes.iter().map(|x| format!(
" --exclude={}",
shell_escape(&x.to_string()),
)).collect::<Vec<String>>().join(" "),
pattern_file = options.pattern_file.as_ref().map_or(
"".to_string(),
|x| format!(" --patterns-from {}", shell_escape(x)),
),
exclude_file = options.exclude_file.as_ref().map_or(
"".to_string(),
|x| format!(" --exclude-from {}", shell_escape(x)),
),
repo = shell_escape(&options.repository),
archive = shell_escape(&options.archive),
paths = options.paths.join(" "),
)
}
pub(crate) fn create_parse_output(res: Output) -> Result<Create, CreateError> {
let Some(exit_code) = res.status.code() else {
warn!("borg process was terminated by signal");
return Err(CreateError::TerminatedBySignal);
};
let mut output = String::new();
for line in BufRead::lines(res.stderr.as_slice()) {
let line = line.map_err(CreateError::InvalidBorgOutput)?;
writeln!(output, "{line}").unwrap();
trace!("borg output: {line}");
let log_msg = LoggingMessage::from_str(&line)?;
if let LoggingMessage::LogMessage {
name,
message,
level_name,
time,
msg_id,
} = log_msg
{
log_message(level_name, time, name, message);
if let Some(msg_id) = msg_id {
match msg_id {
MessageId::ArchiveAlreadyExists => {
return Err(CreateError::ArchiveAlreadyExists)
}
MessageId::PassphraseWrong => {
return Err(CreateError::PassphraseWrong);
}
_ => {
if exit_code > 1 {
return Err(CreateError::UnexpectedMessageId(msg_id));
}
}
}
}
}
}
if exit_code > 1 {
return Err(CreateError::Unknown(output));
}
trace!("Parsing stats");
let stats: Create = serde_json::from_slice(&res.stdout)?;
Ok(stats)
}
pub(crate) fn compact_fmt_args(options: &CompactOptions, common_options: &CommonOptions) -> String {
format!(
"--log-json {common_options}compact {repository}",
common_options = String::from(common_options),
repository = shell_escape(&options.repository)
)
}
pub(crate) fn compact_parse_output(res: Output) -> Result<(), CompactError> {
let Some(exit_code) = res.status.code() else {
warn!("borg process was terminated by signal");
return Err(CompactError::TerminatedBySignal);
};
let mut output = String::new();
for line in BufRead::lines(res.stderr.as_slice()) {
let line = line.map_err(CompactError::InvalidBorgOutput)?;
writeln!(output, "{line}").unwrap();
trace!("borg output: {line}");
let log_msg = LoggingMessage::from_str(&line)?;
if let LoggingMessage::LogMessage {
name,
message,
level_name,
time,
msg_id,
} = log_msg
{
log_message(level_name, time, name, message);
if let Some(msg_id) = msg_id {
if exit_code > 1 {
return Err(CompactError::UnexpectedMessageId(msg_id));
}
}
}
}
if exit_code > 1 {
return Err(CompactError::Unknown(output));
}
Ok(())
}
#[cfg(test)]
mod tests {
use std::num::NonZeroU16;
use crate::common::{
mount_fmt_args, prune_fmt_args, CommonOptions, MountOptions, MountSource, Pattern,
PruneOptions,
};
#[test]
fn test_prune_fmt_args() {
let mut prune_option = PruneOptions::new("prune_option_repo".to_string());
prune_option.keep_secondly = NonZeroU16::new(1);
prune_option.keep_minutely = NonZeroU16::new(2);
prune_option.keep_hourly = NonZeroU16::new(3);
prune_option.keep_daily = NonZeroU16::new(4);
prune_option.keep_weekly = NonZeroU16::new(5);
prune_option.keep_monthly = NonZeroU16::new(6);
prune_option.keep_yearly = NonZeroU16::new(7);
let args = prune_fmt_args(&prune_option, &CommonOptions::default());
assert_eq!("--log-json prune --keep-secondly 1 --keep-minutely 2 --keep-hourly 3 --keep-daily 4 --keep-weekly 5 --keep-monthly 6 --keep-yearly 7 'prune_option_repo'", args);
}
#[test]
fn test_mount_fmt_args() {
let mount_option = MountOptions::new(
MountSource::Archive {
archive_name: "/tmp/borg-repo::archive".to_string(),
},
String::from("/mnt/borg-mount"),
);
let args = mount_fmt_args(&mount_option, &CommonOptions::default());
assert_eq!(
"--log-json mount /tmp/borg-repo::archive /mnt/borg-mount",
args
);
}
#[test]
fn test_mount_fmt_args_patterns() {
let mut mount_option = MountOptions::new(
MountSource::Archive {
archive_name: "/my-borg-repo".to_string(),
},
String::from("/borg-mount"),
);
mount_option.select_paths = vec![
Pattern::Shell("**/test/*".to_string()),
Pattern::Regex("^[A-Z]{3}".to_string()),
];
let args = mount_fmt_args(&mount_option, &CommonOptions::default());
assert_eq!(
"--log-json mount /my-borg-repo /borg-mount --pattern='sh:**/test/*' --pattern='re:^[A-Z]{3}'",
args
);
}
#[test]
fn test_mount_fmt_args_repo() {
let mut mount_option = MountOptions::new(
MountSource::Repository {
name: "/my-repo".to_string(),
first_n_archives: Some(NonZeroU16::new(10).unwrap()),
last_n_archives: Some(NonZeroU16::new(5).unwrap()),
glob_archives: Some("archive-name*12-2022*".to_string()),
},
String::from("/borg-mount"),
);
mount_option.select_paths = vec![Pattern::Shell("**/foobar/*".to_string())];
let args = mount_fmt_args(&mount_option, &CommonOptions::default());
assert_eq!(
"--log-json mount /my-repo --first 10 --last 5 --glob-archives archive-name*12-2022* /borg-mount --pattern='sh:**/foobar/*'",
args
);
}
}