add .tmp/ to gitignore

This commit is contained in:
Emil Lerch 2026-08-04 16:55:15 -07:00
parent a5a9d0d7d7
commit 6bc5b5a209
Signed by: lobo
GPG key ID: A7B62D657EF764F8
2 changed files with 88 additions and 20 deletions

1
.gitignore vendored
View file

@ -2,5 +2,6 @@
.zig-cache/
zig-out/
coverage/
.tmp/
# Zig 0.16 stores fetched packages in-tree rather than the global cache
zig-pkg/

View file

@ -1,13 +1,62 @@
const std = @import("std");
const log = std.log.scoped(.geolite2);
pub fn ensureDatabase(allocator: std.mem.Allocator, io: std.Io, path: []const u8) !void {
std.Io.Dir.cwd().access(io, path, .{}) catch {
log.info("GeoLite2 database not found at {s}, will download", .{path});
try downloadDatabase(allocator, io, path);
log.info("GeoLite2 database downloaded successfully", .{});
/// Ensures a GeoLite2 database exists at `path`, downloading it when missing
/// and refreshing it once it is older than `max_age_seconds`.
///
/// Refresh matters: the upstream release is rebuilt continuously and IP
/// allocations move between cities, so a stale database silently resolves
/// clients to the wrong place. Before this, the database was only fetched when
/// absent, so a deployment could -- and did -- run for months on the copy it
/// happened to download first.
///
/// A missing database is fatal (there is nothing to fall back on). A failed
/// *refresh* is not: an out-of-date database still answers lookups, so a
/// network blip must not stop the service from starting.
pub fn ensureDatabase(
allocator: std.mem.Allocator,
io: std.Io,
path: []const u8,
max_age_seconds: u64,
) !void {
const stat = std.Io.Dir.cwd().statFile(io, path, .{}) catch |err| switch (err) {
error.FileNotFound => {
log.info("GeoLite2 database not found at {s}, will download", .{path});
try downloadDatabase(allocator, io, path);
log.info("GeoLite2 database downloaded successfully", .{});
return;
},
else => return err,
};
const age_seconds = ageInSeconds(io, stat.mtime);
if (age_seconds < max_age_seconds) {
log.info("GeoLite2 database is {d} day(s) old, no refresh needed", .{@divFloor(age_seconds, std.time.s_per_day)});
return;
}
log.info(
"GeoLite2 database is {d} day(s) old (limit {d}), refreshing",
.{ @divFloor(age_seconds, std.time.s_per_day), @divFloor(max_age_seconds, std.time.s_per_day) },
);
downloadDatabase(allocator, io, path) catch |err| {
// Keep serving from the stale database rather than refusing to start.
log.warn(
"GeoLite2 refresh failed ({t}); continuing with the existing database at {s}",
.{ err, path },
);
return;
};
log.info("GeoLite2 database refreshed successfully", .{});
}
/// Age of `mtime` in seconds, clamped at 0 so a clock skew into the future
/// cannot read as an enormous age and trigger a pointless download.
fn ageInSeconds(io: std.Io, mtime: std.Io.Timestamp) u64 {
const now_ns = std.Io.Timestamp.now(io, .real).nanoseconds;
const age_ns = now_ns - mtime.nanoseconds;
if (age_ns <= 0) return 0;
return @intCast(@divFloor(age_ns, std.time.ns_per_s));
}
fn downloadDatabase(allocator: std.mem.Allocator, io: std.Io, path: []const u8) !void {
@ -18,27 +67,45 @@ fn downloadDatabase(allocator: std.mem.Allocator, io: std.Io, path: []const u8)
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;
// Ensure directory exists
if (std.fs.path.dirname(path)) |dir| {
try std.Io.Dir.cwd().createDirPath(io, dir);
}
try std.Io.Dir.cwd().writeFile(io, .{
.sub_path = path,
.data = response_buf[0..writer.end],
});
// Download to a sibling temp file and rename into place. The rename is
// atomic, so a download that dies partway cannot leave a truncated
// database behind, and a database already mmap'd by this process keeps
// pointing at the old inode until it is reopened.
const tmp_path = try std.fmt.allocPrint(allocator, "{s}.download", .{path});
defer allocator.free(tmp_path);
{
const file = try std.Io.Dir.cwd().createFile(io, tmp_path, .{});
// The temp file is only useful if everything below succeeds.
errdefer std.Io.Dir.cwd().deleteFile(io, tmp_path) catch |err|
log.warn("could not remove partial download {s}: {t}", .{ tmp_path, err });
defer file.close(io);
// Stream the response straight to disk. Buffering the whole database in
// memory needed an allocation sized by guesswork, and the guess (64 MiB)
// was already within ~1 MB of the real database size.
var buf: [64 * 1024]u8 = undefined;
var file_writer = file.writer(io, &buf);
const result = try client.fetch(.{
.location = .{ .uri = uri },
.method = .GET,
.response_writer = &file_writer.interface,
});
if (result.status != .ok) {
log.err("GeoLite2 download returned HTTP {d}", .{@intFromEnum(result.status)});
return error.DownloadFailed;
}
try file_writer.interface.flush();
}
try std.Io.Dir.cwd().rename(tmp_path, .cwd(), path, io);
}
fn getLatestReleaseUrl(allocator: std.mem.Allocator, io: std.Io) ![]const u8 {