tovuk 0.1.70

Deploy Rust workers, static frontends, and worker-static apps to Tovuk.
use super::super::{
    constants::{ARCHIVE_EXCLUDES, ARCHIVE_LIMIT_BYTES, WALK_EXCLUDED_DIRS},
    errors::{Result, agent_error},
};
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
use flate2::{Compression, write::GzEncoder};
use std::path::Path;
use walkdir::{DirEntry, WalkDir};

pub(super) fn create_archive_base64(project_dir: &Path, json_output: bool) -> Result<String> {
    let mut archive = Vec::new();
    {
        let encoder = GzEncoder::new(&mut archive, Compression::default());
        let mut builder = tar::Builder::new(encoder);
        for entry in WalkDir::new(project_dir)
            .into_iter()
            .filter_entry(|entry| !is_archive_excluded_entry(project_dir, entry))
            .flatten()
        {
            if !entry.file_type().is_file() {
                continue;
            }
            let relative = match entry.path().strip_prefix(project_dir) {
                Ok(relative) => relative,
                Err(_error) => continue,
            };
            builder
                .append_path_with_name(entry.path(), Path::new(".").join(relative))
                .map_err(|error| {
                    agent_error(
                        "archive_failed",
                        "Could not create source archive.",
                        format!("Check project files and retry: {error}"),
                        json_output,
                    )
                })?;
        }
        let encoder = builder.into_inner().map_err(|error| {
            agent_error(
                "archive_failed",
                "Could not create source archive.",
                format!("Check project files and retry: {error}"),
                json_output,
            )
        })?;
        encoder.finish().map_err(|error| {
            agent_error(
                "archive_failed",
                "Could not create source archive.",
                format!("Check project files and retry: {error}"),
                json_output,
            )
        })?;
    }
    if archive.len() > ARCHIVE_LIMIT_BYTES {
        return Err(agent_error(
            "archive_too_large",
            "Source archive is too large.",
            "Remove build outputs, target directories, logs, and local caches before deploying.",
            json_output,
        ));
    }
    Ok(BASE64.encode(archive))
}

fn is_archive_excluded_entry(project_dir: &Path, entry: &DirEntry) -> bool {
    if entry.path() == project_dir {
        return false;
    }
    let relative = match entry.path().strip_prefix(project_dir) {
        Ok(relative) => relative.to_string_lossy().replace('\\', "/"),
        Err(_error) => return true,
    };
    is_archive_excluded(&relative, entry.file_type().is_dir())
}

fn is_archive_excluded(relative: &str, is_dir: bool) -> bool {
    let basename = relative.rsplit('/').next().unwrap_or(relative);
    if is_dir && WALK_EXCLUDED_DIRS.contains(&basename) {
        return true;
    }
    ARCHIVE_EXCLUDES.iter().any(|pattern| match *pattern {
        "*.pem" => basename_has_extension(basename, "pem"),
        "*.key" => basename_has_extension(basename, "key"),
        "*.p12" => basename_has_extension(basename, "p12"),
        "*.pfx" => basename_has_extension(basename, "pfx"),
        "*.tfstate" => basename_has_extension(basename, "tfstate"),
        "*.tfstate.*" => basename.contains(".tfstate."),
        "*.sqlite" => basename_has_extension(basename, "sqlite"),
        "*.sqlite3" => basename_has_extension(basename, "sqlite3"),
        "*.db" => basename_has_extension(basename, "db"),
        "*.log" => basename_has_extension(basename, "log"),
        "._*" => basename.starts_with("._"),
        ".env.*" => basename.starts_with(".env."),
        pattern => relative == pattern || relative.starts_with(&format!("{pattern}/")),
    })
}

fn basename_has_extension(basename: &str, extension: &str) -> bool {
    Path::new(basename)
        .extension()
        .is_some_and(|value| value.eq_ignore_ascii_case(extension))
}