stigmerge 0.6.2

Anonymous decentralized file distribution and transfer
use std::{io::IsTerminal, path::PathBuf};

use anyhow::{Error, Result};
use clap::{Parser, Subcommand};
use sha2::{Digest, Sha256};

use crate::share;

#[derive(Parser, Debug)]
#[command(name = "stigmerge")]
#[command(bin_name = "stigmerge")]
pub struct Cli {
    #[arg(long, env)]
    pub no_ui: bool,

    #[arg(long, env)]
    pub state_dir: Option<String>,

    #[command(subcommand)]
    pub commands: Commands,
}

impl Cli {
    pub fn no_ui(&self) -> bool {
        self.no_ui || !std::io::stdout().is_terminal()
    }

    pub fn state_dir(&self) -> Result<String> {
        if let Some(s) = &self.state_dir {
            return Ok(s.to_owned());
        }
        match self.commands {
            Commands::Fetch {
                ref share_keys,
                ref index_digest,
                ref output_path,
                ..
            } => {
                if share_keys.is_empty() {
                    return Err(Error::msg("at least one share key must be provided"));
                }
                let key_match = match index_digest {
                    Some(digest) => digest,
                    None => &share_keys[0],
                };
                self.state_dir_for(format!(
                    "get:{}:{}",
                    key_match,
                    output_path.to_string_lossy()
                ))
            }
            Commands::Seed { ref path } => {
                self.state_dir_for(format!("seed:{}", path.to_string_lossy()))
            }
            Commands::Info { ref path, .. } => {
                self.state_dir_for(format!("info:{}", path.to_string_lossy()))
            }
            _ => Err(Error::msg("invalid command")),
        }
    }

    pub fn state_dir_for(&self, key: String) -> Result<String> {
        let mut key_digest = Sha256::new();
        key_digest.update(key.as_bytes());
        let key_digest_bytes: [u8; 32] = key_digest.finalize().into();
        let dir_name = hex::encode(key_digest_bytes);
        let data_dir = dirs::state_dir()
            .or(dirs::data_local_dir())
            .ok_or(Error::msg("cannot resolve state dir"))?;
        let state_dir = data_dir
            .join("stigmerge")
            .join(dir_name)
            .into_os_string()
            .into_string()
            .map_err(|os| Error::msg(format!("{:?}", os)))?;
        Ok(state_dir)
    }

    pub fn version(&self) -> bool {
        if let Commands::Version = self.commands {
            return true;
        }
        false
    }
}

#[derive(Subcommand, Debug)]
pub enum Commands {
    Fetch {
        share_keys: Vec<String>,

        #[arg(long = "index-digest", short = 'i')]
        index_digest: Option<String>,

        #[arg(long = "output-path", short = 'o', default_value = ".")]
        output_path: PathBuf,
    },
    Seed {
        path: PathBuf,
    },
    Info {
        share_key: String,

        #[arg(default_value = ".")]
        path: PathBuf,
    },
    Version,
}

impl Commands {
    pub fn share_args(&self) -> Result<share::Mode> {
        Ok(match self {
            Commands::Fetch {
                share_keys: share_key_strings,
                index_digest,
                output_path,
            } => {
                let want_index_digest = match index_digest {
                    Some(digest_string) => {
                        let digest = hex::decode(digest_string)?;
                        Some(
                            digest
                                .try_into()
                                .map_err(|_| Error::msg("invalid digest length"))?,
                        )
                    }
                    None => None,
                };
                let mut share_keys = vec![];
                for share_key_string in share_key_strings {
                    share_keys.push(share_key_string.parse()?);
                }
                share::Mode::Fetch {
                    root: output_path.into(),
                    want_index_digest,
                    share_keys,
                }
            }
            Commands::Seed { path } => share::Mode::Seed {
                path: path.to_owned(),
            },
            _ => return Err(Error::msg("invalid command")),
        })
    }
}