Compare commits
No commits in common. "9bdaaa64d7bc4f7d6b8eba062d3c6174bacec5d4" and "ba6db5077e7ab79e39419d250da0919086acd88b" have entirely different histories.
9bdaaa64d7
...
ba6db5077e
10 changed files with 88 additions and 384 deletions
|
|
@ -1,79 +0,0 @@
|
|||
name: Generic zig build
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- '*'
|
||||
env:
|
||||
BUILD_TARGET: x86_64-linux-musl # Needs to be musl since we're using dlopen
|
||||
BUILD_OPTIMIZATION: ReleaseSafe # Safety is usually a good thing
|
||||
BINARY_NAME: wttr
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v6
|
||||
- name: Setup Zig
|
||||
uses: https://codeberg.org/mlugg/setup-zig@v2.1.0
|
||||
- name: Build project
|
||||
run: zig build --summary all
|
||||
- name: Run tests
|
||||
run: zig build test -Ddownload-geoip --summary all
|
||||
- name: Package
|
||||
run: zig build -Dtarget="$BUILD_TARGET" -Doptimize="$BUILD_OPTIMIZATION"
|
||||
- name: Upload
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: $BINARY_NAME
|
||||
path: zig-out/bin/$BINARY_NAME
|
||||
- name: Notify
|
||||
uses: https://git.lerch.org/lobo/action-notify-ntfy@v2
|
||||
if: always() && env.GITEA_ACTIONS == 'true'
|
||||
with:
|
||||
host: ${{ secrets.NTFY_HOST }}
|
||||
topic: ${{ secrets.NTFY_TOPIC }}
|
||||
status: ${{ job.status }}
|
||||
user: ${{ secrets.NTFY_USER }}
|
||||
password: ${{ secrets.NTFY_PASSWORD }}
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ghcr.io/catthehacker/ubuntu:act-22.04
|
||||
needs: build
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v6
|
||||
- name: Download Artifact
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: $BINARY_NAME
|
||||
- name: "Make executable actually executable"
|
||||
run: chmod 755 $BINARY_NAME && mv $BINARY_NAME docker
|
||||
- name: Get short ref
|
||||
id: vars
|
||||
run: echo "shortsha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||
-
|
||||
name: Login to Gitea
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: git.lerch.org
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.PACKAGE_PUSH }}
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: docker
|
||||
push: true
|
||||
tags: |
|
||||
git.lerch.org/${{ github.repository }}:${{ steps.vars.outputs.shortsha }}
|
||||
git.lerch.org/${{ github.repository }}:latest
|
||||
- name: Notify
|
||||
uses: https://git.lerch.org/lobo/action-notify-ntfy@v2
|
||||
if: always()
|
||||
with:
|
||||
host: ${{ secrets.NTFY_HOST }}
|
||||
topic: ${{ secrets.NTFY_TOPIC }}
|
||||
user: ${{ secrets.NTFY_USER }}
|
||||
password: ${{ secrets.NTFY_PASSWORD }}
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,3 +1,4 @@
|
|||
*.swp
|
||||
GeoLite2-City.mmdb
|
||||
.zig-cache/
|
||||
zig-out/
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
const std = @import("std");
|
||||
const GitVersion = @import("build/GitVersion.zig");
|
||||
|
||||
pub fn build(b: *std.Build) void {
|
||||
const target = b.standardTargetOptions(.{});
|
||||
|
|
@ -54,10 +53,6 @@ pub fn build(b: *std.Build) void {
|
|||
|
||||
maxminddb.installHeadersDirectory(maxminddb_upstream.path("include"), "", .{});
|
||||
|
||||
const version = GitVersion.getVersion(b, .{});
|
||||
const build_options = b.addOptions();
|
||||
build_options.addOption([]const u8, "version", version);
|
||||
|
||||
const exe = b.addExecutable(.{
|
||||
.name = "wttr",
|
||||
.root_module = b.createModule(.{
|
||||
|
|
@ -72,7 +67,6 @@ pub fn build(b: *std.Build) void {
|
|||
exe.root_module.addAnonymousImport("airports.dat", .{
|
||||
.root_source_file = openflights.path("data/airports.dat"),
|
||||
});
|
||||
exe.root_module.addOptions("build_options", build_options);
|
||||
exe.root_module.addIncludePath(maxminddb_upstream.path("include"));
|
||||
exe.root_module.addConfigHeader(maxminddb_config);
|
||||
exe.linkLibrary(maxminddb);
|
||||
|
|
@ -93,7 +87,6 @@ pub fn build(b: *std.Build) void {
|
|||
|
||||
const test_options = b.addOptions();
|
||||
test_options.addOption(bool, "download_geoip", download_geoip);
|
||||
test_options.addOption([]const u8, "version", version);
|
||||
|
||||
const tests = b.addTest(.{
|
||||
.root_module = b.createModule(.{
|
||||
|
|
|
|||
|
|
@ -1,161 +0,0 @@
|
|||
const std = @import("std");
|
||||
const Build = std.Build;
|
||||
|
||||
pub const Options = struct {
|
||||
/// Length of the short hash (default: 7, git's default)
|
||||
hash_length: u6 = 7,
|
||||
/// String to append when working tree is dirty
|
||||
dirty_flag: []const u8 = "*",
|
||||
};
|
||||
|
||||
/// Get git version information by reading .git files directly
|
||||
pub fn getVersion(b: *Build, options: Options) []const u8 {
|
||||
const allocator = b.allocator;
|
||||
|
||||
// Find build root by looking for build.zig
|
||||
const build_root = findBuildRoot(allocator) catch return "unknown";
|
||||
defer allocator.free(build_root);
|
||||
|
||||
// Read .git/HEAD relative to build root
|
||||
const head_path = std.fmt.allocPrint(allocator, "{s}/.git/HEAD", .{build_root}) catch return "unknown";
|
||||
defer allocator.free(head_path);
|
||||
|
||||
const head_data = std.fs.cwd().readFileAlloc(allocator, head_path, 1024) catch {
|
||||
return "not under version control";
|
||||
};
|
||||
defer allocator.free(head_data);
|
||||
|
||||
const head_trimmed = std.mem.trim(u8, head_data, &std.ascii.whitespace);
|
||||
|
||||
// Parse HEAD - either "ref: refs/heads/branch" or direct hash
|
||||
const hash_owned = if (std.mem.startsWith(u8, head_trimmed, "ref: ")) blk: {
|
||||
const ref_path_rel = std.mem.trimLeft(u8, head_trimmed[5..], &std.ascii.whitespace);
|
||||
const ref_file = std.fmt.allocPrint(allocator, "{s}/.git/{s}", .{ build_root, ref_path_rel }) catch return "unknown";
|
||||
defer allocator.free(ref_file);
|
||||
|
||||
const ref_fd = std.fs.openFileAbsolute(ref_file, .{}) catch return "unknown";
|
||||
defer ref_fd.close();
|
||||
|
||||
var ref_buf: [1024]u8 = undefined;
|
||||
const bytes_read = ref_fd.readAll(&ref_buf) catch return "unknown";
|
||||
const ref_data = ref_buf[0..bytes_read];
|
||||
|
||||
const ref_trimmed = std.mem.trim(u8, ref_data, &std.ascii.whitespace);
|
||||
break :blk allocator.dupe(u8, ref_trimmed) catch return "unknown";
|
||||
} else allocator.dupe(u8, head_trimmed) catch return "unknown";
|
||||
defer allocator.free(hash_owned);
|
||||
|
||||
// Truncate to short hash
|
||||
const short_hash = if (hash_owned.len > options.hash_length)
|
||||
hash_owned[0..options.hash_length]
|
||||
else
|
||||
hash_owned;
|
||||
|
||||
// Check if dirty using simple heuristic:
|
||||
// If any .zig files are newer than .git/index, mark as dirty
|
||||
const is_dirty = isDirty(allocator, build_root) catch return "unknown";
|
||||
|
||||
if (is_dirty) {
|
||||
return std.fmt.allocPrint(allocator, "{s}{s}", .{ short_hash, options.dirty_flag }) catch return "unknown";
|
||||
}
|
||||
|
||||
return allocator.dupe(u8, short_hash) catch return "unknown";
|
||||
}
|
||||
|
||||
fn findBuildRoot(allocator: std.mem.Allocator) ![]const u8 {
|
||||
var buf: [std.fs.max_path_bytes]u8 = undefined;
|
||||
const start_cwd = try std.fs.cwd().realpath(".", &buf);
|
||||
var cwd: []const u8 = start_cwd;
|
||||
|
||||
while (true) {
|
||||
// Check if build.zig exists in current directory
|
||||
var dir = std.fs.openDirAbsolute(cwd, .{}) catch break;
|
||||
defer dir.close();
|
||||
|
||||
dir.access("build.zig", .{}) catch {
|
||||
// build.zig not found, try parent
|
||||
const parent = std.fs.path.dirname(cwd) orelse break;
|
||||
if (std.mem.eql(u8, parent, cwd)) break; // Reached root
|
||||
cwd = parent;
|
||||
continue;
|
||||
};
|
||||
|
||||
return allocator.dupe(u8, cwd);
|
||||
}
|
||||
|
||||
return error.BuildRootNotFound;
|
||||
}
|
||||
|
||||
fn isDirty(allocator: std.mem.Allocator, build_root: []const u8) !bool {
|
||||
// Get .git/index mtime
|
||||
const index_path = try std.fs.path.join(allocator, &[_][]const u8{ build_root, ".git", "index" });
|
||||
defer allocator.free(index_path);
|
||||
|
||||
const index_stat = std.fs.cwd().statFile(index_path) catch return error.CannotDetermineDirty;
|
||||
const index_mtime = index_stat.mtime;
|
||||
|
||||
// Read .gitignore
|
||||
const ignore_path = try std.fs.path.join(allocator, &[_][]const u8{ build_root, ".gitignore" });
|
||||
defer allocator.free(ignore_path);
|
||||
const ignore_data = std.fs.cwd().readFileAlloc(allocator, ignore_path, 1024 * 1024) catch
|
||||
try allocator.dupe(u8, "");
|
||||
defer allocator.free(ignore_data);
|
||||
|
||||
// Walk source files in build root and check if any are newer
|
||||
var dir = std.fs.openDirAbsolute(build_root, .{ .iterate = true }) catch return error.CannotDetermineDirty;
|
||||
defer dir.close();
|
||||
|
||||
var walker = dir.walk(allocator) catch return error.CannotDetermineDirty;
|
||||
defer walker.deinit();
|
||||
|
||||
while (walker.next() catch return error.CannotDetermineDirty) |entry| {
|
||||
if (entry.kind != .file) continue;
|
||||
|
||||
// Always ignore .git/
|
||||
if (std.mem.startsWith(u8, entry.path, ".git/")) continue;
|
||||
|
||||
// Check if path matches any ignore pattern
|
||||
var lines = std.mem.splitScalar(u8, ignore_data, '\n');
|
||||
var ignored = false;
|
||||
while (lines.next()) |line| {
|
||||
const trimmed = std.mem.trim(u8, line, &std.ascii.whitespace);
|
||||
if (trimmed.len == 0) continue;
|
||||
if (trimmed[0] == '#') continue;
|
||||
|
||||
if (isIgnored(entry.path, trimmed)) {
|
||||
ignored = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (ignored) continue;
|
||||
|
||||
const stat = entry.dir.statFile(entry.basename) catch continue;
|
||||
if (stat.mtime > index_mtime) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Handles checking a gitignore style pattern to determine if a path should
|
||||
/// be ignored. Mostly, globs are not handled, but an extension-like pattern
|
||||
/// is common and recognized by this function, so things like '*.swp' will work
|
||||
/// as expected
|
||||
fn isIgnored(path: []const u8, pattern: []const u8) bool {
|
||||
// TODO: Handle other glob patterns (?, [], *prefix, suffix*)
|
||||
// Handle *.extension pattern
|
||||
if (std.mem.startsWith(u8, pattern, "*."))
|
||||
return std.mem.endsWith(u8, path, pattern[1..]);
|
||||
|
||||
// Pattern ending with / matches directory prefix
|
||||
if (std.mem.endsWith(u8, pattern, "/"))
|
||||
return std.mem.startsWith(u8, path, pattern);
|
||||
|
||||
// Pattern starting with / matches from root
|
||||
if (std.mem.startsWith(u8, pattern, "/"))
|
||||
return std.mem.eql(u8, path, pattern[1..]);
|
||||
|
||||
// Otherwise match as substring (file/dir name anywhere in path)
|
||||
return std.mem.indexOf(u8, path, pattern) != null;
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
FROM scratch
|
||||
COPY ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||
COPY wttr /wttr
|
||||
ENV HOME=/home/wttr
|
||||
USER 1000:1000
|
||||
EXPOSE 8002
|
||||
ENTRYPOINT ["/wttr"]
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
const std = @import("std");
|
||||
|
||||
const Config = @This();
|
||||
|
||||
listen_host: []const u8,
|
||||
listen_port: u16,
|
||||
cache_size: usize,
|
||||
cache_dir: []const u8,
|
||||
|
||||
/// GeoLite2 is used for GeoIP (IP -> geographic location)
|
||||
/// IP2Location is a fallback if IP is not found in this db
|
||||
geolite_path: []const u8,
|
||||
|
||||
/// Geocache file stores location lookups
|
||||
/// (e.g. "Portland -> 45.52345°N, -122.67621° W). When not found in cache,
|
||||
/// a web service from Nominatum (https://nominatim.org/) is used
|
||||
geocache_file: ?[]const u8,
|
||||
|
||||
/// If provided, when GeoLite2 is missing data, https://www.ip2location.com/
|
||||
/// can be used. This will also be cached in the cached file
|
||||
ip2location_api_key: ?[]const u8,
|
||||
ip2location_cache_file: []const u8,
|
||||
|
||||
pub fn load(allocator: std.mem.Allocator) !Config {
|
||||
var env = try std.process.getEnvMap(allocator);
|
||||
defer env.deinit();
|
||||
|
||||
// Get XDG_CACHE_HOME or default to ~/.cache
|
||||
const home = env.get("HOME") orelse "/tmp";
|
||||
const xdg_cache = env.get("XDG_CACHE_HOME") orelse
|
||||
try std.fs.path.join(allocator, &[_][]const u8{ home, ".cache" });
|
||||
defer if (env.get("XDG_CACHE_HOME") == null) allocator.free(xdg_cache);
|
||||
|
||||
const default_cache_dir = try std.fs.path.join(allocator, &[_][]const u8{ xdg_cache, "wttr" });
|
||||
defer allocator.free(default_cache_dir);
|
||||
|
||||
return .{
|
||||
.listen_host = env.get("WTTR_LISTEN_HOST") orelse try allocator.dupe(u8, "0.0.0.0"),
|
||||
.listen_port = if (env.get("WTTR_LISTEN_PORT")) |p|
|
||||
try std.fmt.parseInt(u16, p, 10)
|
||||
else
|
||||
8002,
|
||||
.cache_size = if (env.get("WTTR_CACHE_SIZE")) |s|
|
||||
try std.fmt.parseInt(usize, s, 10)
|
||||
else
|
||||
10_000,
|
||||
.cache_dir = try allocator.dupe(u8, env.get("WTTR_CACHE_DIR") orelse default_cache_dir),
|
||||
.geolite_path = blk: {
|
||||
if (env.get("WTTR_GEOLITE_PATH")) |v| {
|
||||
break :blk try allocator.dupe(u8, v);
|
||||
}
|
||||
break :blk try std.fmt.allocPrint(allocator, "{s}/GeoLite2-City.mmdb", .{
|
||||
env.get("WTTR_CACHE_DIR") orelse default_cache_dir,
|
||||
});
|
||||
},
|
||||
.geocache_file = if (env.get("WTTR_GEOCACHE_FILE")) |v| try allocator.dupe(u8, v) else try std.fs.path.join(allocator, &[_][]const u8{ default_cache_dir, "geocache.json" }),
|
||||
.ip2location_api_key = if (env.get("IP2LOCATION_API_KEY")) |v| try allocator.dupe(u8, v) else null,
|
||||
.ip2location_cache_file = blk: {
|
||||
if (env.get("IP2LOCATION_CACHE_FILE")) |v| {
|
||||
break :blk try allocator.dupe(u8, v);
|
||||
}
|
||||
break :blk try std.fmt.allocPrint(allocator, "{s}/ip2location.cache", .{env.get("WTTR_CACHE_DIR") orelse default_cache_dir});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: Config, allocator: std.mem.Allocator) void {
|
||||
allocator.free(self.listen_host);
|
||||
allocator.free(self.cache_dir);
|
||||
allocator.free(self.geolite_path);
|
||||
if (self.geocache_file) |f| allocator.free(f);
|
||||
if (self.ip2location_api_key) |k| allocator.free(k);
|
||||
allocator.free(self.ip2location_cache_file);
|
||||
}
|
||||
|
||||
test "config loads defaults" {
|
||||
const allocator = std.testing.allocator;
|
||||
const cfg = try Config.load(allocator);
|
||||
defer cfg.deinit(allocator);
|
||||
|
||||
try std.testing.expectEqualStrings("0.0.0.0", cfg.listen_host);
|
||||
try std.testing.expectEqual(@as(u16, 8002), cfg.listen_port);
|
||||
try std.testing.expectEqual(@as(usize, 10_000), cfg.cache_size);
|
||||
}
|
||||
75
src/config.zig
Normal file
75
src/config.zig
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
const std = @import("std");
|
||||
|
||||
pub const Config = struct {
|
||||
listen_host: []const u8,
|
||||
listen_port: u16,
|
||||
cache_size: usize,
|
||||
cache_dir: []const u8,
|
||||
geolite_path: []const u8,
|
||||
geocache_file: ?[]const u8,
|
||||
ip2location_api_key: ?[]const u8,
|
||||
ip2location_cache_file: []const u8,
|
||||
|
||||
pub fn load(allocator: std.mem.Allocator) !Config {
|
||||
var env = try std.process.getEnvMap(allocator);
|
||||
defer env.deinit();
|
||||
|
||||
// Get XDG_CACHE_HOME or default to ~/.cache
|
||||
const home = env.get("HOME") orelse "/tmp";
|
||||
const xdg_cache = env.get("XDG_CACHE_HOME") orelse
|
||||
try std.fmt.allocPrint(allocator, "{s}/.cache", .{home});
|
||||
defer if (env.get("XDG_CACHE_HOME") == null) allocator.free(xdg_cache);
|
||||
|
||||
const default_cache_dir = try std.fmt.allocPrint(allocator, "{s}/wttr", .{xdg_cache});
|
||||
defer allocator.free(default_cache_dir);
|
||||
|
||||
return Config{
|
||||
.listen_host = env.get("WTTR_LISTEN_HOST") orelse try allocator.dupe(u8, "0.0.0.0"),
|
||||
.listen_port = if (env.get("WTTR_LISTEN_PORT")) |p|
|
||||
try std.fmt.parseInt(u16, p, 10)
|
||||
else
|
||||
8002,
|
||||
.cache_size = if (env.get("WTTR_CACHE_SIZE")) |s|
|
||||
try std.fmt.parseInt(usize, s, 10)
|
||||
else
|
||||
10_000,
|
||||
.cache_dir = try allocator.dupe(u8, env.get("WTTR_CACHE_DIR") orelse default_cache_dir),
|
||||
.geolite_path = blk: {
|
||||
if (env.get("WTTR_GEOLITE_PATH")) |v| {
|
||||
break :blk try allocator.dupe(u8, v);
|
||||
}
|
||||
break :blk try std.fmt.allocPrint(allocator, "{s}/GeoLite2-City.mmdb", .{
|
||||
env.get("WTTR_CACHE_DIR") orelse default_cache_dir,
|
||||
});
|
||||
},
|
||||
.geocache_file = if (env.get("WTTR_GEOCACHE_FILE")) |v| try allocator.dupe(u8, v) else null,
|
||||
.ip2location_api_key = if (env.get("IP2LOCATION_API_KEY")) |v| try allocator.dupe(u8, v) else null,
|
||||
.ip2location_cache_file = blk: {
|
||||
if (env.get("IP2LOCATION_CACHE_FILE")) |v| {
|
||||
break :blk try allocator.dupe(u8, v);
|
||||
}
|
||||
break :blk try std.fmt.allocPrint(allocator, "{s}/ip2location.cache", .{env.get("WTTR_CACHE_DIR") orelse default_cache_dir});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: Config, allocator: std.mem.Allocator) void {
|
||||
allocator.free(self.listen_host);
|
||||
allocator.free(self.cache_dir);
|
||||
allocator.free(self.geolite_path);
|
||||
if (self.geocache_file) |f| allocator.free(f);
|
||||
if (self.ip2location_api_key) |k| allocator.free(k);
|
||||
allocator.free(self.ip2location_cache_file);
|
||||
}
|
||||
};
|
||||
|
||||
test "config loads defaults" {
|
||||
const allocator = std.testing.allocator;
|
||||
const cfg = try Config.load(allocator);
|
||||
defer cfg.deinit(allocator);
|
||||
|
||||
try std.testing.expectEqualStrings("0.0.0.0", cfg.listen_host);
|
||||
try std.testing.expectEqual(@as(u16, 8002), cfg.listen_port);
|
||||
try std.testing.expectEqual(@as(usize, 10_000), cfg.cache_size);
|
||||
try std.testing.expect(cfg.geocache_file == null);
|
||||
}
|
||||
|
|
@ -138,12 +138,8 @@ test "GeoIP init with invalid path fails" {
|
|||
}
|
||||
|
||||
test "isUSIP detects US IPs" {
|
||||
const allocator = std.testing.allocator;
|
||||
const Config = @import("../Config.zig");
|
||||
const config = try Config.load(allocator);
|
||||
defer config.deinit(allocator);
|
||||
const build_options = @import("build_options");
|
||||
const db_path = config.geolite_path;
|
||||
const db_path = "./GeoLite2-City.mmdb";
|
||||
|
||||
if (build_options.download_geoip) {
|
||||
const GeoLite2 = @import("GeoLite2.zig");
|
||||
|
|
|
|||
21
src/main.zig
21
src/main.zig
|
|
@ -1,5 +1,5 @@
|
|||
const std = @import("std");
|
||||
const Config = @import("Config.zig");
|
||||
const config = @import("config.zig");
|
||||
const Cache = @import("cache/Cache.zig");
|
||||
const MetNo = @import("weather/MetNo.zig");
|
||||
const Server = @import("http/Server.zig");
|
||||
|
|
@ -9,17 +9,16 @@ const GeoCache = @import("location/GeoCache.zig");
|
|||
const Airports = @import("location/Airports.zig");
|
||||
const Resolver = @import("location/resolver.zig").Resolver;
|
||||
const GeoLite2 = @import("location/GeoLite2.zig");
|
||||
const version = @import("build_options").version;
|
||||
|
||||
pub fn main() !u8 {
|
||||
pub fn main() !void {
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
defer _ = gpa.deinit();
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
const cfg = try Config.load(allocator);
|
||||
const cfg = try config.Config.load(allocator);
|
||||
defer cfg.deinit(allocator);
|
||||
|
||||
std.log.info("wttr version {s} starting on {s}:{d}", .{ version, cfg.listen_host, cfg.listen_port });
|
||||
std.log.info("wttr starting on {s}:{d}", .{ cfg.listen_host, cfg.listen_port });
|
||||
std.log.info("Cache size: {d}", .{cfg.cache_size});
|
||||
std.log.info("Cache dir: {s}", .{cfg.cache_dir});
|
||||
std.log.info("GeoLite2 path: {s}", .{cfg.geolite_path});
|
||||
|
|
@ -29,12 +28,6 @@ pub fn main() !u8 {
|
|||
std.log.info("Geocache: in-memory only", .{});
|
||||
}
|
||||
|
||||
var metno = MetNo.init(allocator, null) catch |err| {
|
||||
if (err == MetNo.MissingIdentificationError) return 1;
|
||||
return err;
|
||||
};
|
||||
defer metno.deinit();
|
||||
|
||||
// Ensure GeoLite2 database exists
|
||||
try GeoLite2.ensureDatabase(allocator, cfg.geolite_path);
|
||||
|
||||
|
|
@ -74,6 +67,9 @@ pub fn main() !u8 {
|
|||
});
|
||||
defer rate_limiter.deinit();
|
||||
|
||||
var metno = try MetNo.init(allocator);
|
||||
defer metno.deinit();
|
||||
|
||||
var server = try Server.init(allocator, cfg.listen_host, cfg.listen_port, .{
|
||||
.provider = metno.provider(cache),
|
||||
.resolver = &resolver,
|
||||
|
|
@ -81,12 +77,11 @@ pub fn main() !u8 {
|
|||
}, &rate_limiter);
|
||||
|
||||
try server.listen();
|
||||
return 0;
|
||||
}
|
||||
|
||||
test {
|
||||
std.testing.refAllDecls(@This());
|
||||
_ = @import("Config.zig");
|
||||
_ = @import("config.zig");
|
||||
_ = @import("cache/Lru.zig");
|
||||
_ = @import("weather/Mock.zig");
|
||||
_ = @import("http/RateLimiter.zig");
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
const std = @import("std");
|
||||
const build_options = @import("build_options");
|
||||
const WeatherProvider = @import("Provider.zig");
|
||||
const Coordinates = @import("../Coordinates.zig");
|
||||
const types = @import("types.zig");
|
||||
|
|
@ -8,8 +7,6 @@ const zeit = @import("zeit");
|
|||
|
||||
const MetNo = @This();
|
||||
|
||||
pub const MissingIdentificationError = error.MetNoIdentificationRequired;
|
||||
|
||||
const MetNoOpenWeatherEntry = struct { []const u8, types.WeatherCode };
|
||||
// symbol codes: https://github.com/metno/weathericons/tree/main/weather
|
||||
// they also have _day, _night and _polartwilight variants
|
||||
|
|
@ -66,24 +63,10 @@ const WeatherCodeMap = std.StaticStringMap(types.WeatherCode);
|
|||
const weather_code_map = WeatherCodeMap.initComptime(weather_code_entries);
|
||||
|
||||
allocator: std.mem.Allocator,
|
||||
identifying_email: []const u8,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, identifying_email: ?[]const u8) !MetNo {
|
||||
const email = identifying_email orelse blk: {
|
||||
const env_email = std.process.getEnvVarOwned(allocator, "METNO_TOS_IDENTIFYING_EMAIL") catch |err| {
|
||||
if (err == error.EnvironmentVariableNotFound) {
|
||||
std.log.err("Met.no Terms of Service require identification. Set METNO_TOS_IDENTIFYING_EMAIL environment variable", .{});
|
||||
std.log.err("See \x1b]8;;https://api.met.no/doc/TermsOfService\x1b\\https://api.met.no/doc/TermsOfService\x1b]8;;\x1b\\ for more information", .{});
|
||||
return MissingIdentificationError;
|
||||
}
|
||||
return err;
|
||||
};
|
||||
break :blk env_email;
|
||||
};
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator) !MetNo {
|
||||
return MetNo{
|
||||
.allocator = allocator,
|
||||
.identifying_email = email,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -117,20 +100,12 @@ fn fetchRaw(ptr: *anyopaque, allocator: std.mem.Allocator, coords: Coordinates)
|
|||
|
||||
var response_buf: [1024 * 1024]u8 = undefined;
|
||||
var writer = std.Io.Writer.fixed(&response_buf);
|
||||
|
||||
const user_agent = try std.fmt.allocPrint(
|
||||
self.allocator,
|
||||
"wttr/{s} git.lerch.org/lobo/wttr {s}",
|
||||
.{ build_options.version, self.identifying_email },
|
||||
);
|
||||
defer self.allocator.free(user_agent);
|
||||
|
||||
const result = try client.fetch(.{
|
||||
.location = .{ .uri = uri },
|
||||
.method = .GET,
|
||||
.response_writer = &writer,
|
||||
.extra_headers = &.{
|
||||
.{ .name = "User-Agent", .value = user_agent },
|
||||
.{ .name = "User-Agent", .value = "wttr.in-zig/1.0 github.com/chubin/wttr.in" },
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -170,7 +145,7 @@ fn deinitProvider(ptr: *anyopaque) void {
|
|||
}
|
||||
|
||||
pub fn deinit(self: *MetNo) void {
|
||||
self.allocator.free(self.identifying_email);
|
||||
_ = self;
|
||||
}
|
||||
|
||||
fn parseMetNoResponse(allocator: std.mem.Allocator, coords: Coordinates, json: std.json.Value) !types.WeatherData {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue