#[cfg(any(feature = "canopy-register", feature = "canopy-import"))]
use std::io::Read as _;
#[cfg(any(feature = "canopy-register", feature = "canopy-import"))]
use base64::{
Engine as _,
engine::general_purpose::{STANDARD, STANDARD_NO_PAD, URL_SAFE, URL_SAFE_NO_PAD},
};
use clap::{Parser, Subcommand};
#[cfg(any(feature = "canopy-register", feature = "canopy-import"))]
use miette::{IntoDiagnostic as _, bail, miette};
use miette::Result;
use super::Context;
#[derive(Debug, Clone, Parser)]
pub struct CanopyArgs {
#[command(subcommand)]
pub action: Action,
}
super::subcommands! {
[CanopyArgs => |args: CanopyArgs, mut ctx: Context| -> Result<(Action, Context)> {
let action = args.action.clone();
ctx.provide(args);
Ok((action, ctx))
}]
#[cfg(feature = "canopy-register")]
register => Register(RegisterArgs),
#[cfg(feature = "canopy-export")]
export => Export(ExportArgs),
#[cfg(feature = "canopy-import")]
import => Import(ImportArgs),
#[cfg(feature = "canopy-tags")]
tags => Tags(TagsArgs),
#[cfg(feature = "canopy-backup")]
backup => Backup(BackupArgs),
#[cfg(feature = "canopy-restore")]
restore => Restore(RestoreArgs),
#[cfg(feature = "canopy-restore")]
kopia => Kopia(KopiaArgs),
#[cfg(feature = "canopy-unregister")]
unregister => Unregister(UnregisterArgs)
}
#[cfg(any(feature = "canopy-register", feature = "canopy-export"))]
async fn load_registration(
config: Option<&std::path::Path>,
) -> Result<Option<bestool_canopy::registration::Registration>> {
match config {
Some(dir) => bestool_canopy::registration::load_from(dir).await,
None => bestool_canopy::registration::load().await,
}
}
#[cfg(all(
feature = "alertd",
any(feature = "canopy-register", feature = "canopy-unregister")
))]
async fn restart_daemon_for_registration_change() {
let addrs = bestool_alertd::commands::default_server_addrs();
if let Err(err) = bestool_alertd::commands::restart(&addrs).await {
tracing::debug!(%err, "alertd daemon not reachable");
println!("(alertd daemon not reachable; it will re-read the registration on next start)");
}
}
#[cfg(all(
not(feature = "alertd"),
any(feature = "canopy-register", feature = "canopy-unregister")
))]
async fn restart_daemon_for_registration_change() {}
#[cfg(all(
unix,
any(
feature = "canopy-register",
feature = "canopy-import",
feature = "canopy-unregister"
)
))]
fn ensure_writable_or_reexec(dir: &std::path::Path) -> Result<()> {
if registration_dir_writable(dir) || privilege::user::privileged() {
return Ok(());
}
tracing::info!(
dir = %dir.display(),
"registration directory is not writable; re-executing under sudo"
);
let args: Vec<String> = std::env::args().collect();
let status = std::process::Command::new("sudo")
.args(args)
.status()
.into_diagnostic()?;
std::process::exit(status.code().unwrap_or(1));
}
#[cfg(all(
not(unix),
any(
feature = "canopy-register",
feature = "canopy-import",
feature = "canopy-unregister"
)
))]
fn ensure_writable_or_reexec(_dir: &std::path::Path) -> Result<()> {
Ok(())
}
#[cfg(all(
unix,
any(
feature = "canopy-register",
feature = "canopy-import",
feature = "canopy-unregister"
)
))]
fn registration_dir_writable(dir: &std::path::Path) -> bool {
let mut candidate = dir;
let existing = loop {
if candidate.exists() {
break candidate;
}
match candidate.parent() {
Some(parent) => candidate = parent,
None => return false,
}
};
let probe = existing.join(format!(".bestool-write-test.{}", std::process::id()));
match std::fs::File::create(&probe) {
Ok(_) => {
let _ = std::fs::remove_file(&probe);
true
}
Err(_) => false,
}
}
#[cfg(any(feature = "canopy-register", feature = "canopy-import"))]
fn read_stdin(what: &str) -> Result<String> {
let mut buf = String::new();
std::io::stdin()
.read_to_string(&mut buf)
.into_diagnostic()
.map_err(|e| miette!("reading {what} from stdin: {e}"))?;
if buf.trim().is_empty() {
bail!("no {what} given on the command line or stdin");
}
Ok(buf)
}
#[cfg(any(feature = "canopy-register", feature = "canopy-import"))]
fn decode_base64(input: &str) -> Result<Vec<u8>> {
for engine in [&STANDARD, &STANDARD_NO_PAD, &URL_SAFE, &URL_SAFE_NO_PAD] {
if let Ok(bytes) = engine.decode(input) {
return Ok(bytes);
}
}
Err(miette!("input is not valid base64"))
}
#[cfg(test)]
#[cfg(any(feature = "canopy-register", feature = "canopy-import"))]
mod tests {
use super::*;
#[test]
fn decode_base64_accepts_all_variants() {
let raw = b"\x00\xff\x10hello world?!";
for encoded in [
STANDARD.encode(raw),
STANDARD_NO_PAD.encode(raw),
URL_SAFE.encode(raw),
URL_SAFE_NO_PAD.encode(raw),
] {
assert_eq!(decode_base64(&encoded).unwrap(), raw);
}
}
#[test]
fn decode_base64_rejects_garbage() {
assert!(decode_base64("not valid base64 !!!! \u{00a0}").is_err());
}
#[cfg(unix)]
#[test]
fn registration_dir_writable_true_for_writable_dir() {
let dir = std::env::temp_dir().join(format!("bestool-canopy-rw-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
assert!(registration_dir_writable(&dir), "fresh temp dir should be writable");
assert!(registration_dir_writable(&dir.join("missing/deeper")));
std::fs::remove_dir_all(&dir).ok();
}
#[cfg(unix)]
#[test]
fn registration_dir_writable_false_for_readonly_dir() {
use std::os::unix::fs::PermissionsExt as _;
if privilege::user::privileged() {
return;
}
let dir = std::env::temp_dir().join(format!("bestool-canopy-ro-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o500)).unwrap();
assert!(!registration_dir_writable(&dir), "0500 dir must not be writable");
std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o700)).ok();
std::fs::remove_dir_all(&dir).ok();
}
}