Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/api/updates.zig
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ pub fn handleApplyUpdateRuntime(
const new_bin_path = paths.binary(allocator, component, latest_tag) catch return serverError();
defer allocator.free(new_bin_path);

downloader.download(allocator, asset.browser_download_url, new_bin_path) catch return .{
downloader.downloadIfMissing(allocator, asset.browser_download_url, new_bin_path) catch return .{
.status = "502 Bad Gateway",
.content_type = "application/json",
.body = "{\"error\":\"failed to download latest binary\"}",
Expand Down
7 changes: 1 addition & 6 deletions src/api/wizard.zig
Original file line number Diff line number Diff line change
Expand Up @@ -480,12 +480,7 @@ fn fetchLatestComponentBinary(allocator: std.mem.Allocator, component: []const u
paths.ensureDirs() catch return null;
const bin_path = paths.binary(allocator, component, release.value.tag_name) catch return null;

if (std_compat.fs.openFileAbsolute(bin_path, .{})) |f| {
f.close();
return bin_path;
} else |_| {}

downloader.download(allocator, asset.browser_download_url, bin_path) catch {
downloader.downloadIfMissing(allocator, asset.browser_download_url, bin_path) catch {
allocator.free(bin_path);
return null;
};
Expand Down
48 changes: 48 additions & 0 deletions src/installer/downloader.zig
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ pub fn computeSha256(allocator: std.mem.Allocator, file_path: []const u8) ![64]u

// ─── Download ────────────────────────────────────────────────────────────────

pub fn fileExists(file_path: []const u8) bool {
const file = std_compat.fs.openFileAbsolute(file_path, .{}) catch return false;
file.close();
return true;
}

/// Download a file from `url` to `dest_path` using curl.
///
/// Uses an atomic write pattern: downloads to `dest_path.tmp`, then renames
Expand Down Expand Up @@ -76,6 +82,12 @@ pub fn download(allocator: std.mem.Allocator, url: []const u8, dest_path: []cons
}
}

/// Download a file only when `dest_path` is not already present.
pub fn downloadIfMissing(allocator: std.mem.Allocator, url: []const u8, dest_path: []const u8) !void {
if (fileExists(dest_path)) return;
try download(allocator, url, dest_path);
}

/// Download a file and verify its SHA256 checksum.
///
/// If the checksum does not match `expected_sha256`, the downloaded file is
Expand Down Expand Up @@ -176,6 +188,42 @@ test "download performs atomic rename and sets executable bit" {
}
}

test "downloadIfMissing keeps an existing destination" {
const allocator = std.testing.allocator;

const tmp_dir = "/tmp/test-nullhub-downloader-skip-existing";
std_compat.fs.deleteTreeAbsolute(tmp_dir) catch {};
try std_compat.fs.makeDirAbsolute(tmp_dir);
defer std_compat.fs.deleteTreeAbsolute(tmp_dir) catch {};

const src_path = try std.fmt.allocPrint(allocator, "{s}/source.txt", .{tmp_dir});
defer allocator.free(src_path);
const dest_path = try std.fmt.allocPrint(allocator, "{s}/binary", .{tmp_dir});
defer allocator.free(dest_path);

{
var file = try std_compat.fs.createFileAbsolute(src_path, .{});
defer file.close();
try file.writeAll("fresh content");
}
{
var file = try std_compat.fs.createFileAbsolute(dest_path, .{});
defer file.close();
try file.writeAll("cached content");
}

const file_url = try std.fmt.allocPrint(allocator, "file://{s}", .{src_path});
defer allocator.free(file_url);

try downloadIfMissing(allocator, file_url, dest_path);

var file = try std_compat.fs.openFileAbsolute(dest_path, .{});
defer file.close();
var buf: [256]u8 = undefined;
const n = try file.readAll(&buf);
try std.testing.expectEqualStrings("cached content", buf[0..n]);
}

test "downloadWithSha256 detects checksum mismatch" {
const allocator = std.testing.allocator;

Expand Down
7 changes: 3 additions & 4 deletions src/installer/orchestrator.zig
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ pub fn install(
resolved_version = allocator.dupe(u8, release.value.tag_name) catch return error.FetchFailed;
const bin_path = p.binary(allocator, opts.component, resolved_version.?) catch return error.DownloadFailed;
resolved_bin_path = bin_path;
downloader.download(allocator, asset.browser_download_url, bin_path) catch {
downloader.downloadIfMissing(allocator, asset.browser_download_url, bin_path) catch {
allocator.free(bin_path);
resolved_bin_path = null;
allocator.free(resolved_version.?);
Expand Down Expand Up @@ -919,10 +919,9 @@ fn stageLocalBinary(allocator: std.mem.Allocator, p: paths_mod.Paths, component:
const bin_path = p.binary(allocator, component, version) catch return null;
errdefer allocator.free(bin_path);

if (std_compat.fs.openFileAbsolute(bin_path, .{})) |f| {
f.close();
if (downloader.fileExists(bin_path)) {
return .{ .version = version, .bin_path = bin_path };
} else |_| {}
}

std_compat.fs.copyFileAbsolute(local_path, bin_path, .{}) catch return null;
if (comptime std_compat.fs.has_executable_bit) {
Expand Down
Loading