aion-cli 0.31.0

The `aion` command line: operate Aion durable workflows over gRPC and run the Aion server.
//! `aion store harden`: bring every file under a store to its private mode,
//! once, for a store that predates private-by-creation (haematite 0.12.1) or
//! that someone loosened by hand.
//!
//! The boot no longer walks the tree — it checks the data root and the shard
//! directories only — so this verb is where the full walk lives. It narrates
//! one line per top-level entry with the running file count and ends with a
//! total, and it refuses to run while a live server holds the store.
//!
//! Exit codes: 0 walked; 2 the verb could not do its job (no store to walk, a
//! held store, a failure in the tree) — the reason is on stderr in one
//! sentence.

use std::path::PathBuf;
use std::process::ExitCode;

use aion_server::config::{CliOverrides, ServerConfig};
use aion_server::store_harden::harden_store_tree;
use clap::{Args, Subcommand};

/// `aion store …`: verbs against a store's data directory, with no server.
#[derive(Args, Clone, Debug)]
pub struct StoreCommand {
    /// The verb.
    #[command(subcommand)]
    pub command: StoreSubcommand,
}

/// The store verbs.
#[derive(Subcommand, Clone, Debug)]
pub enum StoreSubcommand {
    /// Bring every directory under the store to 0700 and every file to 0600.
    ///
    /// The one-time step for a store created before Aion 0.28 (files were
    /// created with the process umask and repaired at every boot), or for a
    /// store whose modes were loosened by hand. Refuses while a server holds
    /// the store. Newer stores need it never: every file is private from its
    /// own creation.
    Harden(HardenArgs),
}

/// Arguments to `aion store harden`.
#[derive(Args, Clone, Debug)]
pub struct HardenArgs {
    /// The store's data directory. Defaults to `store.data_dir` from the
    /// configuration this machine's server would load.
    #[arg(long, value_name = "DIR")]
    pub data_dir: Option<PathBuf>,
    /// Explicit configuration file, when resolving the data directory from
    /// configuration.
    #[arg(long, value_name = "FILE")]
    pub config: Option<PathBuf>,
}

/// Run `aion store harden`.
pub fn harden(args: &HardenArgs) -> ExitCode {
    let data_dir = match resolve_data_dir(args) {
        Ok(data_dir) => data_dir,
        Err(reason) => {
            eprintln!("aion store harden: {reason}");
            return ExitCode::from(2);
        }
    };
    println!("hardening {}", data_dir.display());
    let outcome = harden_store_tree(&data_dir, |progress| {
        println!(
            "  {}: {} file(s), {} so far",
            progress.entry, progress.files, progress.total_files
        );
    });
    match outcome {
        Ok(report) => {
            println!(
                "done: {} directories set to 0700, {} files set to 0600",
                report.directories, report.files
            );
            ExitCode::SUCCESS
        }
        Err(error) => {
            eprintln!("aion store harden: {error}");
            ExitCode::from(2)
        }
    }
}

fn resolve_data_dir(args: &HardenArgs) -> Result<PathBuf, String> {
    if let Some(explicit) = &args.data_dir {
        return Ok(explicit.clone());
    }
    let overrides = CliOverrides {
        config_path: args.config.clone(),
        ..CliOverrides::default()
    };
    let config = ServerConfig::load(&overrides)
        .map_err(|error| format!("could not load the configuration to find the store: {error}"))?;
    match config.store.data_dir {
        Some(data_dir) => Ok(PathBuf::from(data_dir)),
        None => Err(
            "the configuration names no store.data_dir (the store backend is not haematite); \
             pass --data-dir"
                .to_owned(),
        ),
    }
}