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

feat(commands): Add option stdin_command to be used in CLI and config file #266

Merged
merged 15 commits into from
Sep 21, 2024
22 changes: 21 additions & 1 deletion crates/core/src/backend.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! Module for backend related functionality.
pub(crate) mod cache;
pub(crate) mod childstdout;
pub(crate) mod decrypt;
pub(crate) mod dry_run;
pub(crate) mod hotcold;
Expand All @@ -22,7 +23,7 @@
use serde_derive::{Deserialize, Serialize};

use crate::{
backend::node::Node,
backend::node::{Metadata, Node, NodeType},
error::{BackendAccessErrorKind, RusticErrorKind},
id::Id,
RusticResult,
Expand Down Expand Up @@ -424,6 +425,17 @@
pub open: Option<O>,
}

impl<O> ReadSourceEntry<O> {
fn from_path(path: PathBuf, open: Option<O>) -> Self {
let node = Node::new_node(
path.file_name().unwrap(),
aawsome marked this conversation as resolved.
Show resolved Hide resolved
NodeType::File,
Metadata::default(),

Check warning on line 433 in crates/core/src/backend.rs

View check run for this annotation

Codecov / codecov/patch

crates/core/src/backend.rs#L433

Added line #L433 was not covered by tests
);
Self { path, node, open }
}
}

/// Trait for backends that can read and open sources.
/// This trait is implemented by all backends that can read data and open from a source.
pub trait ReadSourceOpen {
Expand All @@ -442,6 +454,14 @@
fn open(self) -> RusticResult<Self::Reader>;
}

/// blanket implementation for readers
impl<T: Read + Send + 'static> ReadSourceOpen for T {
type Reader = T;
fn open(self) -> RusticResult<Self::Reader> {
Ok(self)

Check warning on line 461 in crates/core/src/backend.rs

View check run for this annotation

Codecov / codecov/patch

crates/core/src/backend.rs#L460-L461

Added lines #L460 - L461 were not covered by tests
}
}

/// Trait for backends that can read from a source.
///
/// This trait is implemented by all backends that can read data from a source.
Expand Down
73 changes: 73 additions & 0 deletions crates/core/src/backend/childstdout.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
use std::{
iter::{once, Once},
path::PathBuf,
process::{Child, ChildStdout, Command, Stdio},
sync::Mutex,
};

use crate::{
backend::{ReadSource, ReadSourceEntry},
error::{RepositoryErrorKind, RusticResult},
CommandInput,
};

/// The `ChildSource` is a `ReadSource` when spawning a child process and reading its stdout
simonsan marked this conversation as resolved.
Show resolved Hide resolved
#[derive(Debug)]
pub struct ChildStdoutSource {
/// The path of the stdin entry.
path: PathBuf,
/// The child process
// Note: this is in a Mutex as we want to take out ChildStdout in the `entries` method - but this method only gets a reference of self.
simonsan marked this conversation as resolved.
Show resolved Hide resolved
process: Mutex<Child>,
/// the command which is called
command: CommandInput,
}

impl ChildStdoutSource {
/// Creates a new `ChildSource`.
pub fn new(cmd: &CommandInput, path: PathBuf) -> RusticResult<Self> {
let process = Command::new(cmd.command())
.args(cmd.args())
.stdout(Stdio::piped())
.spawn()
.map_err(|err| {

Check warning on line 33 in crates/core/src/backend/childstdout.rs

View check run for this annotation

Codecov / codecov/patch

crates/core/src/backend/childstdout.rs#L33

Added line #L33 was not covered by tests
RepositoryErrorKind::CommandExecutionFailed(
"stdin-command".into(),
"call".into(),
err,
)
.into()
});

let process = cmd.on_failure().display_result(process)?;

Ok(Self {
path,
process: Mutex::new(process),
command: cmd.clone(),
})
}

/// Finishes the `ChildSource`
pub fn finish(self) -> RusticResult<()> {
let status = self.process.lock().unwrap().wait();
self.command
.on_failure()
.handle_status(status, "stdin-command", "call")?;
Ok(())
}
}

impl ReadSource for ChildStdoutSource {
type Open = ChildStdout;
type Iter = Once<RusticResult<ReadSourceEntry<ChildStdout>>>;

fn size(&self) -> RusticResult<Option<u64>> {
Ok(None)

Check warning on line 66 in crates/core/src/backend/childstdout.rs

View check run for this annotation

Codecov / codecov/patch

crates/core/src/backend/childstdout.rs#L65-L66

Added lines #L65 - L66 were not covered by tests
}

fn entries(&self) -> Self::Iter {
let open = self.process.lock().unwrap().stdout.take();
once(Ok(ReadSourceEntry::from_path(self.path.clone(), open)))
}
}
60 changes: 11 additions & 49 deletions crates/core/src/backend/stdin.rs
Original file line number Diff line number Diff line change
@@ -1,51 +1,33 @@
use std::{io::stdin, path::PathBuf};
use std::{
io::{stdin, Stdin},
iter::{once, Once},
path::PathBuf,
};

use crate::{
backend::{
node::{Metadata, Node, NodeType},
ReadSource, ReadSourceEntry, ReadSourceOpen,
},
backend::{ReadSource, ReadSourceEntry},
error::RusticResult,
};

/// The `StdinSource` is a `ReadSource` for stdin.
#[derive(Debug, Clone)]
pub struct StdinSource {
/// Whether we have already yielded the stdin entry.
finished: bool,
/// The path of the stdin entry.
path: PathBuf,
}

impl StdinSource {
/// Creates a new `StdinSource`.
pub const fn new(path: PathBuf) -> Self {
Self {
finished: false,
path,
}
}
}

/// The `OpenStdin` is a `ReadSourceOpen` for stdin.
#[derive(Debug, Copy, Clone)]
pub struct OpenStdin();

impl ReadSourceOpen for OpenStdin {
/// The reader type.
type Reader = std::io::Stdin;

/// Opens stdin.
fn open(self) -> RusticResult<Self::Reader> {
Ok(stdin())
Self { path }
}
}

impl ReadSource for StdinSource {
/// The open type.
type Open = OpenStdin;
type Open = Stdin;
/// The iterator type.
type Iter = Self;
type Iter = Once<RusticResult<ReadSourceEntry<Stdin>>>;

/// Returns the size of the source.
fn size(&self) -> RusticResult<Option<u64>> {
Expand All @@ -54,27 +36,7 @@

/// Returns an iterator over the source.
fn entries(&self) -> Self::Iter {
self.clone()
}
}

impl Iterator for StdinSource {
type Item = RusticResult<ReadSourceEntry<OpenStdin>>;

fn next(&mut self) -> Option<Self::Item> {
if self.finished {
return None;
}
self.finished = true;

Some(Ok(ReadSourceEntry {
path: self.path.clone(),
node: Node::new_node(
self.path.file_name().unwrap(),
NodeType::File,
Metadata::default(),
),
open: Some(OpenStdin()),
}))
let open = Some(stdin());
once(Ok(ReadSourceEntry::from_path(self.path.clone(), open)))

Check warning on line 40 in crates/core/src/backend/stdin.rs

View check run for this annotation

Codecov / codecov/patch

crates/core/src/backend/stdin.rs#L39-L40

Added lines #L39 - L40 were not covered by tests
}
}
38 changes: 29 additions & 9 deletions crates/core/src/commands/backup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use crate::{
archiver::{parent::Parent, Archiver},
backend::{
childstdout::ChildStdoutSource,
dry_run::DryRunBackend,
ignore::{LocalSource, LocalSourceFilterOptions, LocalSourceSaveOptions},
stdin::StdinSource,
Expand All @@ -23,6 +24,7 @@
PathList, SnapshotFile,
},
repository::{IndexedIds, IndexedTree, Repository},
CommandInput,
};

#[cfg(feature = "clap")]
Expand Down Expand Up @@ -141,6 +143,10 @@
#[cfg_attr(feature = "merge", merge(skip))]
pub stdin_filename: String,

/// Call the given command and use its output as stdin
#[cfg_attr(feature = "clap", clap(long, value_name = "COMMAND"))]
pub stdin_command: Option<CommandInput>,

/// Manually set backup path in snapshot
#[cfg_attr(feature = "clap", clap(long, value_name = "PATH", value_hint = ValueHint::DirPath))]
pub as_path: Option<PathBuf>,
Expand Down Expand Up @@ -246,15 +252,29 @@

let snap = if backup_stdin {
let path = &backup_path[0];
let src = StdinSource::new(path.clone());
archiver.archive(
&src,
path,
as_path.as_ref(),
opts.parent_opts.skip_identical_parent,
opts.no_scan,
&p,
)?
if let Some(command) = &opts.stdin_command {
let src = ChildStdoutSource::new(command, path.clone())?;
let res = archiver.archive(
&src,
path,
as_path.as_ref(),

Check warning on line 260 in crates/core/src/commands/backup.rs

View check run for this annotation

Codecov / codecov/patch

crates/core/src/commands/backup.rs#L258-L260

Added lines #L258 - L260 were not covered by tests
opts.parent_opts.skip_identical_parent,
opts.no_scan,
&p,

Check warning on line 263 in crates/core/src/commands/backup.rs

View check run for this annotation

Codecov / codecov/patch

crates/core/src/commands/backup.rs#L263

Added line #L263 was not covered by tests
)?;
src.finish()?;
res
} else {
let src = StdinSource::new(path.clone());
archiver.archive(
&src,
path,
as_path.as_ref(),
opts.parent_opts.skip_identical_parent,
opts.no_scan,
&p,

Check warning on line 275 in crates/core/src/commands/backup.rs

View check run for this annotation

Codecov / codecov/patch

crates/core/src/commands/backup.rs#L268-L275

Added lines #L268 - L275 were not covered by tests
)?
}
} else {
let src = LocalSource::new(
opts.ignore_save_opts,
Expand Down
23 changes: 16 additions & 7 deletions crates/core/src/repository/command_input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,20 +165,29 @@

impl OnFailure {
fn eval<T>(self, res: RusticResult<T>) -> RusticResult<Option<T>> {
match res {
Err(err) => match self {
let res = self.display_result(res);
match (res, self) {
(Err(err), Self::Error) => Err(err),
(Err(_), _) => Ok(None),

Check warning on line 171 in crates/core/src/repository/command_input.rs

View check run for this annotation

Codecov / codecov/patch

crates/core/src/repository/command_input.rs#L170-L171

Added lines #L170 - L171 were not covered by tests
(Ok(res), _) => Ok(Some(res)),
}
}

/// Displays a result depending on the defined error handling which still yielding the same result
// This can be used where an error might occur, but in that case we have to abort.
simonsan marked this conversation as resolved.
Show resolved Hide resolved
pub fn display_result<T>(self, res: RusticResult<T>) -> RusticResult<T> {
if let Err(err) = &res {
match self {

Check warning on line 180 in crates/core/src/repository/command_input.rs

View check run for this annotation

Codecov / codecov/patch

crates/core/src/repository/command_input.rs#L180

Added line #L180 was not covered by tests
Self::Error => {
error!("{err}");
Err(err)
}
Self::Warn => {
warn!("{err}");
Ok(None)
}
Self::Ignore => Ok(None),
},
Ok(res) => Ok(Some(res)),
Self::Ignore => {}

Check warning on line 187 in crates/core/src/repository/command_input.rs

View check run for this annotation

Codecov / codecov/patch

crates/core/src/repository/command_input.rs#L187

Added line #L187 was not covered by tests
}
}
res
}

/// Handle a status of a called command depending on the defined error handling
Expand Down
38 changes: 34 additions & 4 deletions crates/core/tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ use insta::{
use pretty_assertions::assert_eq;
use rstest::{fixture, rstest};
use rustic_core::{
repofile::SnapshotFile, BackupOptions, CheckOptions, ConfigOptions, FindMatches, FindNode,
FullIndex, IndexedFull, IndexedStatus, KeyOptions, LimitOption, LsOptions, NoProgressBars,
OpenStatus, ParentOptions, PathList, Repository, RepositoryBackends, RepositoryOptions,
RusticResult, SnapshotGroupCriterion, SnapshotOptions, StringList,
repofile::SnapshotFile, BackupOptions, CheckOptions, CommandInput, ConfigOptions, FindMatches,
FindNode, FullIndex, IndexedFull, IndexedStatus, KeyOptions, LimitOption, LsOptions,
NoProgressBars, OpenStatus, ParentOptions, PathList, Repository, RepositoryBackends,
RepositoryOptions, RusticResult, SnapshotGroupCriterion, SnapshotOptions, StringList,
};
use rustic_core::{
repofile::{Metadata, Node},
Expand Down Expand Up @@ -368,6 +368,36 @@ fn test_backup_dry_run_with_tar_gz_passes(
Ok(())
}

#[rstest]
fn test_backup_stdin_command(
set_up_repo: Result<RepoOpen>,
insta_snapshotfile_redaction: Settings,
) -> Result<()> {
// Fixtures
let repo = set_up_repo?.to_indexed_ids()?;
let paths = PathList::from_string("-")?;

let cmd: CommandInput = "echo test".parse()?;
let opts = BackupOptions::default()
.stdin_filename("test")
.stdin_command(cmd);
// backup data from cmd
let snapshot = repo.backup(&opts, &paths, SnapshotFile::default())?;
insta_snapshotfile_redaction.bind(|| {
assert_with_win("stdin-command-summary", &snapshot);
});

// re-read index
let repo = repo.to_indexed()?;

// check content
let node = repo.node_from_snapshot_path("latest:test", |_| true)?;
let mut content = Vec::new();
repo.dump(&node, &mut content)?;
assert_eq!(content, b"test\n");
Ok(())
}

#[rstest]
fn test_ls(
tar_gz_testdata: Result<TestSource>,
Expand Down
Loading