From 58d09b1f7e23174eae1da30193097ab99217787b Mon Sep 17 00:00:00 2001 From: Emil Lerch Date: Thu, 18 Dec 2025 11:23:38 -0800 Subject: [PATCH] AI: download geoip on startup if it does not exist --- zig/src/location/geolite_downloader.zig | 72 +++++++++++++++++++++++++ zig/src/main.zig | 4 ++ 2 files changed, 76 insertions(+) create mode 100644 zig/src/location/geolite_downloader.zig diff --git a/zig/src/location/geolite_downloader.zig b/zig/src/location/geolite_downloader.zig new file mode 100644 index 0000000..f4aaf2c --- /dev/null +++ b/zig/src/location/geolite_downloader.zig @@ -0,0 +1,72 @@ +const std = @import("std"); + +pub fn ensureDatabase(allocator: std.mem.Allocator, path: []const u8) !void { + std.fs.cwd().access(path, .{}) catch { + std.log.info("GeoLite2 database not found at {s}, downloading...", .{path}); + try downloadDatabase(allocator, path); + std.log.info("GeoLite2 database downloaded successfully", .{}); + return; + }; +} + +fn downloadDatabase(allocator: std.mem.Allocator, path: []const u8) !void { + const latest_url = try getLatestReleaseUrl(allocator); + defer allocator.free(latest_url); + + var client: std.http.Client = .{ .allocator = allocator }; + defer client.deinit(); + + const uri = try std.Uri.parse(latest_url); + const response_buf = try allocator.alloc(u8, 64 * 1024 * 1024); + defer allocator.free(response_buf); + + var writer = std.Io.Writer.fixed(response_buf); + const result = try client.fetch(.{ + .location = .{ .uri = uri }, + .method = .GET, + .response_writer = &writer, + }); + + if (result.status != .ok) return error.DownloadFailed; + + const file = try std.fs.cwd().createFile(path, .{}); + defer file.close(); + try file.writeAll(response_buf[0..writer.end]); +} + +fn getLatestReleaseUrl(allocator: std.mem.Allocator) ![]const u8 { + var client: std.http.Client = .{ .allocator = allocator }; + defer client.deinit(); + + const api_url = "https://api.github.com/repos/P3TERX/GeoLite.mmdb/releases/latest"; + const uri = try std.Uri.parse(api_url); + + const response_buf = try allocator.alloc(u8, 1024 * 1024); + defer allocator.free(response_buf); + + var writer = std.Io.Writer.fixed(response_buf); + const result = try client.fetch(.{ + .location = .{ .uri = uri }, + .method = .GET, + .response_writer = &writer, + .extra_headers = &.{ + .{ .name = "User-Agent", .value = "wttr.in" }, + }, + }); + + if (result.status != .ok) return error.ApiFailed; + + const json_data = response_buf[0..writer.end]; + const parsed = try std.json.parseFromSlice(std.json.Value, allocator, json_data, .{}); + defer parsed.deinit(); + + const assets = parsed.value.object.get("assets") orelse return error.NoAssets; + for (assets.array.items) |asset| { + const name = asset.object.get("name") orelse continue; + if (std.mem.eql(u8, name.string, "GeoLite2-City.mmdb")) { + const url = asset.object.get("browser_download_url") orelse return error.NoDownloadUrl; + return allocator.dupe(u8, url.string); + } + } + return error.DatabaseNotFound; +} diff --git a/zig/src/main.zig b/zig/src/main.zig index c39e3e8..3b922bc 100644 --- a/zig/src/main.zig +++ b/zig/src/main.zig @@ -9,6 +9,7 @@ const GeoIP = @import("location/geoip.zig").GeoIP; const GeoCache = @import("location/geocache.zig").GeoCache; const AirportDB = @import("location/airports.zig").AirportDB; const Resolver = @import("location/resolver.zig").Resolver; +const geolite_downloader = @import("location/geolite_downloader.zig"); pub const std_options: std.Options = .{ .log_level = .info, @@ -40,6 +41,9 @@ pub fn main() !void { } try stdout.flush(); + // Ensure GeoLite2 database exists + try geolite_downloader.ensureDatabase(allocator, cfg.geolite_path); + // Initialize GeoIP database var geoip = GeoIP.init(cfg.geolite_path) catch |err| { std.log.warn("Failed to load GeoIP database: {}", .{err});