use anyhow::{bail, Context, Result};
use clap::Args;
use std::path::{Path, PathBuf};
use octocode::lock::IndexLock;
use octocode::storage::ensure_project_storage_exists;
const MARKER_FILE: &str = "octocode-export.marker";
const MARKER_PREFIX: &str = "octocode-export-v";
#[derive(Args, Debug)]
pub struct ImportArgs {
pub file: PathBuf,
}
pub async fn execute(args: &ImportArgs) -> Result<()> {
let current_dir = std::env::current_dir()?;
if !args.file.exists() {
bail!("Import file not found: {}", args.file.display());
}
if !args.file.is_file() {
bail!("Import path is not a file: {}", args.file.display());
}
let project_storage = ensure_project_storage_exists(¤t_dir)?;
let pid = std::process::id();
let temp_dir = project_storage.join(format!(".octocode-import-{}", pid));
if temp_dir.exists() {
let _ = std::fs::remove_dir_all(&temp_dir);
}
std::fs::create_dir_all(&temp_dir)
.with_context(|| format!("Failed to create temp dir {}", temp_dir.display()))?;
println!("Acquiring index lock...");
let mut lock = IndexLock::new(¤t_dir)?;
lock.acquire_async().await?;
let file = args.file.clone();
let temp_clone = temp_dir.clone();
let extract_res =
tokio::task::spawn_blocking(move || extract_and_validate(&file, &temp_clone)).await?;
if let Err(e) = extract_res {
let _ = std::fs::remove_dir_all(&temp_dir);
lock.release()?;
return Err(e);
}
let install_res = install_atomic(&temp_dir, &project_storage, pid);
let _ = std::fs::remove_dir_all(&temp_dir);
lock.release()?;
install_res?;
println!("Imported into {}", project_storage.display());
Ok(())
}
fn extract_and_validate(file: &Path, dest: &Path) -> Result<()> {
let f =
std::fs::File::open(file).with_context(|| format!("Failed to open {}", file.display()))?;
let decoder = zstd::Decoder::new(f).context("Not a valid zstd-compressed archive")?;
let mut archive = tar::Archive::new(decoder);
archive
.unpack(dest)
.context("Failed to extract tar.zst archive")?;
let marker = dest.join(MARKER_FILE);
if !marker.exists() {
bail!(
"Invalid octocode export: missing marker file '{}'",
MARKER_FILE
);
}
let content = std::fs::read_to_string(&marker)
.with_context(|| format!("Failed to read marker {}", marker.display()))?;
if !content.trim_start().starts_with(MARKER_PREFIX) {
bail!(
"Invalid octocode export: marker content does not start with '{}'",
MARKER_PREFIX
);
}
if !dest.join("storage").is_dir() {
bail!("Invalid octocode export: missing 'storage' directory in archive");
}
Ok(())
}
fn install_atomic(temp_dir: &Path, project_storage: &Path, pid: u32) -> Result<()> {
swap_dir(
&temp_dir.join("storage"),
&project_storage.join("storage"),
&project_storage.join(format!(".octocode-backup-storage-{}", pid)),
true,
)?;
let extracted_branches = temp_dir.join("branches");
if extracted_branches.exists() {
swap_dir(
&extracted_branches,
&project_storage.join("branches"),
&project_storage.join(format!(".octocode-backup-branches-{}", pid)),
false,
)?;
}
Ok(())
}
fn swap_dir(source: &Path, target: &Path, backup: &Path, required: bool) -> Result<()> {
if !source.exists() {
if required {
bail!(
"Missing required directory in archive: {}",
source.display()
);
}
return Ok(());
}
let had_target = target.exists();
if had_target {
std::fs::rename(target, backup).with_context(|| {
format!(
"Failed to back up existing {} to {}",
target.display(),
backup.display()
)
})?;
}
if let Err(e) = std::fs::rename(source, target) {
if had_target {
let _ = std::fs::rename(backup, target);
}
return Err(anyhow::anyhow!(
"Failed to install {} into {}: {}",
source.display(),
target.display(),
e
));
}
if had_target {
let _ = std::fs::remove_dir_all(backup);
}
Ok(())
}