rews 0.4.5

A binary client for Usenet.
Documentation
use std::collections::HashMap;
use std::net::ToSocketAddrs;
use std::path::PathBuf;
use std::result::Result;
use std::sync::atomic::{AtomicBool, Ordering};

use clap::{CommandFactory, Parser, Subcommand};
use clap_complete::{generate, Shell};
use directories::{ProjectDirs, UserDirs};
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use rews::configure::{
  execute_configure_action, read_settings, ConfigurationError, ConfigureAction, Settings,
};
use rews::download::DownloadError;
use rews::queue::{Queue, QueueAction, QueueError};
use rews::PROJECT_DIR;

static EXIT: AtomicBool = AtomicBool::new(false);

fn main() -> Result<(), Error> {
  // Read command line arguments.
  let args = Cli::parse();

  // Locate user directories.
  let project_dir =
    ProjectDirs::from("no", "heksesang", "rews").ok_or(Error::MissingUserDirectory)?;
  let user_dir = UserDirs::new().ok_or(Error::MissingUserDirectory)?;
  let download_dir = user_dir.download_dir().ok_or(Error::MissingUserDirectory)?;

  // Set global data.
  PROJECT_DIR.set(project_dir).unwrap();

  // Ensure directories exist.
  std::fs::create_dir_all(PROJECT_DIR.wait().config_dir()).map_err(Error::DirectoryCreateFailed)?;
  std::fs::create_dir_all(PROJECT_DIR.wait().data_dir()).map_err(Error::DirectoryCreateFailed)?;
  std::fs::create_dir_all(download_dir).map_err(Error::DirectoryCreateFailed)?;

  // Execute commands.
  match args.command {
    Commands::Queue { action } => action.execute().map_err(Error::QueueError),

    Commands::Download => Queue::lock(|queue| {
      let settings: Settings = read_settings(PROJECT_DIR.wait());

      let server = settings
        .download_server
        .and_then(|server| settings.servers.get(&server))
        .ok_or(Error::ConfigurationError(ConfigurationError::MissingServer))?;

      format!("{}:80", server.host)
        .to_socket_addrs()
        .expect("Unable to resolve hostname");

      let m = MultiProgress::new();

      let style = ProgressStyle::default_bar()
          .template(
            "{prefix}\n{total_bytes:7.cyan} [{elapsed_precise}] {percent:>3}% {bar:40.cyan/blue} [{eta_precise}] ({bytes_per_sec})\n{msg}",
          )
          .unwrap()
          .progress_chars("##>-");

      let mut active_bars: HashMap<PathBuf, ProgressBar> = HashMap::new();

      let download = queue
        .run(download_dir, server)
        .map_err(|e| Error::DownloadFailed(vec![e]))?;

      ctrlc::set_handler(move || {
        EXIT.store(true, Ordering::Relaxed);
      })
      .map_err(Error::HandlerError)?;

      while let Ok((output_dir, message_id)) = download.next() {
        if let Some(pb) = active_bars.get(&output_dir) {
          pb.inc(queue.size(&message_id));
        } else {
          let find_result = queue.items.iter().find_map(|item| {
            if item.path == output_dir {
              let pb = m.add(ProgressBar::new(item.total_bytes));
              pb.set_style(style.clone());
              let downloaded_bytes = item.total_bytes - item.remaining_bytes();
              if downloaded_bytes > 0 {
                pb.set_position(item.total_bytes - item.remaining_bytes());
              }
              pb.set_prefix(PathBuf::from(&item.path).display().to_string());
              pb.inc(queue.size(&message_id));
              Some(pb)
            } else {
              None
            }
          });

          if let Some(pb) = find_result {
            active_bars.insert(output_dir, pb);
          }
        }

        queue.remove_message(&message_id);

        if EXIT.load(Ordering::Relaxed) && !download.is_cancelled() {
          download.cancel();
        }
      }

      Ok(())
    }),

    Commands::Configure { action } => {
      execute_configure_action(action).map_err(Error::ConfigurationError)
    }

    Commands::Completions { shell } => {
      let cmd = &mut Cli::command();

      generate(
        shell,
        cmd,
        cmd.get_name().to_string(),
        &mut std::io::stdout(),
      );

      Ok(())
    }
  }
}

#[derive(Debug, Parser)]
#[clap(author, version, about, long_about = None)]
struct Cli {
  #[clap(subcommand)]
  command: Commands,
}

#[derive(Debug, Subcommand)]
enum Commands {
  /// Download the queued files.
  Download,
  /// Manage the download queue.
  Queue {
    #[clap(subcommand)]
    action: QueueAction,
  },
  /// Configure the application settings.
  Configure {
    #[clap(subcommand)]
    action: ConfigureAction,
  },
  /// Generate shell completions.
  Completions { shell: Shell },
}

#[derive(Debug)]
enum Error {
  DownloadFailed(Vec<DownloadError>),
  ConfigurationError(ConfigurationError),
  MissingUserDirectory,
  DirectoryCreateFailed(std::io::Error),
  QueueError(QueueError),
  HandlerError(ctrlc::Error),
}

impl From<QueueError> for Error {
  fn from(e: QueueError) -> Self {
    Self::QueueError(e)
  }
}