srcdmp 0.1.0-alpha.1

Amazing code snapshot tool and library
Documentation
//! The `srcdmp` command-line interface.
//!
//! Provides subcommands for wrapping directories into `.srcdmp` archives,
//! unwrapping archives back to files, browsing archives interactively,
//! verifying integrity, and inspecting metadata.

use clish::prelude::*;
use log::{error, finished, opening, packed, packing, unpacking, verified, writing};
use srcdmp::cli::{log, viewer};
use srcdmp::{Codec, Dump};

/// Wrap a directory into a `.srcdmp` snapshot.
#[command(
    help = "Wrap a directory into a .srcdmp snapshot",
    param(directory, help = "Directory to wrap"),
    param(output, help = "Output file path"),
    param(codec, help = "Codec to use", default = "c4", choices = ["7", "8", "c4"]),
    param(ignore, help = "Glob patterns to ignore"),
    param(gitignore, help = "Respect .gitignore"),
    param(from_cargo, name = "from-cargo", help = "Auto-name from Cargo.toml"),
)]
fn wrap(
    directory: Pos<String>,
    output: Pos<Option<String>>,
    codec: Named<String>,
    ignore: Named<Vec<String>>,
    gitignore: bool,
    from_cargo: bool,
) {
    let codec_str = codec;
    let codec = match codec_str.as_str() {
        "7" => Codec::Generic7,
        "8" => Codec::Generic8,
        "c4" => Codec::Compact4,
        _ => unreachable!(),
    };

    let mut builder = Dump::from_dir(&directory).codec(codec);

    if gitignore {
        builder = builder.gitignore();
    }

    for pattern in &ignore {
        builder = builder.ignore(pattern);
    }

    packing(&format!(
        "{directory} to srcdmp (SDMP-{})",
        codec_str.to_ascii_uppercase()
    ));

    let dump = builder.build().unwrap_or_else(|e| {
        error(&e.to_string());
        std::process::exit(1);
    });

    packed(&format!(
        "{} files, {} bytes",
        dump.file_count(),
        dump.original_size()
    ));

    if from_cargo {
        dump.from_cargo().write_to(".").unwrap_or_else(|e| {
            error(&e.to_string());
            std::process::exit(1);
        });
        finished("snapshot written");
    } else if let Some(path) = output.as_deref() {
        dump.write(path).unwrap_or_else(|e| {
            error(&e.to_string());
            std::process::exit(1);
        });
        writing(path);
        finished("snapshot written");
    } else {
        error("provide an output path or use --from-cargo");
        std::process::exit(1);
    }
}

/// Decode a `.srcdmp` archive into a human-readable log file.
#[command(
    help = "Decode a .srcdmp into a human readable log",
    param(input, help = "Input .srcdmp file"),
    param(output, help = "Output .log file")
)]
fn unwrap(input: Pos<String>, output: Pos<String>) {
    opening(&input);

    let dump = Dump::open(&input).unwrap_or_else(|e| {
        error(&e.to_string());
        std::process::exit(1);
    });

    dump.to_log(&output).unwrap_or_else(|e| {
        error(&e.to_string());
        std::process::exit(1);
    });

    finished(&format!("{} files → {}", dump.file_count(), output));
}

/// Reconstruct a directory tree from a `.srcdmp` or `.log` archive.
#[command(
    help = "Reconstruct a directory from a .srcdmp or .log",
    param(input, help = "Input .srcdmp file"),
    param(out_dir, help = "Output directory", name = "output", default = ".")
)]
fn unpack(input: Pos<String>, out_dir: Named<String>) {
    opening(&input);

    let dump = Dump::open(&input).unwrap_or_else(|e| {
        error(&e.to_string());
        std::process::exit(1);
    });

    unpacking(&format!("{} files -> {}", dump.file_count(), out_dir));

    dump.unpack_to(&out_dir).unwrap_or_else(|e| {
        error(&e.to_string());
        std::process::exit(1);
    });

    finished(&out_dir);
}

/// Print header metadata for a `.srcdmp` archive without decoding its contents.
#[command(
    help = "Print header metadata without decoding",
    param(input, help = "Input .srcdmp file")
)]
fn info(input: Pos<String>) {
    let dump = Dump::open(&input).unwrap_or_else(|e| {
        error(&e.to_string());
        std::process::exit(1);
    });

    log::info(&format!("codec:  {}", dump.codec_name()));
    log::info(&format!("files:  {}", dump.file_count()));
    log::info(&format!("size:   {} bytes", dump.original_size()));
}

/// Verify the integrity of all files in a `.srcdmp` archive.
#[command(
    help = "Verify file integrity",
    param(input, help = "Input .srcdmp file")
)]
fn check(input: Pos<String>) {
    opening(&input);

    let dump = Dump::open(&input).unwrap_or_else(|e| {
        error(&e.to_string());
        std::process::exit(1);
    });

    let mut ok = true;
    for path in dump.files() {
        match dump.unpack() {
            Ok(_) => verified(path),
            Err(e) => {
                error(&format!("{path} - {e}"));
                ok = false;
            }
        }
    }

    if ok {
        finished(&format!("all {} files ok", dump.file_count()));
    } else {
        error("file is corrupt");
        std::process::exit(1);
    }
}

/// Browse a `.srcdmp` archive interactively in the terminal.
#[command(
    help = "Browse a .srcdmp file interactively",
    param(input, help = "Input .srcdmp file")
)]
fn view(input: Pos<String>) {
    let dump = Dump::open(&input).unwrap_or_else(|e| {
        log::error(&e.to_string());
        std::process::exit(1);
    });

    let files = dump.unpack().unwrap_or_else(|e| {
        log::error(&e.to_string());
        std::process::exit(1);
    });

    let mut file_list: Vec<(String, String)> = files.into_iter().collect();
    file_list.sort_by(|a, b| a.0.cmp(&b.0));

    let filename = std::path::Path::new(&input)
        .file_name()
        .unwrap_or_default()
        .to_string_lossy()
        .to_string();

    viewer::Viewer::new(file_list, filename, dump.codec_name().to_string())
        .run()
        .unwrap_or_else(|e| {
            log::error(&e.to_string());
            std::process::exit(1);
        });
}

fn main() {
    app!().run();
}