oxyde-cloud-deploy 0.4.3

CLI for deploying the Oxyde Cloud API
Documentation
use anyhow::{Context, Result};
use cargo_leptos::config::Opts;
use oxyde_cloud_client::Client;
use oxyde_cloud_common::config::CloudConfig;
use std::fs::read_dir;
use std::path::{Path, PathBuf};
use walkdir::WalkDir;

pub async fn deploy_with_config_file(config: &PathBuf, cargo_leptos_opts: Opts) -> Result<()> {
    let config = CloudConfig::load(config)
        .await
        .with_context(|| format!("Failed to load config file: {}", config.display()))?;
    deploy(&config, cargo_leptos_opts)
        .await
        .context("Failed to deploy with loaded config")?;
    Ok(())
}

pub async fn deploy(config: &CloudConfig, cargo_leptos_opts: Opts) -> Result<()> {
    crate::build::build(cargo_leptos_opts.clone())
        .await
        .context("Failed to build project")?;

    let target_dir = "target";
    let target_bin_dir = std::env::var("OXYDE_CLOUD_BIN_DIR").unwrap_or_else(|_| "target/x86_64-unknown-linux-musl".to_string());

    let server_bin_dir = if cargo_leptos_opts.release {
        "release"
    } else {
        "debug"
    };
    let frontend_dir = "site";

    let api_key = std::env::var("OXYDE_CLOUD_API_KEY")
        .context("Environment variable OXYDE_CLOUD_API_KEY is required for deployment")?;
    let client = Client::new(api_key.clone());

    let frontend_path = Path::new(target_dir).join(frontend_dir);
    let server_path = Path::new(&target_bin_dir).join(server_bin_dir);

    let mut files = recursive_files_from_dir(frontend_path);
    files.append(&mut server_files(server_path).context("Failed to collect server files")?);

    log::debug!(target:"cargo_leptos", "Found files: {:#?}", files);

    log::info!(target:"cargo_leptos", "Deploying app {}", config.app.slug);

    if let Err(err) = deploy_inner(config, client, &mut files).await {
        log::error!(target:"cargo_leptos", "Deploy failed: {:?}", err);
        return Err(err);
    }

    log::info!(target:"cargo_leptos", "Deployed app to {}", config.deployed_url());

    Ok(())
}

async fn deploy_inner(
    config: &CloudConfig,
    client: Client,
    files: &mut Vec<PathBuf>,
) -> Result<()> {
    for file in files {
        let file_path = file.display().to_string();
        log::debug!(target:"cargo_leptos", "Uploading {}...", file_path);
        client
            .clone()
            .upload_file(&config.app.slug, file)
            .await
            .with_context(|| format!("Failed to upload file: {file_path}"))?;
    }

    log::debug!(target:"cargo_leptos", "Deploying app...");
    client
        .upload_done(config)
        .await
        .context("Failed to signal deployment completion")?;

    Ok(())
}

fn recursive_files_from_dir(dir: impl AsRef<Path>) -> Vec<PathBuf> {
    WalkDir::new(dir)
        .into_iter()
        .filter_map(|e| e.ok().map(|e| e.into_path()))
        .filter_map(|e| if e.is_file() { Some(e) } else { None })
        .collect()
}

fn server_files(dir: impl AsRef<Path>) -> std::io::Result<Vec<PathBuf>> {
    let starts_with_a_dot = |path: &Path| {
        path.file_name()
            .expect("cant read filename")
            .to_str()
            .expect("cant convert filename")
            .starts_with(".")
    };

    Ok(read_dir(dir)?
        .filter_map(|d| {
            d.ok().and_then(|e| {
                let path = e.path();
                if path.is_file() && path.extension().is_none() && !starts_with_a_dot(&path) {
                    Some(path)
                } else {
                    None
                }
            })
        })
        .collect())
}