install-framework-cli 1.0.0

[Install Framework] CLI interface powered by clap
Documentation
// Copyright 2021 Yuri6037

// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:

// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.

// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.

use std::string::String;
use std::collections::HashMap;
use std::path::Path;
use clap::clap_app;
use std::vec::Vec;
use install_framework_core::interface::Interface;
use install_framework_core::interface::ConstructibleInterface;
use install_framework_core::interface::Installer;
use install_framework_core::interface::InstallMethod;
use install_framework_core::interface::PostInstall;
use install_framework_core::interface::PostUninstall;
use install_framework_core::builder::InstallerBuilder;
use install_framework_base::interface::BaseInterface;

mod error;
mod interpreter;

use error::CliError;

pub struct CliInterface
{
    base: BaseInterface<interpreter::CliInterpreter, CliError>,
    install: bool,
    status: bool,
    local: bool,
    yes: bool,
    components: Vec<String>
}

impl ConstructibleInterface<'_> for CliInterface
{
    fn run(mut builder: InstallerBuilder) -> i32
    {
        let mut interface = CliInterface
        {
            base: BaseInterface::new(interpreter::CliInterpreter::new()),
            install: false,
            status: false,
            local: true,
            yes: false,
            components: Vec::new()
        };
        return builder.run(&mut interface);
    }
}

impl CliInterface
{
    fn check_install_status(&mut self, installer: &mut dyn Installer, install_dir: &Path, resources: &HashMap<&'static str, &'static [u8]>) -> Result<(), CliError>
    {
        println!("==> Install status <==");
        let components = self.base.get_components(installer, resources)?;
        let state = self.base.get_installation_state(components, installer, install_dir)?;
        if let Some(v) = state
        {
            println!("{}", v.manifest);
            println!("Installed components:");
            for c in v.installed_components
            {
                println!("    - {} ({:?})", c.name, c.version);
            }
            println!("Not yet installed components:");
            for c in v.non_installed_components
            {
                println!("    - {} ({:?})", c.name, c.version);
            }
        }
        else
        {
            println!("Software not installed");
            return Ok(());
        }
        println!("==> End <==");
        return Ok(());
    }
}

impl Interface for CliInterface
{
    type ErrorType = CliError;

    fn welcome(&mut self, name: &'static str, version: &'static str, author: &'static str) -> Result<(), Self::ErrorType>
    {
        println!("Starting application installer for {} ({})...", name, version);
        let matches = clap_app!(name =>
            (version: version)
            (author: author)
            (about: "Powered by InstallFramework <https://gitlab.com/Yuri6037/install-framework>")
            (@arg install: -i --install "Install one or more component(s)")
            (@arg uninstall: -u --uninstall "Uninstall one or more component(s)")
            (@arg status: -s --status "Show install status including which component(s) have been installed")
            (@arg components: +takes_value +multiple -c --component "Specifies a component to be modified")
            (@arg local: -l --local "Run a local user install (if permitted by this installer)")
            (@arg yes: -y --yes "Agree to all licenses")
            (@arg props: +multiple -p --property +takes_value "Set a property to avoid user input prompt (usefull in CI builds)")
        ).get_matches();
        if !matches.is_present("install") && !matches.is_present("uninstall") && !matches.is_present("status")
        {
            return Err(CliError::Generic(String::from("Nothing to do, please specify an install, uninstall or status command")));
        }
        if (matches.is_present("install") && matches.is_present("uninstall"))
            || (matches.is_present("install") && matches.is_present("status"))
            || (matches.is_present("uninstall") && matches.is_present("status"))
        {
            return Err(CliError::Generic(String::from("Multiple action commands cannot co-exist")));
        }
        self.base.set_static_info(name, version, author);
        self.yes = matches.is_present("yes");
        self.install = matches.is_present("install");
        if matches.is_present("status")
        {
            self.status = true;
            self.install = true;
        }
        if let Some(obj) = matches.values_of("components")
        {
            for v in obj
            {
                self.components.push(String::from(v));
            }
        }
        if let Some(obj) = matches.values_of("props")
        {
            for v in obj
            {
                if let Some(idx) = v.find('=')
                {
                    let key = String::from(&v[0..idx]);
                    let value = String::from(&v[idx + 1..]);
                    self.base.set_prop(key, value);
                }
                else
                {
                    return Err(CliError::Generic(format!("Invalid property {}", v)));
                }
            }
        }
        self.local = matches.is_present("local");
        return Ok(());
    }

    fn get_install_method(&mut self) -> Result<InstallMethod, Self::ErrorType>
    {
        let mut m = InstallMethod::SystemInstall; //Default is system install
        if self.local
        {
            m = InstallMethod::UserInstall;
        }
        return Ok(m);
    }

    fn should_uninstall(&self) -> Result<bool, Self::ErrorType>
    {
        return Ok(!self.install);
    }

    fn run_install(&mut self, installer: &mut dyn Installer, dir: &Path, method: InstallMethod, resources: &HashMap<&'static str, &'static [u8]>) -> Result<(), Self::ErrorType>
    {
        if self.status
        {
            self.check_install_status(installer, dir, resources)?;
            return Err(CliError::Skip);
        }
        println!("==> Installing components... <==");
        let components = self.base.get_components(installer, resources)?;
        for c in &self.components
        {
            if let Some(c) = components.iter().find(|s| s.name == *c)
            {
                println!("");
                println!("Installing component {}...", c.name);
                if let Some(v) = &c.version
                {
                    println!("Version: {}", v);
                }
                if let Some(v) = &c.readme
                {
                    println!("> README");
                    println!("{}", v);
                }
                if let Some(v) = &c.license
                {
                    println!("> LICENSE");
                    println!("{}", v);
                }
                if !self.yes
                {
                    println!("Proceed (y for yes, else no)?");
                    let line = interpreter::read_console()?;
                    if !line.starts_with("y")
                    {
                        return Err(CliError::Generic(String::from("User canceled installation")));
                    }
                }
                self.base.install_component(&c, installer, dir, method, resources)?;
            }
            else
            {
                return Err(CliError::Generic(format!("Unknown component: {}", c)));
            }
        }
        println!("==> End <==");
        return Ok(());
    }

    fn run_post_install(&mut self, post: &mut dyn PostInstall, dir: &Path) -> Result<(), Self::ErrorType>
    {
        return self.base.run_post_install(post, dir);
    }

    fn run_uninstall(&mut self, installer: &mut dyn Installer, dir: &Path, method: InstallMethod, resources: &HashMap<&'static str, &'static [u8]>) -> Result<(), Self::ErrorType>
    {
        println!("==> Uninstalling components... <==");
        let components = self.base.get_components(installer, resources)?;
        for c in &self.components
        {
            if let Some(c) = components.iter().find(|s| s.name == *c)
            {
                println!("");
                println!("Uninstalling component {}...", c.name);
                if !self.yes
                {
                    println!("Proceed (y for yes, else no)?");
                    let line = interpreter::read_console()?;
                    if !line.starts_with("y")
                    {
                        return Err(CliError::Generic(String::from("User canceled installation")));
                    }
                }
                self.base.uninstall_component(&c, installer, dir, method)?;
            }
            else
            {
                return Err(CliError::Generic(format!("Unknown component: {}", c)));
            }
        }
        println!("==> End <==");
        return Ok(());
    }

    fn run_post_uninstall(&mut self, post: &mut dyn PostUninstall, dir: &Path) -> Result<(), Self::ErrorType>
    {
        return self.base.run_post_uninstall(post, dir);
    }

    fn error(&mut self, e: Self::ErrorType) -> i32
    {
        match e
        {
            CliError::Generic(s) =>
            {
                eprintln!("A generic error has occured: {}", s);
                return 1;
            },
            CliError::Io(e) =>
            {
                eprintln!("An IO error has occured: {}", e);
                return 2;
            },
            CliError::Skip => return 0,
            CliError::Network(e) =>
            {
                eprintln!("A network error has occured: {}", e);
                return 3;
            },
            CliError::Zip(e) =>
            {
                eprintln!("A zip error has occured: {}", e);
                return 4;
            }
        }
    }

    fn finish(&mut self) -> i32
    {
        println!("Install succeeded!");
        return 0;
    }
}