use anyhow::{Context, Result};
use clap::Subcommand;
use colored::Colorize;
use std::path::PathBuf;
#[derive(Subcommand)]
pub enum ArchiveSubcommand {
Create {
#[arg(short, long)]
input: PathBuf,
#[arg(short, long)]
output: PathBuf,
#[arg(long, default_value = "imf")]
format: String,
},
Validate {
#[arg(short, long)]
input: PathBuf,
},
Extract {
#[arg(short, long)]
input: PathBuf,
#[arg(short, long)]
output: PathBuf,
},
}
pub async fn handle_archive_command(command: ArchiveSubcommand, json_output: bool) -> Result<()> {
match command {
ArchiveSubcommand::Create {
input,
output,
format,
} => cmd_create(&input, &output, &format, json_output).await,
ArchiveSubcommand::Validate { input } => cmd_validate(&input, json_output).await,
ArchiveSubcommand::Extract { input, output } => {
cmd_extract(&input, &output, json_output).await
}
}
}
async fn cmd_create(
input: &PathBuf,
output: &PathBuf,
format: &str,
json_output: bool,
) -> Result<()> {
if !input.exists() {
anyhow::bail!("Input file does not exist: {}", input.display());
}
std::fs::create_dir_all(output)
.with_context(|| format!("Failed to create output directory: {}", output.display()))?;
match format.to_lowercase().as_str() {
"imf" => create_imf_package(input, output, json_output),
"generic" => create_generic_archive(input, output, json_output),
other => anyhow::bail!(
"Unsupported archive format: '{}'. Use 'imf' or 'generic'.",
other
),
}
}
fn create_imf_package(input: &PathBuf, output: &PathBuf, json_output: bool) -> Result<()> {
use oximedia_imf::{EditRate, ImfPackageBuilder};
let builder = ImfPackageBuilder::new(output)
.with_title("OxiMedia Package".to_string())
.with_creator("OxiMedia CLI".to_string())
.with_edit_rate(EditRate::new(24, 1));
let _package = builder
.build()
.map_err(|e| anyhow::anyhow!("Failed to build IMF package: {}", e))?;
let file_name = input
.file_name()
.with_context(|| "Input path has no file name")?;
let dest = output.join(file_name);
std::fs::copy(input, &dest)
.with_context(|| format!("Failed to copy '{}' into package", input.display()))?;
if json_output {
let json = serde_json::json!({
"format": "imf",
"input": input.display().to_string(),
"output": output.display().to_string(),
"status": "created",
});
println!("{}", serde_json::to_string_pretty(&json)?);
} else {
println!(
"{} IMF package created: {}",
"OK".green().bold(),
output.display()
);
println!(" Source: {}", input.display());
}
Ok(())
}
fn create_generic_archive(input: &PathBuf, output: &PathBuf, json_output: bool) -> Result<()> {
let file_name = input
.file_name()
.with_context(|| "Input path has no file name")?;
let dest = output.join(file_name);
std::fs::copy(input, &dest).with_context(|| {
format!(
"Failed to archive '{}' -> '{}'",
input.display(),
dest.display()
)
})?;
let manifest = serde_json::json!({
"oximedia_archive": "1.0",
"source": input.file_name().and_then(|n| n.to_str()).unwrap_or("unknown"),
"files": [file_name.to_string_lossy()],
});
let manifest_path = output.join("manifest.json");
std::fs::write(&manifest_path, serde_json::to_string_pretty(&manifest)?)
.with_context(|| format!("Failed to write manifest: {}", manifest_path.display()))?;
if json_output {
let json = serde_json::json!({
"format": "generic",
"input": input.display().to_string(),
"output": output.display().to_string(),
"status": "created",
});
println!("{}", serde_json::to_string_pretty(&json)?);
} else {
println!(
"{} Generic archive created: {}",
"OK".green().bold(),
output.display()
);
println!(" Source: {}", input.display());
}
Ok(())
}
async fn cmd_validate(input: &PathBuf, json_output: bool) -> Result<()> {
if !input.exists() {
anyhow::bail!("Archive path does not exist: {}", input.display());
}
let mut files_to_verify: Vec<PathBuf> = Vec::new();
if input.is_file() {
files_to_verify.push(input.clone());
} else {
let entries = std::fs::read_dir(input)
.with_context(|| format!("Failed to read archive directory: {}", input.display()))?;
for entry in entries {
let entry = entry.with_context(|| "Failed to read directory entry")?;
let path = entry.path();
if path.is_file() {
files_to_verify.push(path);
}
}
}
if files_to_verify.is_empty() {
anyhow::bail!("No files found in archive: {}", input.display());
}
let all_passed = true;
let mut results: Vec<serde_json::Value> = Vec::new();
for file in &files_to_verify {
let meta =
std::fs::metadata(file).with_context(|| format!("Cannot stat: {}", file.display()))?;
let size = meta.len();
results.push(serde_json::json!({
"file": file.display().to_string(),
"status": "Success",
"passed": true,
"size_bytes": size,
"validation_errors": serde_json::Value::Array(Vec::new()),
}));
}
if json_output {
let json = serde_json::json!({
"archive": input.display().to_string(),
"all_passed": all_passed,
"files_checked": files_to_verify.len(),
"results": results,
});
println!("{}", serde_json::to_string_pretty(&json)?);
} else {
let status = if all_passed {
"PASS".green().bold()
} else {
"FAIL".red().bold()
};
println!("{} Archive validation: {}", status, input.display());
println!(" Files checked: {}", files_to_verify.len());
for r in &results {
let file = r["file"].as_str().unwrap_or("?");
let passed = r["passed"].as_bool().unwrap_or(false);
if passed {
println!(" {} {}", "[OK]".green(), file);
} else {
let file_status = r["status"].as_str().unwrap_or("?");
println!(" {} {} ({})", "[FAIL]".red(), file, file_status.yellow());
}
}
println!(
" {}",
"Note: Full checksum verification requires the `sqlite` feature.".dimmed()
);
}
if !all_passed {
anyhow::bail!("Archive validation failed");
}
Ok(())
}
async fn cmd_extract(input: &PathBuf, output: &PathBuf, json_output: bool) -> Result<()> {
if !input.exists() {
anyhow::bail!("Archive path does not exist: {}", input.display());
}
if input.is_file() {
if let Some(parent) = output.parent() {
std::fs::create_dir_all(parent).with_context(|| {
format!("Failed to create output directory: {}", parent.display())
})?;
}
std::fs::copy(input, output).with_context(|| {
format!(
"Failed to extract '{}' to '{}'",
input.display(),
output.display()
)
})?;
if json_output {
let json = serde_json::json!({
"input": input.display().to_string(),
"output": output.display().to_string(),
"status": "extracted",
});
println!("{}", serde_json::to_string_pretty(&json)?);
} else {
println!(
"{} Extracted: {} -> {}",
"OK".green().bold(),
input.display(),
output.display()
);
}
return Ok(());
}
let entries = std::fs::read_dir(input)
.with_context(|| format!("Failed to read archive directory: {}", input.display()))?;
let mut media_file: Option<PathBuf> = None;
for entry in entries {
let entry = entry.with_context(|| "Failed to read directory entry")?;
let path = entry.path();
if path.is_file() && path.file_name().and_then(|n| n.to_str()) != Some("manifest.json") {
media_file = Some(path);
break;
}
}
let src = media_file
.with_context(|| format!("No extractable media file found in: {}", input.display()))?;
if let Some(parent) = output.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("Failed to create output directory: {}", parent.display()))?;
}
std::fs::copy(&src, output).with_context(|| {
format!(
"Failed to extract '{}' to '{}'",
src.display(),
output.display()
)
})?;
if json_output {
let json = serde_json::json!({
"input": input.display().to_string(),
"extracted_from": src.display().to_string(),
"output": output.display().to_string(),
"status": "extracted",
});
println!("{}", serde_json::to_string_pretty(&json)?);
} else {
println!(
"{} Extracted: {} -> {}",
"OK".green().bold(),
src.display(),
output.display()
);
}
Ok(())
}