use std::path::Path;
use std::process::Command;
use std::time::{SystemTime, UNIX_EPOCH};
use serde::Serialize;
use void_core::config;
use void_core::pipeline::{export_commit_to_car, ExportCarOptions};
use crate::context::open_repo;
use crate::output::{run_command, CliError, CliOptions};
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PinRemoteOutput {
pub remote: String,
pub commit: String,
pub blocks_exported: usize,
pub car_size: u64,
pub host: String,
}
pub struct PinRemoteArgs {
pub remote: String,
pub commit: Option<String>,
}
fn expand_tilde(path: &str) -> String {
if path.starts_with('~') {
if let Some(home) = dirs::home_dir() {
return path.replacen('~', &home.to_string_lossy(), 1);
}
}
path.to_string()
}
pub fn run(cwd: &Path, args: PinRemoteArgs, opts: &CliOptions) -> Result<(), CliError> {
run_command("pin-remote", opts, |ctx| {
let repo = open_repo(cwd)?;
let void_dir = repo.void_dir().to_owned();
let cfg = config::load(void_dir.as_std_path())
.map_err(|e| CliError::internal(format!("failed to load config: {e}")))?;
let remote = cfg.remote.get(&args.remote).ok_or_else(|| {
CliError::not_found(format!(
"Remote '{}' not found. Use 'void remote add' to configure it.",
args.remote
))
})?;
let ssh_host = remote.host.as_deref().ok_or_else(|| {
CliError::invalid_args(format!("Remote '{}' has no host configured", args.remote))
})?;
let ssh_user = remote.user.as_deref().unwrap_or("root");
let ssh_key = remote
.key_path
.as_deref()
.map(expand_tilde)
.unwrap_or_else(|| {
dirs::home_dir()
.map(|h| h.join(".ssh/id_rsa").to_string_lossy().to_string())
.unwrap_or_else(|| "~/.ssh/id_rsa".to_string())
});
let host_display = format!("{}@{}", ssh_user, ssh_host);
if !ctx.use_json() {
ctx.info("Exporting commit to CAR file...");
}
let ts = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis();
let temp_car = std::env::temp_dir().join(format!("void-{}.car", ts));
let export_opts = ExportCarOptions {
ctx: repo.context().clone(),
commit_cid: args.commit.clone(),
};
let export_result = export_commit_to_car(export_opts, &temp_car)
.map_err(|e| CliError::internal(format!("failed to export CAR: {e}")))?;
if !ctx.use_json() {
ctx.info(format!(
"Exported {} blocks ({} bytes)",
export_result.blocks_exported, export_result.car_size
));
}
if !ctx.use_json() {
ctx.info(format!("Copying to {}...", host_display));
}
let remote_car_path = format!("/tmp/void-{}.car", ts);
let scp_dest = format!("{}@{}:{}", ssh_user, ssh_host, remote_car_path);
let scp_status = Command::new("scp")
.args([
"-i",
&ssh_key,
"-o",
"StrictHostKeyChecking=accept-new",
&temp_car.to_string_lossy(),
&scp_dest,
])
.output();
let _ = std::fs::remove_file(&temp_car);
let scp_output =
scp_status.map_err(|e| CliError::internal(format!("failed to run scp: {e}")))?;
if !scp_output.status.success() {
let stderr = String::from_utf8_lossy(&scp_output.stderr);
return Err(CliError::internal(format!("SCP failed: {}", stderr.trim())));
}
if !ctx.use_json() {
ctx.info("Importing and pinning on remote...");
}
let ssh_cmd = format!(
"sudo IPFS_PATH=/home/ipfs/.ipfs ipfs dag import {} && \
sudo IPFS_PATH=/home/ipfs/.ipfs ipfs pin add {} && \
rm {}",
remote_car_path, export_result.commit_cid, remote_car_path
);
let ssh_output = Command::new("ssh")
.args([
"-i",
&ssh_key,
"-o",
"StrictHostKeyChecking=accept-new",
&format!("{}@{}", ssh_user, ssh_host),
&ssh_cmd,
])
.output()
.map_err(|e| CliError::internal(format!("failed to run ssh: {e}")))?;
if !ssh_output.status.success() {
let stderr = String::from_utf8_lossy(&ssh_output.stderr);
return Err(CliError::internal(format!(
"SSH command failed: {}",
stderr.trim()
)));
}
if !ctx.use_json() {
ctx.info(format!(
"Pinned {} on {}",
export_result.commit_cid, host_display
));
}
Ok(PinRemoteOutput {
remote: args.remote,
commit: export_result.commit_cid,
blocks_exported: export_result.blocks_exported,
car_size: export_result.car_size,
host: host_display,
})
})
}