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);
|
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();
|
defer parsed_config.deinit();
|
||||||
var config = parsed_config.value;
|
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| {
|
if (args.syncthing_url) |url| {
|
||||||
config.syncthing_url = url;
|
config.syncthing_url = url;
|
||||||
|
@ -26,12 +45,15 @@ pub fn main() !u8 {
|
||||||
const stdout = std.io.getStdOut().writer();
|
const stdout = std.io.getStdOut().writer();
|
||||||
try stdout.print("Monitoring Syncthing events at {s}\n", .{config.syncthing_url});
|
try stdout.print("Monitoring Syncthing events at {s}\n", .{config.syncthing_url});
|
||||||
|
|
||||||
|
var last_id: ?i64 = null;
|
||||||
while (true) {
|
while (true) {
|
||||||
var arena_alloc = std.heap.ArenaAllocator.init(allocator);
|
var arena_alloc = std.heap.ArenaAllocator.init(allocator);
|
||||||
defer arena_alloc.deinit();
|
defer arena_alloc.deinit();
|
||||||
const arena = arena_alloc.allocator();
|
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) {
|
const events = poller.poll() catch |err| switch (err) {
|
||||||
error.Unauthorized => {
|
error.Unauthorized => {
|
||||||
std.log.err("Not authorized to use syncthing. Please set ST_EVENTS_AUTH environment variable and try again", .{});
|
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 (events) |event| {
|
||||||
for (config.watchers) |watcher| {
|
for (config.watchers) |watcher| {
|
||||||
if (watcher.matches(event.folder, event.path)) {
|
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);
|
try lib.executeCommand(allocator, watcher.command, event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -91,17 +113,6 @@ fn parseArgs(allocator: std.mem.Allocator) !Args {
|
||||||
return 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 {
|
const FileType = enum {
|
||||||
json,
|
json,
|
||||||
zon,
|
zon,
|
||||||
|
@ -136,6 +147,9 @@ fn printUsage() void {
|
||||||
\\ --url <url> Override Syncthing URL from config
|
\\ --url <url> Override Syncthing URL from config
|
||||||
\\ --help Show this help message
|
\\ --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, .{});
|
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",
|
syncthing_url: []const u8 = "http://localhost:8384",
|
||||||
max_retries: usize = std.math.maxInt(usize),
|
max_retries: usize = std.math.maxInt(usize),
|
||||||
retry_delay_ms: u32 = 1000,
|
retry_delay_ms: u32 = 1000,
|
||||||
watchers: []Watcher,
|
watchers: []*Watcher,
|
||||||
|
|
||||||
pub fn deinit(self: *Config, allocator: std.mem.Allocator) void {
|
|
||||||
for (self.watchers) |*watcher| {
|
|
||||||
watcher.deinit(allocator);
|
|
||||||
}
|
|
||||||
allocator.free(self.watchers);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Watcher = struct {
|
pub const Watcher = struct {
|
||||||
|
@ -21,51 +14,45 @@ pub const Watcher = struct {
|
||||||
command: []const u8,
|
command: []const u8,
|
||||||
compiled_pattern: ?mvzr.Regex = null,
|
compiled_pattern: ?mvzr.Regex = null,
|
||||||
|
|
||||||
pub fn init(allocator: std.mem.Allocator, folder: []const u8, path_pattern: []const u8, command: []const u8) !Watcher {
|
pub fn matches(self: *Watcher, folder: []const u8, path: []const u8) bool {
|
||||||
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 {
|
|
||||||
if (!std.mem.eql(u8, folder, self.folder)) {
|
if (!std.mem.eql(u8, folder, self.folder)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (self.compiled_pattern) |pattern| {
|
std.log.debug(
|
||||||
return pattern.match(path) != null;
|
"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;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const SyncthingEvent = struct {
|
pub const SyncthingEvent = struct {
|
||||||
id: i64,
|
id: i64,
|
||||||
type: []const u8,
|
data_type: []const u8,
|
||||||
folder: []const u8,
|
folder: []const u8,
|
||||||
path: []const u8,
|
path: []const u8,
|
||||||
|
time: []const u8,
|
||||||
|
|
||||||
pub fn fromJson(allocator: std.mem.Allocator, value: std.json.Value) !SyncthingEvent {
|
pub fn fromJson(allocator: std.mem.Allocator, value: std.json.Value) !SyncthingEvent {
|
||||||
|
const data = value.object.get("data").?.object;
|
||||||
return SyncthingEvent{
|
return SyncthingEvent{
|
||||||
.id = value.object.get("id").?.integer,
|
.id = value.object.get("id").?.integer,
|
||||||
.type = try allocator.dupe(u8, value.object.get("type").?.string),
|
.time = value.object.get("time").?.string,
|
||||||
.folder = try allocator.dupe(u8, value.object.get("folder").?.string),
|
.data_type = try allocator.dupe(u8, data.get("type").?.string),
|
||||||
.path = try allocator.dupe(u8, value.object.get("path").?.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 {
|
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.folder);
|
||||||
allocator.free(self.path);
|
allocator.free(self.path);
|
||||||
}
|
}
|
||||||
|
@ -75,12 +62,14 @@ pub const EventPoller = struct {
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
config: Config,
|
config: Config,
|
||||||
last_id: ?i64,
|
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 .{
|
return .{
|
||||||
.allocator = allocator,
|
.allocator = allocator,
|
||||||
.config = config,
|
.config = config,
|
||||||
.last_id = null,
|
.last_id = null,
|
||||||
|
.api_key = api_key,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,6 +80,9 @@ pub const EventPoller = struct {
|
||||||
const aa = arena.allocator();
|
const aa = arena.allocator();
|
||||||
try client.initDefaultProxies(aa);
|
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;
|
var retry_count: usize = 0;
|
||||||
while (retry_count < self.config.max_retries) : (retry_count += 1) {
|
while (retry_count < self.config.max_retries) : (retry_count += 1) {
|
||||||
var url_buf: [1024]u8 = undefined;
|
var url_buf: [1024]u8 = undefined;
|
||||||
|
@ -109,6 +101,9 @@ pub const EventPoller = struct {
|
||||||
const response = client.fetch(.{
|
const response = client.fetch(.{
|
||||||
.location = .{ .url = url },
|
.location = .{ .url = url },
|
||||||
.response_storage = .{ .dynamic = &al },
|
.response_storage = .{ .dynamic = &al },
|
||||||
|
.headers = .{
|
||||||
|
.authorization = .{ .override = auth },
|
||||||
|
},
|
||||||
}) catch |err| {
|
}) catch |err| {
|
||||||
std.log.err("HTTP request failed: {s}", .{@errorName(err)});
|
std.log.err("HTTP request failed: {s}", .{@errorName(err)});
|
||||||
if (retry_count + 1 < self.config.max_retries) {
|
if (retry_count + 1 < self.config.max_retries) {
|
||||||
|
@ -131,6 +126,7 @@ pub const EventPoller = struct {
|
||||||
var events = std.ArrayList(SyncthingEvent).init(self.allocator);
|
var events = std.ArrayList(SyncthingEvent).init(self.allocator);
|
||||||
errdefer events.deinit();
|
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 parsed = try std.json.parseFromSliceLeaky(std.json.Value, aa, al.items, .{});
|
||||||
|
|
||||||
const array = parsed.array;
|
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];
|
const var_name = command[i + 2 .. j];
|
||||||
if (std.mem.eql(u8, var_name, "path")) {
|
if (std.mem.eql(u8, var_name, "path")) {
|
||||||
try result.appendSlice(event.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")) {
|
} else if (std.mem.eql(u8, var_name, "folder")) {
|
||||||
try result.appendSlice(event.folder);
|
try result.appendSlice(event.folder);
|
||||||
} else if (std.mem.eql(u8, var_name, "type")) {
|
} else if (std.mem.eql(u8, var_name, "data_type")) {
|
||||||
try result.appendSlice(event.type);
|
try result.appendSlice(event.data_type);
|
||||||
}
|
}
|
||||||
i = j + 1;
|
i = j + 1;
|
||||||
continue;
|
continue;
|
||||||
|
@ -224,8 +222,11 @@ test "event parsing" {
|
||||||
\\{
|
\\{
|
||||||
\\ "id": 123,
|
\\ "id": 123,
|
||||||
\\ "type": "ItemFinished",
|
\\ "type": "ItemFinished",
|
||||||
|
\\ "data": {
|
||||||
\\ "folder": "default",
|
\\ "folder": "default",
|
||||||
\\ "path": "test.txt"
|
\\ "item": "test.txt",
|
||||||
|
\\ "type": "file"
|
||||||
|
\\ }
|
||||||
\\}
|
\\}
|
||||||
;
|
;
|
||||||
|
|
||||||
|
@ -236,7 +237,7 @@ test "event parsing" {
|
||||||
defer event.deinit(std.testing.allocator);
|
defer event.deinit(std.testing.allocator);
|
||||||
|
|
||||||
try std.testing.expectEqual(@as(i64, 123), event.id);
|
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("default", event.folder);
|
||||||
try std.testing.expectEqualStrings("test.txt", event.path);
|
try std.testing.expectEqualStrings("test.txt", event.path);
|
||||||
}
|
}
|
||||||
|
@ -244,32 +245,70 @@ test "event parsing" {
|
||||||
test "command variable expansion" {
|
test "command variable expansion" {
|
||||||
const event = SyncthingEvent{
|
const event = SyncthingEvent{
|
||||||
.id = 1,
|
.id = 1,
|
||||||
.type = "ItemFinished",
|
.data_type = "file",
|
||||||
.folder = "photos",
|
.folder = "photos",
|
||||||
.path = "vacation.jpg",
|
.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);
|
const expanded = try expandCommandVariables(std.testing.allocator, command, event);
|
||||||
defer std.testing.allocator.free(expanded);
|
defer std.testing.allocator.free(expanded);
|
||||||
|
|
||||||
try std.testing.expectEqualStrings(
|
try std.testing.expectEqualStrings(
|
||||||
"convert vacation.jpg -resize 800x600 thumb_photos_ItemFinished.jpg",
|
"convert vacation.jpg -resize 800x600 thumb_photos_1.jpg",
|
||||||
expanded,
|
expanded,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
test "watcher pattern matching" {
|
test "watcher pattern matching" {
|
||||||
var watcher = try Watcher.init(
|
var watcher = Watcher{
|
||||||
std.testing.allocator,
|
.folder = "photos",
|
||||||
"photos",
|
.path_pattern = ".*\\.jpe?g$",
|
||||||
".*\\.jpe?g$",
|
.command = "echo ${path}",
|
||||||
"echo ${path}",
|
};
|
||||||
);
|
|
||||||
defer watcher.deinit(std.testing.allocator);
|
|
||||||
|
|
||||||
try std.testing.expect(watcher.matches("photos", "test.jpg"));
|
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.jpeg"));
|
||||||
try std.testing.expect(!watcher.matches("photos", "test.png"));
|
try std.testing.expect(!watcher.matches("photos", "test.png"));
|
||||||
try std.testing.expect(!watcher.matches("documents", "test.jpg"));
|
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