syncthing-events/src/main.zig

230 lines
7.5 KiB
Zig

const std = @import("std");
const lib = @import("syncthing_events_lib");
const Config = lib.Config;
const EventPoller = lib.EventPoller;
const Args = struct {
config_path: []const u8 = "config.json",
syncthing_url: ?[]const u8 = null,
};
pub const std_options: std.Options = .{
.logFn = logFn,
.log_level = .debug,
};
var log_level = std.log.default_level;
fn logFn(
comptime message_level: std.log.Level,
comptime scope: @TypeOf(.enum_literal),
comptime format: []const u8,
args: anytype,
) void {
if (@intFromEnum(message_level) <= @intFromEnum(log_level)) {
std.log.defaultLog(message_level, scope, format, args);
}
}
pub fn main() !u8 {
var gpa = std.heap.GeneralPurposeAllocator(.{}).init;
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const args = try parseArgs(allocator);
const file = try std.fs.cwd().openFile(args.config_path, .{});
defer file.close();
const max_size = 1024 * 1024; // 1MB max config size
const content = try file.readToEndAlloc(allocator, max_size);
defer allocator.free(content);
const parsed_config = try parseConfig(allocator, content, try detectFileType(args.config_path));
defer parsed_config.deinit();
var config = parsed_config.value;
for (config.watchers) |watcher|
std.log.debug("Watching folder {s} for paths matching pattern '{s}'", .{ watcher.folder, watcher.path_pattern });
const api_key = std.process.getEnvVarOwned(allocator, "ST_EVENTS_AUTH") catch |err|
switch (err) {
error.EnvironmentVariableNotFound => {
std.log.err("ST_EVENTS_AUTH not set. Please set this variable and re-run", .{});
return 2;
},
else => return err,
};
defer allocator.free(api_key);
if (args.syncthing_url) |url| {
config.syncthing_url = url;
}
const stdout = std.io.getStdOut().writer();
try stdout.print("Monitoring Syncthing events at {s}\n", .{config.syncthing_url});
var last_id: ?i64 = null;
while (true) {
var arena_alloc = std.heap.ArenaAllocator.init(allocator);
defer arena_alloc.deinit();
const arena = arena_alloc.allocator();
var poller = try EventPoller.init(arena, api_key, config);
defer last_id = poller.last_id;
poller.last_id = last_id;
const events = poller.poll() catch |err| switch (err) {
error.Unauthorized => {
std.log.err("Not authorized to use syncthing. Please set ST_EVENTS_AUTH environment variable and try again", .{});
return 2;
},
error.MaxRetriesExceeded => {
std.log.err("Maximum retries exceeded - exiting", .{});
return 1;
},
error.MaximumUnexpectedConnectionFailureRetriesExceeded => {
std.log.err("Maximum unexpected connection failure retries exceeded - exiting", .{});
return 100; // This feels like a system issue of some sort
},
else => {
std.log.err("Error polling events: {s}", .{@errorName(err)});
continue;
},
};
for (events) |event| {
for (config.watchers) |watcher| {
if (watcher.matches(event.folder, event.path, event.action)) {
try stdout.print("Match found for folder {s}, path {s}, executing command\n\t{s}\n", .{ event.folder, event.path, watcher.command });
try lib.executeCommand(allocator, watcher.command, event);
}
}
}
}
return 0;
}
fn parseArgs(allocator: std.mem.Allocator) !Args {
var args = Args{};
var arg_it = try std.process.ArgIterator.initWithAllocator(allocator);
defer arg_it.deinit();
// Skip program name
_ = arg_it.skip();
while (arg_it.next()) |arg| {
if (std.mem.eql(u8, arg, "--config")) {
if (arg_it.next()) |config_path| {
args.config_path = try allocator.dupe(u8, config_path);
} else {
std.debug.print("Error: --config requires a path argument\n", .{});
std.process.exit(1);
}
} else if (std.mem.eql(u8, arg, "--url")) {
if (arg_it.next()) |url| {
args.syncthing_url = try allocator.dupe(u8, url);
} else {
std.debug.print("Error: --url requires a URL argument\n", .{});
std.process.exit(1);
}
} else if (std.mem.eql(u8, arg, "-v")) {
moreVerbose();
} else if (std.mem.eql(u8, arg, "--help")) {
printUsage();
std.process.exit(0);
}
}
return args;
}
fn moreVerbose() void {
log_level = switch (log_level) {
.info, .debug => .debug,
.warn => .info,
.err => .warn,
};
}
const FileType = enum {
json,
zon,
yaml,
};
fn detectFileType(path: []const u8) !FileType {
const ext = std.fs.path.extension(path);
if (std.mem.eql(u8, ext, ".json"))
return .json;
if (std.mem.eql(u8, ext, ".zon"))
return .zon;
if (std.mem.eql(u8, ext, ".yaml") or std.mem.eql(u8, ext, ".yml"))
return .yaml;
return error.UnknownFileType;
}
fn parseConfig(allocator: std.mem.Allocator, content: []const u8, file_type: FileType) !std.json.Parsed(Config) {
return switch (file_type) {
.json => try std.json.parseFromSlice(Config, allocator, content, .{}),
.zon => error.UnsupportedConfigFormat, // TODO: Implement ZON parsing
.yaml => error.UnsupportedConfigFormat, // TODO: Implement YAML parsing
};
}
fn printUsage() void {
const usage =
\\Usage: syncthing_events [options]
\\
\\Options:
\\ --config <path> Path to config file (default: config.json)
\\ --url <url> Override Syncthing URL from config
\\ -v Increase logging verbosity (can be used multiple times)
\\ --help Show this help message
\\
\\ST_EVENTS_AUTH environment variable must contain the auth token for
\\syncthing. This can be found in the syncthing UI by clicking Actions,
\\then settings, and copying the API Key variable
;
std.debug.print(usage, .{});
}
test "argument parsing" {
const testing = std.testing;
const allocator = testing.allocator;
// Test default values
const default_args = try parseArgs(allocator);
try testing.expectEqualStrings("config.json", default_args.config_path);
try testing.expectEqual(@as(?[]const u8, null), default_args.syncthing_url);
}
test "config loading" {
const testing = std.testing;
const allocator = testing.allocator;
// Create a temporary config file
const config_json =
\\{
\\ "syncthing_url": "http://test:8384",
\\ "max_retries": 3,
\\ "retry_delay_ms": 2000,
\\ "watchers": [
\\ {
\\ "folder": "test",
\\ "path_pattern": ".*\\.txt$",
\\ "action": "update",
\\ "command": "echo ${path}"
\\ }
\\ ]
\\}
;
// Test code:
const parsed_config = try parseConfig(allocator, config_json, .json);
defer parsed_config.deinit();
const config = parsed_config.value;
try testing.expectEqualStrings("http://test:8384", config.syncthing_url);
try testing.expectEqual(@as(u32, 3), config.max_retries);
try testing.expectEqual(@as(u32, 2000), config.retry_delay_ms);
try testing.expectEqual(@as(usize, 1), config.watchers.len);
}