jarn 0.1.0

A package manager for jaseci applications
Documentation
use console::style;
use dialoguer::{theme, Input};
use jarn::JaseciConfig;
use log::error;
use serde::{Deserialize, Serialize};
use std::{
    ffi::OsStr,
    fs::{self, File},
    io::{Read, Seek, Write},
    path::{Path, PathBuf},
};
use walkdir::{DirEntry, WalkDir};

use zip::{result::ZipError, write::FileOptions};

#[derive(Serialize, Deserialize, Debug)]
struct PackageConfig {
    scope: String,
    name: String,
    version: String,
}

pub fn install_package(path: &str, project_dir: &str) {
    let zip_path = PathBuf::from(&path);
    let project_path = PathBuf::from(&project_dir);
    let config_path = project_path.clone().join("jaseci.toml");

    let config: JaseciConfig = toml::from_str(&fs::read_to_string(config_path).unwrap())
        .expect("Unable to find jaseci.toml file in project directory!");

    extract_zip(&zip_path, &project_path.join("packages"));
}

pub fn publish_package(dir: &str) {
    let mut zip_path = PathBuf::new();
    let config_path = PathBuf::from(&dir).join("action.toml");

    zip_path.push(&dir);

    let config: PackageConfig =
        toml::from_str(&fs::read_to_string(config_path).unwrap()).expect("Failed to read config!");

    zip_path.push(format!(
        "{}-jpkg_{}.zip",
        config.name,
        config.version.to_string()
    ));

    let result = create_zip(
        &dir,
        &zip_path.clone().to_str().unwrap(),
        zip::CompressionMethod::Bzip2,
    );

    match result {
        Ok(_) => {
            println!("Package created at {}", zip_path.display());
        }
        Err(_err) => {
            error!("Failed to create package!");
        }
    };
}

pub fn init_package(dir: &str) {
    println!("{}", style("\nTell us about your new package: \n").blue());

    let scope: String = Input::with_theme(&theme::ColorfulTheme::default())
        .with_prompt("Package scope")
        .interact()
        .unwrap();
    let name: String = Input::with_theme(&theme::ColorfulTheme::default())
        .with_prompt("Package name")
        .interact()
        .unwrap();

    let mut path = PathBuf::new();
    path.push(&dir);

    if !path.exists() {
        fs::create_dir_all(&path).unwrap();
    }

    let config: PackageConfig = PackageConfig {
        name: name.clone(),
        scope: scope.clone(),
        version: String::from("1.0.0"),
    };

    let default_action_code = format!(
        r#"from jaseci.jsorc.live_actions import jaseci_action

@jaseci_action(act_group=["{}.{}"], allow_remote=True)
def add(first_number: int, second_number: int):
    return first_number + second_number
    "#,
        scope, name
    );

    let action_path = path.join("action.toml");
    let python_path = path.join("action.py");

    fs::write(action_path, toml::to_string(&config).unwrap()).unwrap();
    fs::write(python_path, default_action_code).unwrap();

    println!(
        "{}",
        format!(
            "{} {}",
            style("\nSweet! Package created at").green(),
            style(path.display()).bold().italic().green()
        )
    );
}

fn zip_dir<T>(
    it: &mut dyn Iterator<Item = DirEntry>,
    prefix: &str,
    writer: T,
    method: zip::CompressionMethod,
) -> zip::result::ZipResult<()>
where
    T: Write + Seek,
{
    let mut zip = zip::ZipWriter::new(writer);
    let options = FileOptions::default()
        .compression_method(method)
        .unix_permissions(0o755);

    let mut buffer = Vec::new();
    for entry in it {
        let path = entry.path();
        let name = path.strip_prefix(Path::new(prefix)).unwrap();
        let allowed_extensions = vec![OsStr::new("toml"), OsStr::new("txt"), OsStr::new("py")];

        // Write file or directory explicitly
        // Some unzip tools unzip files with directory paths correctly, some do not!
        if path.is_file() {
            match path.extension() {
                Some(ext) => {
                    println!("{:?}", ext);
                    if !allowed_extensions.contains(&ext) {
                        println!("{}", style(format!("skipping file {path:?}...")).yellow());
                        continue;
                    }
                }
                None => {
                    println!("{}", style(format!("skipping file {path:?}...")).yellow());
                    continue;
                }
            }

            println!(
                "{}",
                style(format!("adding file {path:?} as {name:?} ...")).green()
            );
            #[allow(deprecated)]
            zip.start_file_from_path(name, options)?;
            let mut f = File::open(path)?;

            f.read_to_end(&mut buffer)?;
            zip.write_all(&buffer)?;
            buffer.clear();
        } else if !name.as_os_str().is_empty() {
            // Only if not root! Avoids path spec / warning
            // and mapname conversion failed error on unzip
            println!("adding dir {path:?} as {name:?} ...");
            #[allow(deprecated)]
            zip.add_directory_from_path(name, options)?;
        }
    }
    zip.finish()?;
    Result::Ok(())
}

fn create_zip(
    src_dir: &str,
    dst_file: &str,
    method: zip::CompressionMethod,
) -> zip::result::ZipResult<()> {
    if !Path::new(src_dir).is_dir() {
        return Err(ZipError::FileNotFound);
    }

    let path = Path::new(dst_file);
    let file = File::create(path).unwrap();

    let walkdir = WalkDir::new(src_dir);
    zip_dir(
        &mut walkdir.into_iter().filter_map(|e| e.ok()),
        src_dir,
        file,
        method,
    )?;

    Ok(())
}

fn extract_zip(fname: &PathBuf, target: &PathBuf) {
    let file = fs::File::open(fname).unwrap();

    let mut archive = zip::ZipArchive::new(file).unwrap();

    let mut action_file_contents = String::new();

    let mut action_file = match archive.by_name("action.toml") {
        Ok(file) => file,
        Err(..) => {
            println!("File action.toml not found");
            return ();
        }
    };

    if let Err(err) = action_file.read_to_string(&mut action_file_contents) {
        println!("Unable to read action.toml {}", err);
        return ();
    }

    let action_config: PackageConfig =
        toml::from_str(&action_file_contents).expect("Unable to read action.toml");

    let target = &target.join(action_config.scope).join(action_config.name);

    // start full extraction
    let file = fs::File::open(fname).unwrap();
    let mut archive = zip::ZipArchive::new(file).unwrap();

    for i in 0..archive.len() {
        let mut file = archive.by_index(i).unwrap();
        let outpath = match file.enclosed_name() {
            Some(path) => path.to_owned(),
            None => continue,
        };

        let outpath = PathBuf::from(target).join(outpath);

        if (&*file.name()).ends_with('/') {
            println!(
                "{}",
                style(format!("File {} extracted to \"{}\"", i, outpath.display())).green()
            );
            fs::create_dir_all(&outpath).unwrap();
        } else {
            println!(
                "{}",
                style(format!(
                    "File {} extracted to \"{}\" ({} bytes)",
                    i,
                    outpath.display(),
                    file.size()
                ))
                .green()
            );
            if let Some(p) = outpath.parent() {
                if !p.exists() {
                    fs::create_dir_all(&p).unwrap();
                }
            }
            let mut outfile = fs::File::create(&outpath).unwrap();
            std::io::copy(&mut file, &mut outfile).unwrap();
        }
    }
}