Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for mounting and unmounting archives and repositories #4

Merged
merged 4 commits into from
May 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/asynchronous/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ pub use compact::compact;
pub use create::{create, create_progress, CreateProgress};
pub use init::init;
pub use list::list;
pub use mount::{mount, umount};
pub use prune::prune;

mod compact;
mod create;
mod init;
mod list;
mod mount;
mod prune;

pub(crate) async fn execute_borg(
Expand Down
46 changes: 46 additions & 0 deletions src/asynchronous/mount.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
use log::{debug, info};

use crate::asynchronous::execute_borg;
use crate::common::{mount_fmt_args, mount_parse_output, CommonOptions, MountOptions};
use crate::errors::MountError;

/// Mount an archive or repo as a FUSE filesystem.
///
/// **Parameter**:
/// - `options`: Reference to [MountOptions]
/// - `common_options`: The [CommonOptions] that can be applied to any command
pub async fn mount(
options: &MountOptions,
common_options: &CommonOptions,
) -> Result<(), MountError> {
let local_path = common_options.local_path.as_ref().map_or("borg", |x| x);

let args = mount_fmt_args(options, common_options);
debug!("Calling borg: {local_path} {args}");
let args = shlex::split(&args).ok_or(MountError::ShlexError)?;
let res = execute_borg(local_path, args, &options.passphrase).await?;

mount_parse_output(res)?;

info!("Finished mounting");

Ok(())
}

/// Unmount a previously mounted archive or repository.
///
/// **Parameter**:
/// - `mountpoint`: The mountpoint to be unmounted.
/// - `common_options`: The [CommonOptions] that can be applied to any command
pub async fn umount(mountpoint: String, common_options: &CommonOptions) -> Result<(), MountError> {
let local_path = common_options.local_path.as_ref().map_or("borg", |x| x);

let args = vec!["umount".to_string(), mountpoint];
let res = execute_borg(local_path, args, &None).await?;

mount_parse_output(res)?;

info!("Finished mounting");

Ok(())
}
222 changes: 215 additions & 7 deletions src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use std::process::Output;
use log::{debug, error, info, trace, warn};
use serde::{Deserialize, Serialize};

use crate::errors::{CompactError, CreateError, InitError, ListError, PruneError};
use crate::errors::{CompactError, CreateError, InitError, ListError, MountError, PruneError};
use crate::output::create::Create;
use crate::output::list::ListRepository;
use crate::output::logging::{LevelName, LoggingMessage, MessageId};
Expand Down Expand Up @@ -397,6 +397,79 @@ impl PruneOptions {
}
}

/// Options for [crate::sync::mount]
///
/// Mount an archive or repository as a FUSE filesystem. This is useful for
/// browsing archives or repositories and interactive restoration.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum MountSource {
/// Mount a repository
Repository {
/// Name of the repository you wish to mount
///
/// Example values:
/// - `/tmp/foo`
/// - `user@example.com:/opt/repo`
/// - `ssh://user@example.com:2323:/opt/repo`
name: String,
/// Obtain the first N archives
first_n_archives: Option<NonZeroU16>,
/// Obtain the last N archives
last_n_archives: Option<NonZeroU16>,
/// only consider archive names matching the glob.
glob_archives: Option<String>,
},
/// Mount an archive (repo_name::archive_name)
Archive {
/// Path to the borg archive you wish to mount
///
/// Example values:
/// - `/tmp/foo::my-archive`
/// - `user@example.com:/opt/repo::archive`
/// - `ssh://user@example.com:2323:/opt/repo`
archive_name: String,
},
}

/// Options for [crate::sync::mount]
///
/// Mount an archive or repository as a FUSE filesystem. This is useful for
/// browsing archives or repositories and interactive restoration.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct MountOptions {
/// The archive or repo you wish to mount
pub mount_source: MountSource,
/// The path where the repo or archive will be mounted.
///
/// Example values:
/// - `/mnt/my-borg-backup`
pub mountpoint: String,
/// The passphrase for the repository
///
/// If using a repository with [EncryptionMode::None],
/// you can leave this option empty
pub passphrase: Option<String>,
/// Paths to extract. If left empty all paths will be present. Useful
/// for whitelisting certain paths in the backup.
///
/// Example values:
/// - `/a/path/I/actually/care/about`
/// - `**/some/intermediate/folder/*`
pub select_paths: Vec<Pattern>,
}

impl MountOptions {
/// Create an new [MountOptions]
pub fn new(mount_source: MountSource, mountpoint: String) -> Self {
Self {
mount_source,
mountpoint,
passphrase: None,
select_paths: vec![],
}
}
}

/// Options for [crate::sync::compact]
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct CompactOptions {
Expand Down Expand Up @@ -633,7 +706,7 @@ pub(crate) fn init_parse_result(res: Output) -> Result<(), InitError> {

trace!("borg output: {line}");

let log_msg: LoggingMessage = serde_json::from_str(&line)?;
let log_msg = LoggingMessage::from_str(&line)?;

if let LoggingMessage::LogMessage {
name,
Expand Down Expand Up @@ -702,7 +775,7 @@ pub(crate) fn prune_parse_output(res: Output) -> Result<(), PruneError> {

trace!("borg output: {line}");

let log_msg: LoggingMessage = serde_json::from_str(&line)?;
let log_msg = LoggingMessage::from_str(&line)?;

if let LoggingMessage::LogMessage {
name,
Expand All @@ -729,6 +802,88 @@ pub(crate) fn prune_parse_output(res: Output) -> Result<(), PruneError> {
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}",
mount_source = mount_source_formatted,
mountpoint = options.mountpoint,
select_paths = options
.select_paths
.iter()
.map(|x| format!(" --pattern={}", shlex::quote(&x.to_string()),))
.collect::<Vec<String>>()
.join(" "),
)
}

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));
}
}
myOmikron marked this conversation as resolved.
Show resolved Hide resolved
}
}

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}",
Expand All @@ -750,7 +905,7 @@ pub(crate) fn list_parse_output(res: Output) -> Result<ListRepository, ListError

trace!("borg output: {line}");

let log_msg: LoggingMessage = serde_json::from_str(&line)?;
let log_msg = LoggingMessage::from_str(&line)?;

if let LoggingMessage::LogMessage {
name,
Expand Down Expand Up @@ -846,7 +1001,7 @@ pub(crate) fn create_parse_output(res: Output) -> Result<Create, CreateError> {

trace!("borg output: {line}");

let log_msg: LoggingMessage = serde_json::from_str(&line)?;
let log_msg = LoggingMessage::from_str(&line)?;

if let LoggingMessage::LogMessage {
name,
Expand Down Expand Up @@ -907,7 +1062,7 @@ pub(crate) fn compact_parse_output(res: Output) -> Result<(), CompactError> {

trace!("borg output: {line}");

let log_msg: LoggingMessage = serde_json::from_str(&line)?;
let log_msg = LoggingMessage::from_str(&line)?;

if let LoggingMessage::LogMessage {
name,
Expand Down Expand Up @@ -938,7 +1093,10 @@ pub(crate) fn compact_parse_output(res: Output) -> Result<(), CompactError> {
mod tests {
use std::num::NonZeroU16;

use crate::common::{prune_fmt_args, CommonOptions, PruneOptions};
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());
Expand All @@ -952,4 +1110,54 @@ mod tests {
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
);
}
}
Loading