From ef1d0f4c062dbecebbd2e24eb8f48ed6e9fed160 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Mon, 7 Sep 2026 16:06:54 +0200 Subject: [PATCH] feat: extend main with runRemoteUser and runRemoteTtl --- src/main.zig | 173 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) diff --git a/src/main.zig b/src/main.zig index 4a24cbf..3c36747 100644 --- a/src/main.zig +++ b/src/main.zig @@ -3079,6 +3079,179 @@ fn runRemoteTxn(allocator: std.mem.Allocator, stdout: anytype, stderr: anytype, std.process.exit(1); }; defer client.close(); + + try authenticateIfRequested(&client, stderr, authFlags.auth); + + const applied = client.txnOp(entries.items) catch |err| { + try stderr.print("transaction failed, nothing applied: {s}\n", .{@errorName(err)}); + std.process.exit(1); + }; + try stdout.print("OK: {d} operation(s) committed atomically\n", .{applied}); +} + +/// The network counterpart of `runUser`: dispatches `remote-user ...` +/// onto `client.zig`'s `admin*` methods (which wire-encode the +/// `user_admin` opcode) instead of a local `auth.Store`. Bootstrapping the +/// very first admin account still has to happen locally via `user add` +/// (see the module doc on `runUser`), but every subsequent user management +/// operation can go over the wire once that first admin can authenticate. +fn runRemoteUser(allocator: std.mem.Allocator, stdout: anytype, stderr: anytype, addressSpec: []const u8, rest: []const []const u8) !void { + if (rest.len < 1) return fail(stderr, "remote-user needs a subcommand (add/passwd/role/del/list)"); + const address = try parseHostPort(stdout, addressSpec); + + var client = cobalt.Client.connect(allocator, address) catch |err| { + try stderr.print("error: could not connect to {s}: {s}\n", .{ addressSpec, @errorName(err) }); + std.process.exit(1); + }; + defer client.close(); + + const authFlags = extractAuthFlags(allocator, rest[1..]) catch return fail(stderr, "invalid --user/--pass/--token value"); + defer allocator.free(authFlags.rest); + + try authenticateIfRequested(&client, stderr, authFlags.auth); + + const op = rest[0]; + const args = authFlags.rest; + + if (std.mem.eql(u8, op, "add")) { + if (args.len < 3) return fail(stderr, "remote-user add needs readonly|readwrite|admin"); + const role = cobalt.AuthRole.parse(args[2]) catch return fail(stderr, "role must be readonly, readwrite, or admin"); + client.adminCreateUser(args[0], args[1], role, cobalt.auth.recommendedRounds) catch |err| { + try stderr.print("error: {s}\n", .{@errorName(err)}); + std.process.exit(1); + }; + try stdout.print("user '{s}' created with role {s}\n", .{ args[0], role.name() }); + } else if (std.mem.eql(u8, op, "passwd")) { + if (args.len < 2) return fail(stderr, "remote-user passwd needs "); + client.adminSetPassword(args[0], args[1], cobalt.auth.recommendedRounds) catch |err| { + try stderr.print("error: {s}\n", .{@errorName(err)}); + std.process.exit(1); + }; + try stdout.print("password updated for '{s}'\n", .{args[0]}); + } else if (std.mem.eql(u8, op, "role")) { + if (args.len < 2) return fail(stderr, "remote-user role needs readonly|readwrite|admin"); + const role = cobalt.AuthRole.parse(args[1]) catch return fail(stderr, "role must be readonly, readwrite, or admin"); + client.adminSetRole(args[0], role) catch |err| { + try stderr.print("error: {s}\n", .{@errorName(err)}); + std.process.exit(1); + }; + try stdout.print("role for '{s}' set to {s}\n", .{ args[0], role.name() }); + } else if (std.mem.eql(u8, op, "del")) { + if (args.len < 1) return fail(stderr, "remote-user del needs "); + client.adminDeleteUser(args[0]) catch |err| { + try stderr.print("error: {s}\n", .{@errorName(err)}); + std.process.exit(1); + }; + try stdout.print("user '{s}' deleted\n", .{args[0]}); + } else if (std.mem.eql(u8, op, "list")) { + const users = client.adminListUsers(allocator) catch |err| { + try stderr.print("error: {s}\n", .{@errorName(err)}); + std.process.exit(1); + }; + defer cobalt.auth.freeUserList(allocator, users); + for (users) |u| try stdout.print("{s}\t{s}\n", .{ u.username, u.role.name() }); + } else { + return fail(stderr, "unknown remote-user operation"); + } +} + +/// The network counterpart of `runTtl`-shaped local commands (`expire`, +/// `persist`, `ttl`, `expire-sweep`): dispatches `remote-ttl ...` onto +/// `client.zig`'s `ttl*` methods (which wire-encode the `ttl_op` opcode) +/// instead of calling `ttl.zig` directly against a local Db, the same +/// "reuse client.zig, keep the arg parsing local" shape as `runRemoteUser`. +fn runRemoteTtl(allocator: std.mem.Allocator, stdout: anytype, stderr: anytype, addressSpec: []const u8, rest: []const []const u8) !void { + if (rest.len < 1) return fail(stderr, "remote-ttl needs a subcommand (expire/persist/ttl/sweep)"); + const address = try parseHostPort(stdout, addressSpec); + + var client = cobalt.Client.connect(allocator, address) catch |err| { + try stderr.print("error: could not connect to {s}: {s}\n", .{ addressSpec, @errorName(err) }); + std.process.exit(1); + }; + defer client.close(); + + const authFlags = extractAuthFlags(allocator, rest[1..]) catch return fail(stderr, "invalid --user/--pass/--token value"); + defer allocator.free(authFlags.rest); + + try authenticateIfRequested(&client, stderr, authFlags.auth); + + const op = rest[0]; + const args = authFlags.rest; + + if (std.mem.eql(u8, op, "expire")) { + if (args.len < 2) return fail(stderr, "remote-ttl expire needs "); + const seconds = std.fmt.parseInt(u64, args[1], 10) catch return fail(stderr, "expire needs a whole number of seconds"); + client.ttlExpire(args[0], seconds) catch |err| { + if (err == error.ServerKeyNotFound) return fail(stderr, "no such key"); + try stderr.print("error: {s}\n", .{@errorName(err)}); + std.process.exit(1); + }; + } else if (std.mem.eql(u8, op, "persist")) { + if (args.len < 1) return fail(stderr, "remote-ttl persist needs "); + client.ttlPersist(args[0]) catch |err| { + try stderr.print("error: {s}\n", .{@errorName(err)}); + std.process.exit(1); + }; + } else if (std.mem.eql(u8, op, "ttl")) { + if (args.len < 1) return fail(stderr, "remote-ttl ttl needs "); + const result = client.ttlQuery(args[0]) catch |err| { + try stderr.print("error: {s}\n", .{@errorName(err)}); + std.process.exit(1); + }; + switch (result) { + .noKey => try stdout.writeAll("-2\n"), + .noExpiry => try stdout.writeAll("-1\n"), + .seconds => |s| try stdout.print("{d}\n", .{s}), + } + } else if (std.mem.eql(u8, op, "sweep")) { + const reaped = client.ttlSweep() catch |err| { + try stderr.print("error: {s}\n", .{@errorName(err)}); + std.process.exit(1); + }; + try stdout.print("reaped: {d}\n", .{reaped}); + } else { + return fail(stderr, "unknown remote-ttl operation"); + } +} + +/// The network counterpart of `runCollTtl`-shaped local commands (`coll +/// expire|ttl|persist`, `coll expire-sweep`): dispatches +/// `remote-coll-ttl sweep` or `remote-coll-ttl ...` onto +/// `client.zig`'s `collTtl*` methods (which wire-encode the `coll_ttl_op` +/// opcode), same "reuse client.zig, keep the arg parsing local" shape as +/// `runRemoteTtl`. `sweep` has no `` (it reaps every kind at once, +/// same as local `coll expire-sweep`), so it is checked before trying to +/// parse `rest[0]` as a kind. +fn runRemoteCollTtl(allocator: std.mem.Allocator, stdout: anytype, stderr: anytype, addressSpec: []const u8, rest: []const []const u8) !void { + if (rest.len < 1) return fail(stderr, "remote-coll-ttl needs sweep or expire|ttl|persist "); + const address = try parseHostPort(stdout, addressSpec); + + var client = cobalt.Client.connect(allocator, address) catch |err| { + try stderr.print("error: could not connect to {s}: {s}\n", .{ addressSpec, @errorName(err) }); + std.process.exit(1); + }; + defer client.close(); + + if (std.mem.eql(u8, rest[0], "sweep")) { + const authFlags = extractAuthFlags(allocator, rest[1..]) catch return fail(stderr, "invalid --user/--pass/--token value"); + defer allocator.free(authFlags.rest); + try authenticateIfRequested(&client, stderr, authFlags.auth); + const reaped = client.collTtlSweep() catch |err| { + try stderr.print("error: {s}\n", .{@errorName(err)}); + std.process.exit(1); + }; + try stdout.print("reaped: {d}\n", .{reaped}); + return; + } + + const kind = cobalt.CollectionKind.parse(rest[0]) orelse + return fail(stderr, "unknown collection kind (expected counter/hash/set/list/zset)"); + if (rest.len < 2) return fail(stderr, "remote-coll-ttl needs expire|ttl|persist "); + + const authFlags = extractAuthFlags(allocator, rest[2..]) catch return fail(stderr, "invalid --user/--pass/--token value"); + defer allocator.free(authFlags.rest); + + try authenticateIfRequested(&client, stderr, authFlags.auth); } fn runVerify(allocator: std.mem.Allocator, stdout: anytype, dir: []const u8, count: usize) !void {