use crate::digest_tracker::DigestTracker;
use crate::extracted_image::ExtractedImage;
use crate::git::GitRepo;
use crate::image_metadata::ImageMetadata;
use crate::notifier::Notifier;
use crate::sources::Source;
use crate::successor_navigator::SuccessorNavigator;
use anyhow::{Context, Result};
use std::fs;
use std::path::Path;
pub struct ImageProcessor<S: Source> {
source: S,
notifier: Notifier,
}
impl<S: Source> ImageProcessor<S> {
pub fn new(source: S, notifier: Notifier) -> Self {
Self { source, notifier }
}
pub fn convert(&self, image_name: &str, output_dir: &Path) -> Result<()> {
self.notifier.info(&format!(
"Starting conversion of image with {} source: {}",
self.source.name(),
image_name
));
self.notifier
.debug(&format!("Output directory: {}", output_dir.display()));
let mut temp_dirs: Vec<tempfile::TempDir> = Vec::new();
self.notifier.info(&format!(
"Getting image tarball using {} source...",
self.source.name()
));
let (tarball_path, tarball_temp_dir) =
self.source.get_image_tarball(image_name, &self.notifier)?;
if let Some(temp_dir) = tarball_temp_dir {
temp_dirs.push(temp_dir);
}
self.notifier.info("Extracting image tarball...");
let extracted_image = ExtractedImage::from_tarball(&tarball_path, &self.notifier)?;
self.notifier.info("Analyzing image layers...");
let layers = extracted_image.layers()?;
self.notifier
.debug(&format!("Found {} layers in the image", layers.len()));
self.notifier.info("Extracting image metadata...");
let metadata = extracted_image.metadata(image_name)?;
self.notifier.debug(&format!("Image ID: {}", metadata.id));
self.notifier.info("Initializing Git repository...");
let os_arch = format!("{}-{}", metadata.os, metadata.architecture);
self.notifier.debug(&format!(
"Creating branch name for image '{}' with os-arch '{}' and digest: '{}'",
image_name, os_arch, metadata.id
));
let branch_name = self.source.branch_name(image_name, &os_arch, &metadata.id);
self.notifier
.debug(&format!("Generated branch name: '{branch_name}'"));
let repo = GitRepo::init_with_branch(output_dir, None)?;
let (start_from_commit, skip_layers) = if repo.exists_and_has_commits() {
self.notifier
.info("Existing repository detected, finding optimal branch point...");
let (branch_commit, matched_layers) =
SuccessorNavigator::find_branch_point(&repo, output_dir, &layers)?;
match branch_commit {
Some(commit) => {
self.notifier.info(&format!(
"Found optimal branch point at commit {commit}, skipping {matched_layers} matched layers"
));
(Some(commit), matched_layers)
}
None => {
self.notifier
.info("No matching path found, creating orphaned branch");
(None, 0)
}
}
} else {
self.notifier
.info("New repository, creating initial branch");
(None, 0)
};
if repo.branch_exists(&branch_name) && skip_layers == layers.len() {
self.notifier.info(&format!(
"Image '{image_name}' already exists as branch '{branch_name}' with identical content. Skipping duplicate processing."
));
return Ok(());
}
repo.create_branch(&branch_name, start_from_commit)?;
let rootfs_dir = output_dir.join("rootfs");
fs::create_dir_all(&rootfs_dir)?;
if layers.is_empty() {
self.notifier.warn("No layers found in the image");
self.notifier.info("Warning: No layers found in the image");
return Ok(());
}
let layers_with_tarballs = layers.iter().filter(|l| l.tarball_path.is_some()).count();
self.notifier.debug(&format!(
"Found {} layers with tarballs out of {} total layers",
layers_with_tarballs,
layers.len()
));
self.notifier.info("Preparing layer extraction...");
let rootfs_path = rootfs_dir.clone();
self.notifier.debug(&format!(
"Processing {} layers, {} with tarballs",
layers.len(),
layers_with_tarballs
));
let mut new_digest_tracker = if let Some(start_commit) = start_from_commit {
match repo.read_file_from_commit(start_commit, "Image.md") {
Ok(content) => {
let image_metadata =
crate::image_metadata::ImageMetadata::parse_markdown(&content)
.context("Failed to parse existing Image.md")?;
DigestTracker {
layer_digests: image_metadata.layer_digests,
}
}
Err(_) => {
DigestTracker::new()
}
}
} else {
DigestTracker::new()
};
let mut structured_metadata = ImageMetadata::new(None, None);
structured_metadata.update_layer_digests(&new_digest_tracker);
let layers_to_process = layers.len() - skip_layers;
self.notifier.info(&format!(
"Processing {layers_to_process} layers (skipping {skip_layers} matched layers)..."
));
for (i, layer) in layers.iter().enumerate().skip(skip_layers) {
self.notifier.info(&format!(
"Layer {}/{}: {}",
i + 1,
layers.len(),
layer.command
));
self.notifier.debug(&format!(
"Layer has tarball: {}",
layer.tarball_path.is_some()
));
if new_digest_tracker.layer_matches(i, layer) {
self.notifier.debug(&format!(
"Layer {} already exists with same digest, skipping unpacking",
i + 1
));
continue;
}
if layer.tarball_path.is_none() {
let commit_message = if layer.is_empty {
format!("⚪️ - {}", layer.command)
} else {
format!("⚫ - {}", layer.command)
};
new_digest_tracker.add_layer(
new_digest_tracker.layer_digests.len(),
layer.digest.clone(),
layer.command.clone(),
layer.created_at.to_rfc3339(),
layer.is_empty,
layer.comment.clone(),
);
structured_metadata.update_layer_digests(&new_digest_tracker);
let metadata_path = output_dir.join("Image.md");
structured_metadata.save_markdown(&metadata_path)?;
self.notifier.debug(&format!(
"Creating empty commit for layer: {}",
layer.command
));
repo.commit_all_changes(&commit_message)?;
continue;
}
let layer_tarball = layer.tarball_path.as_ref().unwrap();
self.notifier
.info(&format!("Extracting layer {}/{}", i + 1, layers.len()));
self.notifier
.debug(&format!("Extracting tarball: {layer_tarball:?}"));
fs::create_dir_all(&rootfs_path)?;
extracted_image.extract_layer_to(layer_tarball, &rootfs_path)?;
new_digest_tracker.add_layer(
new_digest_tracker.layer_digests.len(),
layer.digest.clone(),
layer.command.clone(),
layer.created_at.to_rfc3339(),
false,
layer.comment.clone(),
);
structured_metadata.update_layer_digests(&new_digest_tracker);
let metadata_path = output_dir.join("Image.md");
structured_metadata.save_markdown(&metadata_path)?;
self.notifier
.info(&format!("Committing layer {}/{}", i + 1, layers.len()));
repo.commit_all_changes(&format!("🟢 - {}", layer.command))?;
}
self.notifier.info("Creating metadata commit...");
let complete_metadata =
ImageMetadata::from_legacy(&metadata, &new_digest_tracker, image_name);
let metadata_path = output_dir.join("Image.md");
complete_metadata.save_markdown(&metadata_path)?;
repo.commit_all_changes("🛠️ - Metadata")?;
let msg = format!(
"Successfully converted image '{}' to Git repository at '{}'",
image_name,
output_dir.display()
);
self.notifier.info(&msg);
Ok(())
}
}