rews 0.4.5

A binary client for Usenet.
Documentation
use std::{
  collections::HashMap,
  path::{Path, PathBuf},
};

use clap::Subcommand;
use config::Config;
use directories::ProjectDirs;
use ini::Ini;
use quick_xml::DeError;
use serde::Deserialize;

use crate::{download::Server, PROJECT_DIR};

pub fn execute_configure_action(action: ConfigureAction) -> Result<(), ConfigurationError> {
  match action {
    ConfigureAction::Servers { action } => execute_server_action(action),
  }
}

fn execute_server_action(action: ServerAction) -> Result<(), ConfigurationError> {
  match action {
    ServerAction::GetDownloadServer => {
      let settings = read_settings(PROJECT_DIR.wait());

      if let Some(download_server) = settings.download_server {
        Ok(println!("{download_server}"))
      } else {
        Err(ConfigurationError::MissingServer)
      }
    }

    ServerAction::SetDownloadServer { server_name } => {
      update_settings(PROJECT_DIR.wait(), |mut settings| {
        if settings.servers.contains_key(&server_name) {
          settings.download_server = Some(server_name);
          Ok(settings)
        } else {
          Err(ConfigurationError::MissingServer)
        }
      })
    }

    ServerAction::Add {
      name,
      host,
      username,
      password,
      connections,
      secure,
    } => update_settings(PROJECT_DIR.wait(), |mut settings| {
      if settings.servers.contains_key(&name) {
        Err(ConfigurationError::ServerExists)
      } else {
        settings.servers.insert(
          name,
          Server {
            host,
            username,
            password,
            connections,
            secure,
          },
        );
        Ok(settings)
      }
    }),

    ServerAction::Remove { server_name } => update_settings(PROJECT_DIR.wait(), |mut settings| {
      if settings.servers.remove(&server_name).is_some() {
        if let Some(default_server) = &settings.download_server {
          if default_server == &server_name {
            settings.download_server = None;
          }
        }
        Ok(settings)
      } else {
        Err(ConfigurationError::MissingServer)
      }
    }),

    ServerAction::List => {
      let settings = read_settings(PROJECT_DIR.wait());

      Ok(settings.servers.into_iter().for_each(|(name, server)| {
        println!("Name:        {}", name);
        println!("Host:        {}", server.host);
        println!("Username:    {}", server.username);
        println!("Connections: {}", server.connections);
        println!("Secure:      {}", server.secure);
        println!();
      }))
    }
  }
}

fn update_settings<F>(app_dir: &ProjectDirs, f: F) -> Result<(), ConfigurationError>
where
  F: FnOnce(Settings) -> Result<Settings, ConfigurationError>,
{
  f(read_settings(app_dir))?
    .write_to_file(&settings_path(app_dir))
    .map_err(ConfigurationError::WriteFailed)
}

pub fn read_settings(app_dir: &ProjectDirs) -> Settings {
  Settings::from_file(&settings_path(app_dir)).unwrap_or_default()
}

fn settings_path(app_dir: &ProjectDirs) -> PathBuf {
  let mut settings_path = PathBuf::from(app_dir.config_dir());
  settings_path.push("settings.ini");
  settings_path
}

#[derive(Debug, Subcommand)]
pub enum ConfigureAction {
  /// View or update the server configuration.
  Servers {
    #[clap(subcommand)]
    action: ServerAction,
  },
}

#[derive(Debug, Subcommand)]
pub enum ServerAction {
  /// Get the name of the server configuration used for downloads.
  GetDownloadServer,
  /// Set the server configuration to use for downloads.
  SetDownloadServer {
    #[clap(value_parser)]
    server_name: String,
  },
  /// Add a new server configuration.
  Add {
    /// The name of the server.
    #[clap(value_parser)]
    name: String,
    /// The hostname for the server.
    #[clap(value_parser)]
    host: String,
    /// Your username for the server.
    #[clap(value_parser)]
    username: String,
    /// Your password for the server.
    #[clap(value_parser)]
    password: String,
    /// The amount of simultaneous connections are supported.
    #[clap(value_parser)]
    connections: usize,
    /// Whether to use a secure connection (TLS) or not.
    #[clap(value_parser)]
    secure: bool,
  },
  /// Remove an existing server configuration.
  #[clap(alias = "rm")]
  Remove {
    #[clap(value_parser)]
    server_name: String,
  },
  /// List existing server configurations.
  #[clap(alias = "ls")]
  List,
}

#[derive(Debug)]
pub enum ConfigurationError {
  ServerExists,
  MissingServer,
  WriteFailed(std::io::Error),
  ReadFailed(std::io::Error),
  DeserializationFailed(DeError),
}

#[derive(Debug, Deserialize)]
pub struct Settings {
  pub download_server: Option<String>,
  pub servers: HashMap<String, Server>,
}

impl Default for Settings {
  fn default() -> Self {
    Self {
      download_server: Default::default(),
      servers: Default::default(),
    }
  }
}

impl Settings {
  fn from_file(settings_file: &Path) -> Result<Settings, config::ConfigError> {
    Config::builder()
      .add_source(config::File::from(settings_file))
      .build()
      .and_then(|conf| conf.try_deserialize())
  }

  fn write_to_file(&self, settings_file: &Path) -> Result<(), std::io::Error> {
    let mut ini = Ini::new();

    if let Some(default_server) = &self.download_server {
      ini
        .with_general_section()
        .set("download_server", default_server);
    }

    self.servers.iter().for_each(|(name, server)| {
      ini
        .with_section(Some(format!("servers.{name}")))
        .set("host", &server.host)
        .set("username", &server.username)
        .set("password", &server.password)
        .set("connections", server.connections.to_string())
        .set("secure", server.secure.to_string());
    });

    if let Some(dir) = settings_file.parent() {
      if let Err(e) = std::fs::create_dir_all(dir) {
        return Err(e);
      }
    }

    ini.write_to_file(settings_file)
  }
}