use crate::commands::UploadArgs;
use crate::context::CliContext;
use crate::services::credentials::CredentialsService;
use anyhow::{Context, Result};
use std::path::{Path, PathBuf};
use tempfile::TempDir;
const BINARY_REGISTRY_PATH: &str = "/api/builds";
pub async fn handle_upload(ctx: &mut CliContext, args: &UploadArgs) -> Result<()> {
println!();
println!("📦 Mecha10 Robot Binary Upload");
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
println!();
if !ctx.is_project_initialized() {
println!("⚠️ Not in a Mecha10 project directory");
println!();
return Err(anyhow::anyhow!("Not in a Mecha10 project"));
}
let project = ctx.project()?;
let project_name = project.name()?;
let binary_path = args
.binary
.clone()
.unwrap_or_else(|| ctx.working_dir.join("dist").join(&project_name));
let config_path = args
.config
.clone()
.unwrap_or_else(|| ctx.working_dir.join("dist").join("mecha10.json"));
if !binary_path.exists() {
println!("❌ Binary not found: {}", binary_path.display());
println!();
println!("Build first with:");
println!(" mecha10 build robot --docker");
println!();
return Err(anyhow::anyhow!("Binary not found: {}", binary_path.display()));
}
if !config_path.exists() {
println!("❌ Config not found: {}", config_path.display());
println!();
println!("The mecha10.json should be in dist/ after building with:");
println!(" mecha10 build robot --docker");
println!();
return Err(anyhow::anyhow!("Config not found: {}", config_path.display()));
}
let project_config = ctx.load_project_config().await?;
let control_plane_url = project_config.environments.control_plane_url();
let dist_config_content = tokio::fs::read_to_string(&config_path).await?;
let dist_config: serde_json::Value = serde_json::from_str(&dist_config_content)?;
let config_name = dist_config["name"].as_str().unwrap_or(&project_name);
let robot_id = project_config.robot.id.clone();
let version = args
.version
.clone()
.unwrap_or_else(|| dist_config["version"].as_str().unwrap_or("0.1.0").to_string());
let arch = args.arch.clone().unwrap_or_else(|| detect_arch(&binary_path));
let platform = "linux";
let target = args
.target
.clone()
.unwrap_or_else(|| dist_config["build_target"].as_str().unwrap_or("robot").to_string());
let registry_url = args.registry_url.clone().unwrap_or_else(|| {
std::env::var("MECHA10_REGISTRY_URL")
.unwrap_or_else(|_| format!("{}{}", control_plane_url, BINARY_REGISTRY_PATH))
});
let binary_filename = binary_path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(&project_name);
let credentials_service = CredentialsService::new();
let credentials = credentials_service.load()?;
let created_by = credentials.as_ref().map(|c| c.user_id.clone());
let api_key = credentials.as_ref().map(|c| c.api_key.clone());
println!("Project: {}", config_name);
println!("Robot ID: {}", robot_id);
println!("Version: {}", version);
println!("Platform: {}", platform);
println!("Architecture: {}", arch);
println!("Target: {}", target);
println!("Binary: {}", binary_path.display());
println!("Config: {}", config_path.display());
println!("Registry: {}", registry_url);
println!("Uploaded by: {}", created_by.as_deref().unwrap_or("anonymous"));
println!();
println!("Creating tarball...");
let temp_dir = TempDir::new()?;
let tarball_name = format!("{}-v{}-{}-{}.tar.gz", config_name, version, platform, arch);
let tarball_path = temp_dir.path().join(&tarball_name);
let configs_dir = config_path.parent().map(|p| p.join("configs"));
let configs_dir_ref = configs_dir.as_ref().filter(|p| p.exists());
create_tarball(
&binary_path,
binary_filename,
&config_path,
configs_dir_ref.map(|p| p.as_path()),
&tarball_path,
)?;
if configs_dir_ref.is_some() {
println!(" Included configs/ directory");
}
let tarball_size = std::fs::metadata(&tarball_path)?.len();
println!();
println!("Tarball: {} ({} bytes)", tarball_name, tarball_size);
println!();
if args.dry_run {
println!("🔍 DRY RUN - No changes will be made");
println!();
println!("Would upload:");
println!(" POST {}", registry_url);
println!(" - name: {}", config_name);
println!(" - version: {}", version);
println!(" - platform: {}", platform);
println!(" - arch: {}", arch);
println!(" - target: {}", target);
println!(" - tags: robot,{}", robot_id);
println!(" - robot_id: {}", robot_id);
println!(" - created_by: {}", created_by.as_deref().unwrap_or("(none)"));
println!(" - file: {}", tarball_name);
println!();
return Ok(());
}
println!("Uploading tarball...");
println!();
let response = upload_to_registry(
®istry_url,
config_name,
&version,
platform,
&arch,
&target,
&robot_id,
created_by.as_deref(),
api_key.as_deref(),
&tarball_path,
&tarball_name,
)
.await?;
if let Some(binary_id) = response.get("id").and_then(|v| v.as_str()) {
println!("✅ Upload successful!");
println!();
println!("Binary ID: {}", binary_id);
println!("Download URL: {}/{}/download", registry_url, binary_id);
println!();
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
println!("✅ Upload Complete");
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
println!();
} else {
println!("❌ Upload failed!");
println!();
println!("Response: {}", serde_json::to_string_pretty(&response)?);
return Err(anyhow::anyhow!("Upload failed"));
}
Ok(())
}
fn detect_arch(binary_path: &Path) -> String {
if let Ok(bytes) = std::fs::read(binary_path) {
if bytes.len() >= 20 && &bytes[0..4] == b"\x7fELF" {
let e_machine = u16::from_le_bytes([bytes[18], bytes[19]]);
match e_machine {
0x3E => return "x86_64".to_string(), 0xB7 => return "aarch64".to_string(), _ => {}
}
}
}
"x86_64".to_string()
}
fn create_tarball(
binary_path: &Path,
binary_filename: &str,
config_path: &Path,
configs_dir: Option<&Path>,
tarball_path: &Path,
) -> Result<()> {
use flate2::write::GzEncoder;
use flate2::Compression;
use tar::Builder;
let tarball_file = std::fs::File::create(tarball_path)?;
let encoder = GzEncoder::new(tarball_file, Compression::default());
let mut builder = Builder::new(encoder);
builder
.append_path_with_name(binary_path, binary_filename)
.context("Failed to add binary to tarball")?;
builder
.append_path_with_name(config_path, "mecha10.json")
.context("Failed to add mecha10.json to tarball")?;
if let Some(configs) = configs_dir {
if configs.exists() && configs.is_dir() {
add_dir_to_tarball(&mut builder, configs, Path::new("configs"))
.context("Failed to add configs/ to tarball")?;
}
}
builder.finish()?;
Ok(())
}
fn add_dir_to_tarball<W: std::io::Write>(
builder: &mut tar::Builder<W>,
src_dir: &Path,
archive_path: &Path,
) -> Result<()> {
for entry in std::fs::read_dir(src_dir)? {
let entry = entry?;
let path = entry.path();
let name = entry.file_name();
let archive_entry = archive_path.join(&name);
if path.is_dir() {
add_dir_to_tarball(builder, &path, &archive_entry)?;
} else {
builder
.append_path_with_name(&path, &archive_entry)
.with_context(|| format!("Failed to add {} to tarball", path.display()))?;
}
}
Ok(())
}
async fn upload_to_registry(
registry_url: &str,
name: &str,
version: &str,
platform: &str,
arch: &str,
target: &str,
robot_id: &str,
created_by: Option<&str>,
api_key: Option<&str>,
tarball_path: &PathBuf,
tarball_name: &str,
) -> Result<serde_json::Value> {
let client = reqwest::Client::new();
let tarball_bytes = tokio::fs::read(tarball_path).await?;
let file_part = reqwest::multipart::Part::bytes(tarball_bytes)
.file_name(tarball_name.to_string())
.mime_str("application/gzip")?;
let mut form = reqwest::multipart::Form::new()
.text("name", name.to_string())
.text("description", format!("Robot binary bundle for {}", name))
.text("version", version.to_string())
.text("platform", platform.to_string())
.text("arch", arch.to_string())
.text("target", target.to_string())
.text("tags", format!("robot,{}", robot_id))
.text("robot_id", robot_id.to_string())
.part("file", file_part);
if let Some(user) = created_by {
form = form.text("created_by", user.to_string());
}
let mut request = client.post(registry_url).multipart(form);
if let (Some(key), Some(user_id)) = (api_key, created_by) {
request = request
.header("Authorization", format!("Bearer {}", key))
.header("X-User-ID", user_id);
}
let response = request.send().await.context("Failed to send upload request")?;
let status = response.status();
let body = response.text().await?;
if !status.is_success() {
return Err(anyhow::anyhow!("Upload failed with status {}: {}", status, body));
}
let json: serde_json::Value = serde_json::from_str(&body).context("Failed to parse response JSON")?;
Ok(json)
}