Compare commits
No commits in common. "2fd37ab0577a36ed582efc9388b1551da0929e60" and "96f5ddfca69353f71ee6a4eac141e5fcec366f49" have entirely different histories.
2fd37ab057
...
96f5ddfca6
3 changed files with 35 additions and 99 deletions
|
@ -47,8 +47,6 @@ Create a configuration file in either JSON, ZON, or YAML format. Example (in JSO
|
||||||
- `watchers`: Array of event watchers with the following properties:
|
- `watchers`: Array of event watchers with the following properties:
|
||||||
- `folder`: Syncthing folder ID to watch
|
- `folder`: Syncthing folder ID to watch
|
||||||
- `path_pattern`: Regular expression to match file paths
|
- `path_pattern`: Regular expression to match file paths
|
||||||
- `action`: Action to match on (deleted, updated, modified, etc). Defaults to '*', which is all actions
|
|
||||||
- `event_type`: [Event type](https://docs.syncthing.net/dev/events.html#event-types) to match on. Defaults to ItemFinished
|
|
||||||
- `command`: Command to execute when a match is found. Supports variables:
|
- `command`: Command to execute when a match is found. Supports variables:
|
||||||
- `${path}`: Full path to the changed file
|
- `${path}`: Full path to the changed file
|
||||||
- `${folder}`: Folder ID where the change occurred
|
- `${folder}`: Folder ID where the change occurred
|
||||||
|
|
31
src/main.zig
31
src/main.zig
|
@ -6,11 +6,6 @@ const EventPoller = lib.EventPoller;
|
||||||
const Args = struct {
|
const Args = struct {
|
||||||
config_path: []const u8 = "config.json",
|
config_path: []const u8 = "config.json",
|
||||||
syncthing_url: ?[]const u8 = null,
|
syncthing_url: ?[]const u8 = null,
|
||||||
|
|
||||||
pub fn deinit(self: Args, allocator: std.mem.Allocator) void {
|
|
||||||
allocator.free(self.config_path);
|
|
||||||
if (self.syncthing_url) |url| allocator.free(url);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const std_options: std.Options = .{
|
pub const std_options: std.Options = .{
|
||||||
|
@ -37,7 +32,6 @@ pub fn main() !u8 {
|
||||||
const allocator = gpa.allocator();
|
const allocator = gpa.allocator();
|
||||||
|
|
||||||
const args = try parseArgs(allocator);
|
const args = try parseArgs(allocator);
|
||||||
defer args.deinit(allocator);
|
|
||||||
|
|
||||||
const file = try std.fs.cwd().openFile(args.config_path, .{});
|
const file = try std.fs.cwd().openFile(args.config_path, .{});
|
||||||
defer file.close();
|
defer file.close();
|
||||||
|
@ -67,24 +61,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});
|
||||||
|
|
||||||
var last_id: ?i64 = null;
|
var last_id: ?i64 = null;
|
||||||
const connection_pool = std.http.Client.ConnectionPool{};
|
|
||||||
while (true) {
|
while (true) {
|
||||||
// Most processing is fairly small
|
var arena_alloc = std.heap.ArenaAllocator.init(allocator);
|
||||||
var stack_fallback_allocator = std.heap.stackFallback(1024 * 2, allocator);
|
|
||||||
var arena_alloc = std.heap.ArenaAllocator.init(stack_fallback_allocator.get());
|
|
||||||
defer arena_alloc.deinit();
|
defer arena_alloc.deinit();
|
||||||
const arena = arena_alloc.allocator();
|
const arena = arena_alloc.allocator();
|
||||||
|
|
||||||
var poller = try EventPoller.init(
|
var poller = try EventPoller.init(arena, api_key, config);
|
||||||
arena,
|
|
||||||
api_key,
|
|
||||||
config,
|
|
||||||
connection_pool,
|
|
||||||
);
|
|
||||||
if (last_id == null) // first run
|
|
||||||
try stdout.print("Monitoring Syncthing events at {s}\n", .{try poller.url()});
|
|
||||||
defer last_id = poller.last_id;
|
defer last_id = poller.last_id;
|
||||||
poller.last_id = last_id;
|
poller.last_id = last_id;
|
||||||
const events = poller.poll() catch |err| switch (err) {
|
const events = poller.poll() catch |err| switch (err) {
|
||||||
|
@ -108,13 +93,9 @@ pub fn main() !u8 {
|
||||||
|
|
||||||
for (events) |event| {
|
for (events) |event| {
|
||||||
for (config.watchers) |watcher| {
|
for (config.watchers) |watcher| {
|
||||||
if (watcher.matches(event)) {
|
if (watcher.matches(event.folder, event.path, event.action)) {
|
||||||
try stdout.print(
|
try stdout.print("Match found for folder {s}, path {s}, executing command\n\t{s}\n", .{ event.folder, event.path, watcher.command });
|
||||||
"Match - Folder: {s} Action: {s} Event type: {s} Path: {s}\n",
|
try lib.executeCommand(allocator, watcher.command, event);
|
||||||
.{ event.folder, event.action, event.event_type, event.path },
|
|
||||||
);
|
|
||||||
std.log.debug("Executing command \n\t{s}", .{watcher.command});
|
|
||||||
try lib.executeCommand(arena, watcher.command, event);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
99
src/root.zig
99
src/root.zig
|
@ -12,29 +12,22 @@ pub const Config = struct {
|
||||||
pub const Watcher = struct {
|
pub const Watcher = struct {
|
||||||
folder: []const u8,
|
folder: []const u8,
|
||||||
path_pattern: []const u8,
|
path_pattern: []const u8,
|
||||||
action: []const u8 = "*",
|
action: []const u8,
|
||||||
event_type: []const u8 = "ItemFinished",
|
|
||||||
command: []const u8,
|
command: []const u8,
|
||||||
compiled_pattern: ?mvzr.Regex = null,
|
compiled_pattern: ?mvzr.Regex = null,
|
||||||
|
|
||||||
pub fn matches(self: *Watcher, event: SyncthingEvent) bool {
|
pub fn matches(self: *Watcher, folder: []const u8, path: []const u8, action: []const u8) bool {
|
||||||
if (!std.mem.eql(u8, event.folder, self.folder)) {
|
if (!std.mem.eql(u8, folder, self.folder)) {
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!std.mem.eql(u8, event.event_type, self.event_type)) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
std.log.debug(
|
std.log.debug(
|
||||||
"Watcher match on folder {s}/event type {s}. Checking path {s} against pattern {s}",
|
"Watcher match on folder {s}. Checking path {s} against pattern {s}",
|
||||||
.{ event.folder, event.event_type, event.path, self.path_pattern },
|
.{ folder, path, self.path_pattern },
|
||||||
);
|
);
|
||||||
const action_match =
|
if (!std.mem.eql(u8, action, self.action)) {
|
||||||
(self.action.len == 1 and self.action[0] == '*') or
|
|
||||||
std.mem.eql(u8, event.action, self.action);
|
|
||||||
if (!action_match) {
|
|
||||||
std.log.debug(
|
std.log.debug(
|
||||||
"Event action {s}, but watching for action {s}. Skipping command",
|
"Event action {s}, but watching for action {s}. Skipping command",
|
||||||
.{ event.action, self.action },
|
.{ action, self.action },
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -43,38 +36,28 @@ pub const Watcher = struct {
|
||||||
std.log.err("watcher path_pattern failed to compile and will never match: {s}", .{self.path_pattern});
|
std.log.err("watcher path_pattern failed to compile and will never match: {s}", .{self.path_pattern});
|
||||||
}
|
}
|
||||||
if (self.compiled_pattern) |pattern|
|
if (self.compiled_pattern) |pattern|
|
||||||
return pattern.isMatch(event.path);
|
return pattern.isMatch(path);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const SyncthingEvent = struct {
|
pub const SyncthingEvent = struct {
|
||||||
id: i64,
|
id: i64,
|
||||||
event_type: []const u8,
|
|
||||||
data_type: []const u8,
|
data_type: []const u8,
|
||||||
folder: []const u8,
|
folder: []const u8,
|
||||||
path: []const u8,
|
path: []const u8,
|
||||||
action: []const u8,
|
action: []const u8,
|
||||||
time: []const u8,
|
time: []const u8,
|
||||||
original_json: []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;
|
const data = value.object.get("data").?.object;
|
||||||
// event values differ on this point...
|
|
||||||
const path = data.get("item") orelse data.get("path");
|
|
||||||
|
|
||||||
var al = std.ArrayList(u8).init(allocator);
|
|
||||||
defer al.deinit();
|
|
||||||
try std.json.stringify(value, .{ .whitespace = .indent_2 }, al.writer());
|
|
||||||
return SyncthingEvent{
|
return SyncthingEvent{
|
||||||
.id = value.object.get("id").?.integer,
|
.id = value.object.get("id").?.integer,
|
||||||
.time = try allocator.dupe(u8, value.object.get("time").?.string),
|
.time = try allocator.dupe(u8, value.object.get("time").?.string),
|
||||||
.event_type = try allocator.dupe(u8, value.object.get("type").?.string),
|
|
||||||
.data_type = try allocator.dupe(u8, data.get("type").?.string),
|
.data_type = try allocator.dupe(u8, data.get("type").?.string),
|
||||||
.folder = try allocator.dupe(u8, data.get("folder").?.string),
|
.folder = try allocator.dupe(u8, data.get("folder").?.string),
|
||||||
.action = try allocator.dupe(u8, data.get("action").?.string),
|
.action = try allocator.dupe(u8, data.get("action").?.string),
|
||||||
.path = try allocator.dupe(u8, path.?.string),
|
.path = try allocator.dupe(u8, data.get("item").?.string),
|
||||||
.original_json = try al.toOwnedSlice(),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,7 +67,6 @@ pub const SyncthingEvent = struct {
|
||||||
allocator.free(self.folder);
|
allocator.free(self.folder);
|
||||||
allocator.free(self.action);
|
allocator.free(self.action);
|
||||||
allocator.free(self.path);
|
allocator.free(self.path);
|
||||||
allocator.free(self.original_json);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -93,46 +75,18 @@ pub const EventPoller = struct {
|
||||||
config: Config,
|
config: Config,
|
||||||
last_id: ?i64,
|
last_id: ?i64,
|
||||||
api_key: []u8,
|
api_key: []u8,
|
||||||
connection_pool: std.http.Client.ConnectionPool,
|
|
||||||
|
|
||||||
pub fn init(allocator: std.mem.Allocator, api_key: []u8, config: Config, connection_pool: ?std.http.Client.ConnectionPool) !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,
|
.api_key = api_key,
|
||||||
.connection_pool = connection_pool orelse .{},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn url(self: EventPoller) ![]const u8 {
|
|
||||||
const watched_events = blk: {
|
|
||||||
var type_set = std.StringArrayHashMap(void).init(self.allocator);
|
|
||||||
try type_set.ensureTotalCapacity(self.config.watchers.len);
|
|
||||||
for (self.config.watchers) |watcher|
|
|
||||||
type_set.putAssumeCapacity(watcher.event_type, {});
|
|
||||||
break :blk try std.mem.join(self.allocator, ",", type_set.keys());
|
|
||||||
};
|
|
||||||
var since_buf: [100]u8 = undefined;
|
|
||||||
const since = if (self.last_id) |id|
|
|
||||||
try std.fmt.bufPrint(&since_buf, "&since={d}", .{id})
|
|
||||||
else
|
|
||||||
"";
|
|
||||||
return try std.fmt.allocPrint(
|
|
||||||
self.allocator,
|
|
||||||
"{s}/rest/events?events={s}{s}",
|
|
||||||
.{
|
|
||||||
self.config.syncthing_url, watched_events, since,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn poll(self: *EventPoller) ![]SyncthingEvent {
|
pub fn poll(self: *EventPoller) ![]SyncthingEvent {
|
||||||
var client = std.http.Client{
|
var client = std.http.Client{ .allocator = self.allocator };
|
||||||
.allocator = self.allocator,
|
|
||||||
.connection_pool = self.connection_pool,
|
|
||||||
};
|
|
||||||
defer client.deinit();
|
|
||||||
var arena = std.heap.ArenaAllocator.init(self.allocator);
|
var arena = std.heap.ArenaAllocator.init(self.allocator);
|
||||||
defer arena.deinit();
|
defer arena.deinit();
|
||||||
const aa = arena.allocator();
|
const aa = arena.allocator();
|
||||||
|
@ -145,13 +99,22 @@ pub const EventPoller = struct {
|
||||||
const MAX_UCF_RETRIES: usize = 20;
|
const MAX_UCF_RETRIES: usize = 20;
|
||||||
var retry_count: usize = 0;
|
var retry_count: usize = 0;
|
||||||
const first_run = self.last_id == null;
|
const first_run = self.last_id == null;
|
||||||
const poll_url = try self.url();
|
|
||||||
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 since_buf: [100]u8 = undefined;
|
||||||
|
const since = if (self.last_id) |id|
|
||||||
|
try std.fmt.bufPrint(&since_buf, "&since={d}", .{id})
|
||||||
|
else
|
||||||
|
"";
|
||||||
|
const url = try std.fmt.bufPrint(&url_buf, "{s}/rest/events?events=ItemFinished{s}", .{
|
||||||
|
self.config.syncthing_url, since,
|
||||||
|
});
|
||||||
|
|
||||||
var al = std.ArrayList(u8).init(self.allocator);
|
var al = std.ArrayList(u8).init(self.allocator);
|
||||||
defer al.deinit();
|
defer al.deinit();
|
||||||
|
|
||||||
const response = client.fetch(.{
|
const response = client.fetch(.{
|
||||||
.location = .{ .url = poll_url },
|
.location = .{ .url = url },
|
||||||
.response_storage = .{ .dynamic = &al },
|
.response_storage = .{ .dynamic = &al },
|
||||||
.headers = .{
|
.headers = .{
|
||||||
.authorization = .{ .override = auth },
|
.authorization = .{ .override = auth },
|
||||||
|
@ -254,13 +217,7 @@ fn expandCommandVariables(allocator: std.mem.Allocator, command: []const u8, eve
|
||||||
} 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, "data_type")) {
|
} else if (std.mem.eql(u8, var_name, "data_type")) {
|
||||||
try result.appendSlice(event.event_type);
|
|
||||||
} else if (std.mem.eql(u8, var_name, "event_type")) {
|
|
||||||
try result.appendSlice(event.data_type);
|
try result.appendSlice(event.data_type);
|
||||||
} else if (std.mem.eql(u8, var_name, "action")) {
|
|
||||||
try result.appendSlice(event.action);
|
|
||||||
} else if (std.mem.eql(u8, var_name, "dump")) {
|
|
||||||
try result.appendSlice(event.original_json);
|
|
||||||
}
|
}
|
||||||
i = j + 1;
|
i = j + 1;
|
||||||
continue;
|
continue;
|
||||||
|
@ -359,11 +316,11 @@ test "watcher pattern matching" {
|
||||||
.action = "update",
|
.action = "update",
|
||||||
};
|
};
|
||||||
|
|
||||||
try std.testing.expect(watcher.matches(.{ .folder = "photos", .path = "test.jpg", .data_type = "update", .event_type = "ItemFinished" }));
|
try std.testing.expect(watcher.matches("photos", "test.jpg", "update"));
|
||||||
try std.testing.expect(watcher.matches(.{ .folder = "photos", .path = "test.jpeg", .data_type = "update", .event_type = "ItemFinished" }));
|
try std.testing.expect(watcher.matches("photos", "test.jpeg", "update"));
|
||||||
try std.testing.expect(watcher.matches(.{ .folder = "photos", .path = "test.png", .data_type = "update", .event_type = "ItemFinished" }));
|
try std.testing.expect(!watcher.matches("photos", "test.png", "update"));
|
||||||
try std.testing.expect(watcher.matches(.{ .folder = "documents", .path = "test.jpg", .data_type = "update", .event_type = "ItemFinished" }));
|
try std.testing.expect(!watcher.matches("documents", "test.jpg", "update"));
|
||||||
try std.testing.expect(watcher.matches(.{ .folder = "photos", .path = "test.jpeg", .data_type = "delete", .event_type = "ItemFinished" }));
|
try std.testing.expect(!watcher.matches("photos", "test.jpeg", "delete"));
|
||||||
}
|
}
|
||||||
|
|
||||||
test "end to end config / event" {
|
test "end to end config / event" {
|
||||||
|
@ -406,5 +363,5 @@ test "end to end config / event" {
|
||||||
var event = try SyncthingEvent.fromJson(std.testing.allocator, parsed_event.value);
|
var event = try SyncthingEvent.fromJson(std.testing.allocator, parsed_event.value);
|
||||||
defer event.deinit(std.testing.allocator);
|
defer event.deinit(std.testing.allocator);
|
||||||
|
|
||||||
try std.testing.expect(config.watchers[0].matches(event));
|
try std.testing.expect(config.watchers[0].matches(event.folder, event.path, event.action));
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue