4 Commits
risto ... main

Author SHA1 Message Date
Kalzu Rekku
3287c38836 Fix merge left overs. 2025-10-05 22:53:20 +03:00
Kalzu Rekku
f7f956de47 Merge remote-tracking branch 'origin/risto' 2025-10-05 22:45:26 +03:00
Kalzu Rekku
fe45fa5254 Added commands to set meta data, like owner, group and permissions of key. 2025-10-05 22:38:25 +03:00
Kalzu Rekku
ad17dff2fa Fix json quotation bug. 2025-10-05 21:39:09 +03:00

269
main.go
View File

@@ -2,12 +2,14 @@
package main package main
import ( import (
"bufio"
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"os" "os"
"strconv"
"strings" "strings"
"time" "time"
@@ -228,22 +230,38 @@ func (c *KVSClient) handleGet(args []string) {
fmt.Println(cyan("UUID: "), stored.UUID) fmt.Println(cyan("UUID: "), stored.UUID)
fmt.Println(cyan("Timestamp:"), time.UnixMilli(stored.Timestamp).Format(time.RFC3339)) fmt.Println(cyan("Timestamp:"), time.UnixMilli(stored.Timestamp).Format(time.RFC3339))
fmt.Println(cyan("Data:")) fmt.Println(cyan("Data:"))
var pretty bytes.Buffer var pretty bytes.Buffer
json.Indent(&pretty, stored.Data, "", " ") json.Indent(&pretty, stored.Data, "", " ")
fmt.Println(pretty.String()) fmt.Println(pretty.String())
} }
func (c *KVSClient) handlePut(args []string) { func (c *KVSClient) handlePut(args []string) {
if len(args) < 2 { if len(args) < 1 {
fmt.Println(red("Usage: put <key> <json-data>")) fmt.Println(red("Usage: put <key> <json-data>"))
fmt.Println("Or: put <key> (then paste JSON on next line)")
return return
} }
// Parse JSON data var jsonStr string
if len(args) == 1 {
// Multiline mode - read from next input
fmt.Println(yellow("Enter JSON (Ctrl+D when done):"))
scanner := bufio.NewScanner(os.Stdin)
var lines []string
for scanner.Scan() {
lines = append(lines, scanner.Text())
}
jsonStr = strings.Join(lines, "\n")
} else {
// Single line mode
jsonStr = strings.Join(args[1:], " ")
}
// Parse JSON data
var data json.RawMessage var data json.RawMessage
if err := json.Unmarshal([]byte(args[1]), &data); err != nil { if err := json.Unmarshal([]byte(jsonStr), &data); err != nil {
printJSONError(args[1], err) printJSONError(jsonStr, err)
return return
} }
@@ -295,6 +313,134 @@ func (c *KVSClient) handleDelete(args []string) {
} }
} }
// handleMeta is the dispatcher for "meta" sub-commands
func (c *KVSClient) handleMeta(args []string) {
if len(args) == 0 {
fmt.Println(red("Usage: meta <get|set> [options]"))
return
}
subCmd := args[0]
switch subCmd {
case "get":
c.handleMetaGet(args[1:])
case "set":
c.handleMetaSet(args[1:])
default:
fmt.Println(red("Unknown meta command:"), subCmd)
fmt.Println("Available commands: get, set")
}
}
// handleMetaGet fetches and displays metadata for a key
func (c *KVSClient) handleMetaGet(args []string) {
if len(args) < 1 {
fmt.Println(red("Usage: meta get <key>"))
return
}
key := args[0]
respBody, status, err := c.doRequest("GET", "/kv/"+key+"/metadata", nil)
if err != nil {
fmt.Println(red("Error:"), err)
return
}
if status == http.StatusNotFound {
fmt.Println(yellow("No metadata found for key:"), key)
return
}
if status != http.StatusOK {
fmt.Printf(red("Error getting metadata (status %d):\n"), status)
fmt.Println(string(respBody))
return
}
// Struct to parse the metadata response
type MetaResponse struct {
OwnerUUID string `json:"owner_uuid"`
GroupUUID string `json:"group_uuid"`
Permissions int `json:"permissions"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
}
var meta MetaResponse
if err := json.Unmarshal(respBody, &meta); err != nil {
fmt.Println(red("Error parsing metadata response:"), err)
return
}
fmt.Println(cyan("Metadata for key:"), key)
fmt.Printf(" Owner UUID: %s\n", meta.OwnerUUID)
fmt.Printf(" Group UUID: %s\n", meta.GroupUUID)
fmt.Printf(" Permissions: %d\n", meta.Permissions)
fmt.Printf(" Created At: %s\n", time.Unix(meta.CreatedAt, 0).Format(time.RFC3339))
fmt.Printf(" Updated At: %s\n", time.Unix(meta.UpdatedAt, 0).Format(time.RFC3339))
}
// handleMetaSet updates metadata for a key using flags
func (c *KVSClient) handleMetaSet(args []string) {
if len(args) < 3 {
fmt.Println(red("Usage: meta set <key> --owner <uuid> | --group <uuid> | --permissions <number>"))
return
}
key := args[0]
// Use a map to build the JSON payload for partial updates
payload := make(map[string]interface{})
// Parse command-line flags
for i := 1; i < len(args); i++ {
switch args[i] {
case "--owner":
if i+1 < len(args) {
payload["owner_uuid"] = args[i+1]
i++ // Skip the value
}
case "--group":
if i+1 < len(args) {
payload["group_uuid"] = args[i+1]
i++ // Skip the value
}
case "--permissions":
if i+1 < len(args) {
perms, err := strconv.Atoi(args[i+1])
if err != nil {
fmt.Println(red("Invalid permissions value:"), args[i+1])
return
}
payload["permissions"] = perms
i++ // Skip the value
}
}
}
if len(payload) == 0 {
fmt.Println(red("No valid metadata fields provided to set."))
return
}
respBody, status, err := c.doRequest("PUT", "/kv/"+key+"/metadata", payload)
if err != nil {
fmt.Println(red("Error:"), err)
return
}
if status != http.StatusOK {
fmt.Printf(red("Error setting metadata (status %d):\n"), status)
fmt.Println(string(respBody))
return
}
fmt.Println(green("Metadata updated successfully for key:"), key)
// The response body contains the full updated metadata, so we can print it for confirmation
var pretty bytes.Buffer
json.Indent(&pretty, respBody, "", " ")
fmt.Println(pretty.String())
}
func (c *KVSClient) handleMembers(args []string) { func (c *KVSClient) handleMembers(args []string) {
respBody, status, err := c.doRequest("GET", "/members/", nil) respBody, status, err := c.doRequest("GET", "/members/", nil)
if err != nil { if err != nil {
@@ -390,6 +536,43 @@ func (c *KVSClient) handleUserGet(args []string) {
fmt.Printf(" Updated: %s\n", time.Unix(user.UpdatedAt, 0).Format(time.RFC3339)) fmt.Printf(" Updated: %s\n", time.Unix(user.UpdatedAt, 0).Format(time.RFC3339))
} }
func (c *KVSClient) handleUserCreate(args []string) {
if len(args) < 1 {
fmt.Println(red("Usage: user create <nickname>"))
return
}
// The request body requires a JSON object like {"nickname": "..."}
requestPayload := map[string]string{
"nickname": args[0],
}
respBody, status, err := c.doRequest("POST", "/api/users", requestPayload)
if err != nil {
fmt.Println(red("Error:"), err)
return
}
// The backend returns 200 OK on success
if status != http.StatusOK {
fmt.Printf(red("Error creating user (status %d):\n"), status)
fmt.Println(string(respBody))
return
}
// Parse the successful response to get the new user's UUID
var response struct {
UUID string `json:"uuid"`
}
if err := json.Unmarshal(respBody, &response); err != nil {
fmt.Println(red("Error parsing successful response:"), err)
return
}
fmt.Println(green("User created successfully"))
fmt.Println(cyan("UUID:"), response.UUID)
}
// printJSONError displays a user-friendly JSON parsing error with helpful hints // printJSONError displays a user-friendly JSON parsing error with helpful hints
func printJSONError(input string, err error) { func printJSONError(input string, err error) {
fmt.Println(red("❌ Invalid JSON:"), err) fmt.Println(red("❌ Invalid JSON:"), err)
@@ -441,6 +624,11 @@ Key-Value Operations:
put <key> <json> - Store JSON value at key put <key> <json> - Store JSON value at key
delete <key> - Delete key delete <key> - Delete key
Resource Metadata:
meta get <key> - Get metadata (owner, group) for a key
meta set <key> [flags] - Set metadata for a key
Flags: --owner <uuid>, --group <uuid>, --permissions <number>
Cluster Management: Cluster Management:
members - List cluster members members - List cluster members
health - Check service health health - Check service health
@@ -516,14 +704,25 @@ func main() {
client.handlePut(args) client.handlePut(args)
case "delete": case "delete":
client.handleDelete(args) client.handleDelete(args)
case "meta":
client.handleMeta(args)
case "members": case "members":
client.handleMembers(args) client.handleMembers(args)
case "health": case "health":
client.handleHealth(args) client.handleHealth(args)
case "user": case "user":
if len(args) > 0 && args[0] == "get" { if len(args) > 0 {
client.handleUserGet(args[1:]) switch args[0] {
case "get":
client.handleUserGet(args[1:])
case "create":
client.handleUserCreate(args[1:])
default:
fmt.Println(red("Unknown user command:"), args[0])
fmt.Println("Type 'help' for available commands")
}
} else { } else {
// This was the original behavior, which notes that listing users is not implemented.
client.handleUserList(args) client.handleUserList(args)
} }
default: default:
@@ -545,6 +744,14 @@ var completer = readline.NewPrefixCompleter(
readline.PcItem("get"), readline.PcItem("get"),
readline.PcItem("put"), readline.PcItem("put"),
readline.PcItem("delete"), readline.PcItem("delete"),
readline.PcItem("meta",
readline.PcItem("get"),
readline.PcItem("set",
readline.PcItem("--owner"),
readline.PcItem("--group"),
readline.PcItem("--permissions"),
),
),
readline.PcItem("members"), readline.PcItem("members"),
readline.PcItem("health"), readline.PcItem("health"),
readline.PcItem("user", readline.PcItem("user",
@@ -561,41 +768,35 @@ var completer = readline.NewPrefixCompleter(
func parseCommand(line string) []string { func parseCommand(line string) []string {
var parts []string var parts []string
var current strings.Builder var current strings.Builder
var quoteChar byte // Track which quote char started this argument (' or ") var quoteChar rune = 0 // Use a rune to store the active quote character (' or ")
inQuotes := false
for i := 0; i < len(line); i++ { for _, ch := range line {
ch := line[i] switch {
// If we see a quote character
// Check if this is a quote character at the start of a new argument case ch == '\'' || ch == '"':
if (ch == '"' || ch == '\'') && current.Len() == 0 && !inQuotes { if quoteChar == 0 {
// Starting a quoted argument - remember the quote type // Not currently inside a quoted string, so start one
quoteChar = ch quoteChar = ch
inQuotes = true } else if quoteChar == ch {
continue // Don't add the quote itself // Inside a quoted string and found the matching quote, so end it
} quoteChar = 0
} else {
// Check if this is the closing quote // Inside a quoted string but found the *other* type of quote, treat it as a literal character
if inQuotes && ch == quoteChar { current.WriteRune(ch)
// End quoted argument }
inQuotes = false // If we see a space or tab and are NOT inside a quoted string
quoteChar = 0 case (ch == ' ' || ch == '\t') && quoteChar == 0:
continue // Don't add the quote itself
}
// Handle spaces/tabs
if (ch == ' ' || ch == '\t') && !inQuotes {
if current.Len() > 0 { if current.Len() > 0 {
parts = append(parts, current.String()) parts = append(parts, current.String())
current.Reset() current.Reset()
} }
continue // Any other character
default:
current.WriteRune(ch)
} }
// Add all other characters (including quotes inside unquoted args)
current.WriteByte(ch)
} }
// Add the last part if it exists
if current.Len() > 0 { if current.Len() > 0 {
parts = append(parts, current.String()) parts = append(parts, current.String())
} }