Skip to content

Commit

Permalink
Merge pull request #50 from drdo/repository-password-cmdline-args
Browse files Browse the repository at this point in the history
Add support for repository and password files
  • Loading branch information
drdo authored Jul 29, 2024
2 parents 2e5bff7 + cad05f8 commit ba9472d
Show file tree
Hide file tree
Showing 5 changed files with 165 additions and 82 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ description = "This is like ncdu for a restic repository."
anyhow = "1"
camino = "1"
chrono = { version = "0.4", features = ["serde"] }
clap = { version = "4", features = ["derive"] }
clap = { version = "4", features = ["derive", "env"] }
crossterm = "0.27"
directories = "5"
flexi_logger = "0.28"
Expand Down
13 changes: 9 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,21 +30,24 @@ Note: it currently requires nightly to build.

# Running

You can use all the regular restic environment variables, they will be passed
to all restic subprocesses redu spawns.
You can specify the repository and the password command in exactly the same ways
that restic supports, with the exception that redu will not prompt you for the password.

For example:
For example using environment variables:
```
$ export RESTIC_REPOSITORY='sftp://my-backup-server.my-domain.net'
$ export RESTIC_PASSWORD_COMMAND='security find-generic-password -s restic -a personal -w'
$ redu
```

Alternatively, you can pass the repo and the password as arguments to redu:
Or via command line arguments:
```
redu -r 'sftp://my-backup-server.my-domain.net' --password-command 'security find-generic-password -s restic -a personal -w'
```

Note: `--repository-file` (env: `RESTIC_REPOSITORY_FILE`) and `--password-file` (env: `RESTIC_PASSWORD_FILE`)
are supported as well and work just like in restic.

# Usage
Redu keeps a cache with your file/directory sizes (per repo).
On each run it will sync the cache with the snapshots in your repo,
Expand Down Expand Up @@ -81,6 +84,8 @@ about the currently highlighted item:
You can keep navigating with the details window open and it will update as you
browse around.

Hint: you can press **Escape** to close the details window (as well as other dialogs).

### Marking files
You can mark files and directories to build up your list of things to exclude.
Keybinds
Expand Down
118 changes: 118 additions & 0 deletions src/args.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
use clap::{ArgGroup, Parser};
use log::LevelFilter;
use redu::restic::Repository;

use crate::restic::Password;

#[derive(Debug)]
pub struct Args {
pub repository: Repository,
pub password: Password,
pub parallelism: usize,
pub log_level: LevelFilter,
pub no_cache: bool,
}

impl Args {
/// Parse arguments from std::env::args_os(), exit on error.
pub fn parse() -> Self {
let cli = Cli::parse();

Args {
repository: if let Some(repo) = cli.repo {
Repository::Repo(repo)
} else if let Some(file) = cli.repository_file {
Repository::File(file)
} else {
unreachable!("Error in Config: neither repo nor repository_file found. Please open an issue if you see this.")
},
password: if let Some(command) = cli.password_command {
Password::Command(command)
} else if let Some(file) = cli.password_file {
Password::File(file)
} else {
unreachable!("Error in Config: neither password_command nor password_file found. Please open an issue if you see this.")
},
parallelism: cli.parallelism,
log_level: match cli.verbose {
0 => LevelFilter::Info,
1 => LevelFilter::Debug,
_ => LevelFilter::Trace,
},
no_cache: cli.no_cache,
}
}
}

/// This is like ncdu for a restic respository.
///
/// It computes the size for each directory/file by
/// taking the largest over all snapshots in the repository.
///
/// You can browse your repository and mark directories/files.
/// These marks are persisted across runs of redu.
///
/// When you're happy with the marks you can generate
/// a list to stdout with everything that you marked.
/// This list can be used directly as an exclude-file for restic.
///
/// Redu keeps all messages and UI in stderr,
/// only the marks list is generated to stdout.
/// This means that you can pipe redu directly to a file
/// to get the exclude-file.
///
/// NOTE: redu will never do any kind of modification to your repo.
/// It's strictly read-only.
///
/// Keybinds:
/// Arrows or hjkl: Movement
/// PgUp/PgDown or C-b/C-f: Page up / Page down
/// Enter: Details
/// Escape: Close dialog
/// m: Mark
/// u: Unmark
/// c: Clear all marks
/// g: Generate
/// q: Quit
#[derive(Parser)]
#[command(version, long_about, verbatim_doc_comment)]
#[command(group(
ArgGroup::new("repository")
.required(true)
.args(["repo", "repository_file"]),
))]
#[command(group(
ArgGroup::new("password")
.required(true)
.args(["password_command", "password_file"]),
))]
struct Cli {
#[arg(short = 'r', long, env = "RESTIC_REPOSITORY")]
repo: Option<String>,

#[arg(long, env = "RESTIC_REPOSITORY_FILE")]
repository_file: Option<String>,

#[arg(long, value_name = "COMMAND", env = "RESTIC_PASSWORD_COMMAND")]
password_command: Option<String>,

#[arg(long, value_name = "FILE", env = "RESTIC_PASSWORD_FILE")]
password_file: Option<String>,

/// How many restic subprocesses to spawn concurrently.
///
/// If you get ssh-related errors or too much memory use try lowering this.
#[arg(short = 'j', value_name = "NUMBER", default_value_t = 4)]
parallelism: usize,

/// Log verbosity level. You can pass it multiple times (maxes out at two).
#[arg(
short = 'v',
action = clap::ArgAction::Count,
)]
verbose: u8,

/// Pass the --no-cache option to restic subprocesses.
#[arg(long)]
no_cache: bool,
}
73 changes: 7 additions & 66 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ use std::{
};

use anyhow::Context;
use args::Args;
use camino::{Utf8Path, Utf8PathBuf};
use clap::{command, Parser};
use crossterm::{
event::{KeyCode, KeyModifiers},
terminal::{
Expand All @@ -25,7 +25,7 @@ use crossterm::{
ExecutableCommand,
};
use directories::ProjectDirs;
use flexi_logger::{FileSpec, LogSpecification, Logger, WriteMode};
use flexi_logger::{FileSpec, Logger, WriteMode};
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use log::{error, info, trace};
use rand::{seq::SliceRandom, thread_rng};
Expand All @@ -46,67 +46,13 @@ use util::snapshot_short_id;

use crate::ui::{Action, App, Event};

mod args;
mod ui;
mod util;

/// This is like ncdu for a restic respository.
///
/// It computes the size for each directory/file by
/// taking the largest over all snapshots in the repository.
///
/// You can browse your repository and mark directories/files.
/// These marks are persisted across runs of redu.
///
/// When you're happy with the marks you can generate
/// a list to stdout with everything that you marked.
/// This list can be used directly as an exclude-file for restic.
///
/// Redu keeps all messages and UI in stderr,
/// only the marks list is generated to stdout.
/// This means that you can pipe redu directly to a file
/// to get the exclude-file.
///
/// NOTE: redu will never do any kind of modification to your repo.
/// It's strictly read-only.
///
/// Keybinds:
/// Arrows or hjkl: Movement
/// PgUp/PgDown or C-b/C-f: Page up / Page down
/// m: Mark
/// u: Unmark
/// c: Clear all marks
/// g: Generate
/// q: Quit
#[derive(Parser)]
#[command(version, long_about, verbatim_doc_comment)]
struct Cli {
#[arg(short = 'r', long)]
repo: Option<String>,

#[arg(long)]
password_command: Option<String>,

/// How many restic subprocesses to spawn concurrently.
///
/// If you get ssh-related errors or too much memory use try lowering this.
#[arg(short = 'j', value_name = "NUMBER", default_value_t = 4)]
parallelism: usize,

/// Log verbosity level. You can pass it multiple times (maxes out at two).
#[arg(
short = 'v',
action = clap::ArgAction::Count,
)]
verbose: u8,

/// Pass the --no-cache option to restic subprocesses.
#[arg(long)]
no_cache: bool,
}

fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
let restic = Restic::new(cli.repo, cli.password_command, cli.no_cache);
let args = Args::parse();
let restic = Restic::new(args.repository, args.password, args.no_cache);

let dirs = ProjectDirs::from("eu", "drdo", "redu")
.expect("unable to determine project directory");
Expand All @@ -120,12 +66,7 @@ fn main() -> anyhow::Result<()> {
let filespec =
{ FileSpec::default().directory(directory).suppress_basename() };

let spec = match cli.verbose {
0 => LogSpecification::info(),
1 => LogSpecification::debug(),
_ => LogSpecification::trace(),
};
Logger::with(spec)
Logger::with(args.log_level)
.log_to_file(filespec)
.write_mode(WriteMode::BufferAndFlush)
.format(flexi_logger::with_thread)
Expand Down Expand Up @@ -175,7 +116,7 @@ fn main() -> anyhow::Result<()> {
}
};

sync_snapshots(&restic, &mut cache, cli.parallelism)?;
sync_snapshots(&restic, &mut cache, args.parallelism)?;

let entries = cache.get_entries(None)?;
if entries.is_empty() {
Expand Down
41 changes: 30 additions & 11 deletions src/restic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,18 +95,34 @@ pub struct Config {
}

pub struct Restic {
repo: Option<String>,
password_command: Option<String>,
repository: Repository,
password: Password,
no_cache: bool,
}

#[derive(Debug)]
pub enum Repository {
/// A repository string (restic: --repo)
Repo(String),
/// A repository file (restic: --repository-file)
File(String),
}

#[derive(Debug)]
pub enum Password {
/// A password command (restic: --password-command)
Command(String),
/// A password file (restic: --password-file)
File(String),
}

impl Restic {
pub fn new(
repo: Option<String>,
password_command: Option<String>,
repository: Repository,
password: Password,
no_cache: bool,
) -> Self {
Restic { repo, password_command, no_cache }
Restic { repository, password, no_cache }
}

pub fn config(&self) -> Result<Config, Error> {
Expand Down Expand Up @@ -192,12 +208,15 @@ impl Restic {
Ok(())
});
}
if let Some(repo) = &self.repo {
cmd.arg("--repo").arg(repo);
}
if let Some(password_command) = &self.password_command {
cmd.arg("--password-command").arg(password_command);
}
match &self.repository {
Repository::Repo(repo) => cmd.arg("--repo").arg(repo),
Repository::File(file) => cmd.arg("--repository-file").arg(file),
};
match &self.password {
Password::Command(command) =>
cmd.arg("--password-command").arg(command),
Password::File(file) => cmd.arg("--password-file").arg(file),
};
if self.no_cache {
cmd.arg("--no-cache");
}
Expand Down

0 comments on commit ba9472d

Please sign in to comment.