authorization, end to end tests, and fix config processing
This commit is contained in:
parent
0b810b0604
commit
02c37f086f
2 changed files with 116 additions and 63 deletions
42
src/main.zig
42
src/main.zig
|
@ -15,9 +15,28 @@ pub fn main() !u8 {
|
|||
|
||||
const args = try parseArgs(allocator);
|
||||
|
||||
var parsed_config = try loadConfig(allocator, args.config_path);
|
||||
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;
|
||||
|
@ -26,12 +45,15 @@ pub fn main() !u8 {
|
|||
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, config);
|
||||
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", .{});
|
||||
|
@ -50,7 +72,7 @@ pub fn main() !u8 {
|
|||
for (events) |event| {
|
||||
for (config.watchers) |watcher| {
|
||||
if (watcher.matches(event.folder, event.path)) {
|
||||
try stdout.print("Match found for {s}/{s}, executing command\n", .{ event.folder, event.path });
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -91,17 +113,6 @@ fn parseArgs(allocator: std.mem.Allocator) !Args {
|
|||
return args;
|
||||
}
|
||||
|
||||
fn loadConfig(allocator: std.mem.Allocator, path: []const u8) !std.json.Parsed(Config) {
|
||||
const file = try std.fs.cwd().openFile(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);
|
||||
|
||||
return try parseConfig(allocator, content, try detectFileType(path));
|
||||
}
|
||||
|
||||
const FileType = enum {
|
||||
json,
|
||||
zon,
|
||||
|
@ -136,6 +147,9 @@ fn printUsage() void {
|
|||
\\ --url <url> Override Syncthing URL from config
|
||||
\\ --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, .{});
|
||||
}
|
||||
|
|
135
src/root.zig
135
src/root.zig
|
@ -5,14 +5,7 @@ pub const Config = struct {
|
|||
syncthing_url: []const u8 = "http://localhost:8384",
|
||||
max_retries: usize = std.math.maxInt(usize),
|
||||
retry_delay_ms: u32 = 1000,
|
||||
watchers: []Watcher,
|
||||
|
||||
pub fn deinit(self: *Config, allocator: std.mem.Allocator) void {
|
||||
for (self.watchers) |*watcher| {
|
||||
watcher.deinit(allocator);
|
||||
}
|
||||
allocator.free(self.watchers);
|
||||
}
|
||||
watchers: []*Watcher,
|
||||
};
|
||||
|
||||
pub const Watcher = struct {
|
||||
|
@ -21,51 +14,45 @@ pub const Watcher = struct {
|
|||
command: []const u8,
|
||||
compiled_pattern: ?mvzr.Regex = null,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, folder: []const u8, path_pattern: []const u8, command: []const u8) !Watcher {
|
||||
var watcher = Watcher{
|
||||
.folder = try allocator.dupe(u8, folder),
|
||||
.path_pattern = try allocator.dupe(u8, path_pattern),
|
||||
.command = try allocator.dupe(u8, command),
|
||||
.compiled_pattern = null,
|
||||
};
|
||||
watcher.compiled_pattern = mvzr.compile(path_pattern);
|
||||
return watcher;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Watcher, allocator: std.mem.Allocator) void {
|
||||
allocator.free(self.folder);
|
||||
allocator.free(self.path_pattern);
|
||||
allocator.free(self.command);
|
||||
}
|
||||
|
||||
pub fn matches(self: *const Watcher, folder: []const u8, path: []const u8) bool {
|
||||
pub fn matches(self: *Watcher, folder: []const u8, path: []const u8) bool {
|
||||
if (!std.mem.eql(u8, folder, self.folder)) {
|
||||
return false;
|
||||
}
|
||||
if (self.compiled_pattern) |pattern| {
|
||||
return pattern.match(path) != null;
|
||||
std.log.debug(
|
||||
"Watcher match on folder {s}. Checking path {s} against pattern {s}",
|
||||
.{ folder, path, self.path_pattern },
|
||||
);
|
||||
self.compiled_pattern = self.compiled_pattern orelse mvzr.compile(self.path_pattern);
|
||||
if (self.compiled_pattern == null) {
|
||||
std.log.err("watcher path_pattern failed to compile and will never match: {s}", .{self.path_pattern});
|
||||
}
|
||||
if (self.compiled_pattern) |pattern|
|
||||
return pattern.isMatch(path);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
pub const SyncthingEvent = struct {
|
||||
id: i64,
|
||||
type: []const u8,
|
||||
data_type: []const u8,
|
||||
folder: []const u8,
|
||||
path: []const u8,
|
||||
time: []const u8,
|
||||
|
||||
pub fn fromJson(allocator: std.mem.Allocator, value: std.json.Value) !SyncthingEvent {
|
||||
const data = value.object.get("data").?.object;
|
||||
return SyncthingEvent{
|
||||
.id = value.object.get("id").?.integer,
|
||||
.type = try allocator.dupe(u8, value.object.get("type").?.string),
|
||||
.folder = try allocator.dupe(u8, value.object.get("folder").?.string),
|
||||
.path = try allocator.dupe(u8, value.object.get("path").?.string),
|
||||
.time = value.object.get("time").?.string,
|
||||
.data_type = try allocator.dupe(u8, data.get("type").?.string),
|
||||
.folder = try allocator.dupe(u8, data.get("folder").?.string),
|
||||
.path = try allocator.dupe(u8, data.get("item").?.string),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *SyncthingEvent, allocator: std.mem.Allocator) void {
|
||||
allocator.free(self.type);
|
||||
allocator.free(self.data_type);
|
||||
allocator.free(self.time);
|
||||
allocator.free(self.folder);
|
||||
allocator.free(self.path);
|
||||
}
|
||||
|
@ -75,12 +62,14 @@ pub const EventPoller = struct {
|
|||
allocator: std.mem.Allocator,
|
||||
config: Config,
|
||||
last_id: ?i64,
|
||||
api_key: []u8,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, config: Config) !EventPoller {
|
||||
pub fn init(allocator: std.mem.Allocator, api_key: []u8, config: Config) !EventPoller {
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.config = config,
|
||||
.last_id = null,
|
||||
.api_key = api_key,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -91,6 +80,9 @@ pub const EventPoller = struct {
|
|||
const aa = arena.allocator();
|
||||
try client.initDefaultProxies(aa);
|
||||
|
||||
var auth_buf: [1024]u8 = undefined;
|
||||
const auth = try std.fmt.bufPrint(&auth_buf, "Bearer {s}", .{self.api_key});
|
||||
|
||||
var retry_count: usize = 0;
|
||||
while (retry_count < self.config.max_retries) : (retry_count += 1) {
|
||||
var url_buf: [1024]u8 = undefined;
|
||||
|
@ -109,6 +101,9 @@ pub const EventPoller = struct {
|
|||
const response = client.fetch(.{
|
||||
.location = .{ .url = url },
|
||||
.response_storage = .{ .dynamic = &al },
|
||||
.headers = .{
|
||||
.authorization = .{ .override = auth },
|
||||
},
|
||||
}) catch |err| {
|
||||
std.log.err("HTTP request failed: {s}", .{@errorName(err)});
|
||||
if (retry_count + 1 < self.config.max_retries) {
|
||||
|
@ -131,6 +126,7 @@ pub const EventPoller = struct {
|
|||
var events = std.ArrayList(SyncthingEvent).init(self.allocator);
|
||||
errdefer events.deinit();
|
||||
|
||||
std.log.debug("Got event response:\n{s}", .{al.items});
|
||||
const parsed = try std.json.parseFromSliceLeaky(std.json.Value, aa, al.items, .{});
|
||||
|
||||
const array = parsed.array;
|
||||
|
@ -173,10 +169,12 @@ fn expandCommandVariables(allocator: std.mem.Allocator, command: []const u8, eve
|
|||
const var_name = command[i + 2 .. j];
|
||||
if (std.mem.eql(u8, var_name, "path")) {
|
||||
try result.appendSlice(event.path);
|
||||
} else if (std.mem.eql(u8, var_name, "id")) {
|
||||
try std.fmt.format(result.writer(), "{d}", .{event.id});
|
||||
} else if (std.mem.eql(u8, var_name, "folder")) {
|
||||
try result.appendSlice(event.folder);
|
||||
} else if (std.mem.eql(u8, var_name, "type")) {
|
||||
try result.appendSlice(event.type);
|
||||
} else if (std.mem.eql(u8, var_name, "data_type")) {
|
||||
try result.appendSlice(event.data_type);
|
||||
}
|
||||
i = j + 1;
|
||||
continue;
|
||||
|
@ -224,8 +222,11 @@ test "event parsing" {
|
|||
\\{
|
||||
\\ "id": 123,
|
||||
\\ "type": "ItemFinished",
|
||||
\\ "data": {
|
||||
\\ "folder": "default",
|
||||
\\ "path": "test.txt"
|
||||
\\ "item": "test.txt",
|
||||
\\ "type": "file"
|
||||
\\ }
|
||||
\\}
|
||||
;
|
||||
|
||||
|
@ -236,7 +237,7 @@ test "event parsing" {
|
|||
defer event.deinit(std.testing.allocator);
|
||||
|
||||
try std.testing.expectEqual(@as(i64, 123), event.id);
|
||||
try std.testing.expectEqualStrings("ItemFinished", event.type);
|
||||
try std.testing.expectEqualStrings("file", event.data_type);
|
||||
try std.testing.expectEqualStrings("default", event.folder);
|
||||
try std.testing.expectEqualStrings("test.txt", event.path);
|
||||
}
|
||||
|
@ -244,32 +245,70 @@ test "event parsing" {
|
|||
test "command variable expansion" {
|
||||
const event = SyncthingEvent{
|
||||
.id = 1,
|
||||
.type = "ItemFinished",
|
||||
.data_type = "file",
|
||||
.folder = "photos",
|
||||
.path = "vacation.jpg",
|
||||
};
|
||||
|
||||
const command = "convert ${path} -resize 800x600 thumb_${folder}_${type}.jpg";
|
||||
const command = "convert ${path} -resize 800x600 thumb_${folder}_${id}.jpg";
|
||||
const expanded = try expandCommandVariables(std.testing.allocator, command, event);
|
||||
defer std.testing.allocator.free(expanded);
|
||||
|
||||
try std.testing.expectEqualStrings(
|
||||
"convert vacation.jpg -resize 800x600 thumb_photos_ItemFinished.jpg",
|
||||
"convert vacation.jpg -resize 800x600 thumb_photos_1.jpg",
|
||||
expanded,
|
||||
);
|
||||
}
|
||||
|
||||
test "watcher pattern matching" {
|
||||
var watcher = try Watcher.init(
|
||||
std.testing.allocator,
|
||||
"photos",
|
||||
".*\\.jpe?g$",
|
||||
"echo ${path}",
|
||||
);
|
||||
defer watcher.deinit(std.testing.allocator);
|
||||
var watcher = Watcher{
|
||||
.folder = "photos",
|
||||
.path_pattern = ".*\\.jpe?g$",
|
||||
.command = "echo ${path}",
|
||||
};
|
||||
|
||||
try std.testing.expect(watcher.matches("photos", "test.jpg"));
|
||||
try std.testing.expect(watcher.matches("photos", "test.jpeg"));
|
||||
try std.testing.expect(!watcher.matches("photos", "test.png"));
|
||||
try std.testing.expect(!watcher.matches("documents", "test.jpg"));
|
||||
}
|
||||
|
||||
test "end to end config / event" {
|
||||
const config_json =
|
||||
\\{
|
||||
\\ "syncthing_url": "http://test:8384",
|
||||
\\ "max_retries": 3,
|
||||
\\ "retry_delay_ms": 2000,
|
||||
\\ "watchers": [
|
||||
\\ {
|
||||
\\ "folder": "default",
|
||||
\\ "path_pattern": ".*\\.txt$",
|
||||
\\ "command": "echo ${path}"
|
||||
\\ }
|
||||
\\ ]
|
||||
\\}
|
||||
;
|
||||
|
||||
const parsed = try std.json.parseFromSlice(Config, std.testing.allocator, config_json, .{});
|
||||
defer parsed.deinit();
|
||||
|
||||
const config = parsed.value;
|
||||
|
||||
const event_json =
|
||||
\\{
|
||||
\\ "id": 123,
|
||||
\\ "type": "ItemFinished",
|
||||
\\ "data": {
|
||||
\\ "folder": "default",
|
||||
\\ "item": "blah/test.txt",
|
||||
\\ "type": "file"
|
||||
\\ }
|
||||
\\}
|
||||
;
|
||||
var parsed_event = try std.json.parseFromSlice(std.json.Value, std.testing.allocator, event_json, .{});
|
||||
defer parsed_event.deinit();
|
||||
var event = try SyncthingEvent.fromJson(std.testing.allocator, parsed_event.value);
|
||||
defer event.deinit(std.testing.allocator);
|
||||
|
||||
try std.testing.expect(config.watchers[0].matches(event.folder, event.path));
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue