| 1382 | } |
| 1383 | |
| 1384 | nlohmann::json CloudTunnel::StartImageDownload(const nlohmann::json& payload) { |
| 1385 | const auto image = ImageFromJson(payload.value("image", nlohmann::json::object())); |
| 1386 | if (image.id.empty() || image.files.empty()) return Error("image_invalid", "invalid image metadata"); |
| 1387 | |
| 1388 | // Hard guard against host/image platform mismatch. Without this, a buggy |
| 1389 | // or stale console can hand us an x86_64 rootfs on an arm64 host, and we |
| 1390 | // happily download ~1.6 GB only to fail at boot with "invalid ARM64 Image |
| 1391 | // magic". Platform identity comes from the image manifest (authoritative) |
| 1392 | // and is matched against the host's compiled-in arch. |
| 1393 | const std::string host_platform = image_source::HostPlatform(); |
| 1394 | const std::string image_platform = image_source::NormalizePlatform(image.platform); |
| 1395 | if (image_platform != host_platform) { |
| 1396 | return Error("image_arch_mismatch", |
| 1397 | "image platform '" + image_platform + |
| 1398 | "' does not match host platform '" + host_platform + "'"); |
| 1399 | } |
| 1400 | |
| 1401 | const std::string images_dir = ImagesDir(config_); |
| 1402 | if (image_source::IsImageCached(images_dir, image)) { |
| 1403 | return {{"job_id", ""}, {"cache_id", image.CacheId()}, {"status", "done"}}; |
| 1404 | } |
| 1405 | |
| 1406 | const std::string job_id = GenerateUuid(); |
| 1407 | { |
| 1408 | std::lock_guard<std::mutex> lock(download_mu_); |
| 1409 | for (const auto& [id, job] : download_jobs_) { |
| 1410 | if (job.cache_id == image.CacheId() && (job.status == "running" || job.status == "pending")) { |
| 1411 | return {{"job_id", id}, {"cache_id", job.cache_id}, {"status", job.status}}; |
| 1412 | } |
| 1413 | } |
| 1414 | DownloadJob job; |
| 1415 | job.job_id = job_id; |
| 1416 | job.cache_id = image.CacheId(); |
| 1417 | job.image_name = image.display_name; |
| 1418 | job.status = "running"; |
| 1419 | job.total_bytes = image.TotalSize(); |
| 1420 | job.started_at = UnixNow(); |
| 1421 | job.updated_at = job.started_at; |
| 1422 | download_jobs_[job_id] = job; |
| 1423 | } |
| 1424 | |
| 1425 | std::thread([this, image, job_id, images_dir]() { |
| 1426 | pthread_setname_np(pthread_self(), "img-download"); |
| 1427 | const std::string cache_dir = image_source::ImageCacheDir(images_dir, image); |
| 1428 | const auto started = std::chrono::steady_clock::now(); |
| 1429 | // Browser-side progress is rate-limited to ~2Hz to avoid flooding the |
| 1430 | // WS with envelope traffic when curl reports tiny deltas faster than |
| 1431 | // the user can perceive. |
| 1432 | auto last_progress_push = std::chrono::steady_clock::now() - |
| 1433 | std::chrono::seconds(1); |
| 1434 | std::error_code ec; |
| 1435 | std::filesystem::create_directories(cache_dir, ec); |
| 1436 | bool ok = !ec; |
| 1437 | std::string error = ec ? ec.message() : ""; |
| 1438 | uint64_t done = 0; |
| 1439 | for (const auto& file : image.files) { |
| 1440 | if (!ok) break; |
| 1441 | const auto dest = std::filesystem::path(cache_dir) / file.name; |
nothing calls this directly
no test coverage detected