// Copyright 2024 Kelvin Clement Mwinuka // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package admin import ( "errors" "fmt" "github.com/echovault/echovault/pkg/constants" "github.com/echovault/echovault/pkg/types" "github.com/gobwas/glob" "slices" "strings" ) func handleGetAllCommands(params types.HandlerFuncParams) ([]byte, error) { commands := params.GetAllCommands() res := "" commandCount := 0 for _, c := range commands { if c.SubCommands == nil || len(c.SubCommands) <= 0 { res += "*6\r\n" // Command name res += fmt.Sprintf("+command\r\n*1\r\n$%d\r\n%s\r\n", len(c.Command), c.Command) // Command categories res += fmt.Sprintf("+categories\r\n*%d\r\n", len(c.Categories)) for _, category := range c.Categories { res += fmt.Sprintf("$%d\r\n%s\r\n", len(category), category) } // Description res += fmt.Sprintf("+description\r\n*1\r\n$%d\r\n%s\r\n", len(c.Description), c.Description) commandCount += 1 continue } // There are sub-commands for _, sc := range c.SubCommands { res += "*6\r\n" // Command name command := fmt.Sprintf("%s %s", c.Command, sc.Command) res += fmt.Sprintf("+command\r\n*1\r\n$%d\r\n%s\r\n", len(command), command) // Command categories res += fmt.Sprintf("+categories\r\n*%d\r\n", len(sc.Categories)) for _, category := range sc.Categories { res += fmt.Sprintf("$%d\r\n%s\r\n", len(category), category) } // Description res += fmt.Sprintf("+description\r\n*1\r\n$%d\r\n%s\r\n", len(sc.Description), sc.Description) commandCount += 1 } } res = fmt.Sprintf("*%d\r\n%s", commandCount, res) return []byte(res), nil } func handleCommandCount(params types.HandlerFuncParams) ([]byte, error) { var count int commands := params.GetAllCommands() for _, command := range commands { if command.SubCommands != nil && len(command.SubCommands) > 0 { for _, _ = range command.SubCommands { count += 1 } continue } count += 1 } return []byte(fmt.Sprintf(":%d\r\n", count)), nil } func handleCommandList(params types.HandlerFuncParams) ([]byte, error) { switch len(params.Command) { case 2: // Command is COMMAND LIST var count int var res string commands := params.GetAllCommands() for _, command := range commands { if command.SubCommands != nil && len(command.SubCommands) > 0 { for _, subcommand := range command.SubCommands { comm := fmt.Sprintf("%s %s", command.Command, subcommand.Command) res += fmt.Sprintf("$%d\r\n%s\r\n", len(comm), comm) count += 1 } continue } res += fmt.Sprintf("$%d\r\n%s\r\n", len(command.Command), command.Command) count += 1 } res = fmt.Sprintf("*%d\r\n%s", count, res) return []byte(res), nil case 5: var count int var res string // Command has filter if !strings.EqualFold("FILTERBY", params.Command[2]) { return nil, fmt.Errorf("expected FILTERBY, got %s", strings.ToUpper(params.Command[2])) } if strings.EqualFold("ACLCAT", params.Command[3]) { // ACL Category filter commands := params.GetAllCommands() category := strings.ToLower(params.Command[4]) for _, command := range commands { if command.SubCommands != nil && len(command.SubCommands) > 0 { for _, subcommand := range command.SubCommands { if slices.Contains(subcommand.Categories, category) { comm := fmt.Sprintf("%s %s", command.Command, subcommand.Command) res += fmt.Sprintf("$%d\r\n%s\r\n", len(comm), comm) count += 1 } } continue } if slices.Contains(command.Categories, category) { res += fmt.Sprintf("$%d\r\n%s\r\n", len(command.Command), command.Command) count += 1 } } } else if strings.EqualFold("PATTERN", params.Command[3]) { // Pattern filter commands := params.GetAllCommands() g := glob.MustCompile(params.Command[4]) for _, command := range commands { if command.SubCommands != nil && len(command.SubCommands) > 0 { for _, subcommand := range command.SubCommands { comm := fmt.Sprintf("%s %s", command.Command, subcommand.Command) if g.Match(comm) { res += fmt.Sprintf("$%d\r\n%s\r\n", len(comm), comm) count += 1 } } continue } if g.Match(command.Command) { res += fmt.Sprintf("$%d\r\n%s\r\n", len(command.Command), command.Command) count += 1 } } } else if strings.EqualFold("MODULE", params.Command[3]) { // Module filter commands := params.GetAllCommands() module := strings.ToLower(params.Command[4]) for _, command := range commands { if command.SubCommands != nil && len(command.SubCommands) > 0 { for _, subcommand := range command.SubCommands { if strings.EqualFold(subcommand.Module, module) { comm := fmt.Sprintf("%s %s", command.Command, subcommand.Command) res += fmt.Sprintf("$%d\r\n%s\r\n", len(comm), comm) count += 1 } } continue } if strings.EqualFold(command.Module, module) { res += fmt.Sprintf("$%d\r\n%s\r\n", len(command.Command), command.Command) count += 1 } } } else { return nil, fmt.Errorf("expected filter to be ACLCAT or PATTERN, got %s", strings.ToUpper(params.Command[3])) } res = fmt.Sprintf("*%d\r\n%s", count, res) return []byte(res), nil default: return nil, errors.New(constants.WrongArgsResponse) } } func handleCommandDocs(params types.HandlerFuncParams) ([]byte, error) { return []byte("*0\r\n"), nil } func Commands() []types.Command { return []types.Command{ { Command: "commands", Module: constants.AdminModule, Categories: []string{constants.AdminCategory, constants.SlowCategory}, Description: "Get a list of all the commands in available on the echovault with categories and descriptions", Sync: false, KeyExtractionFunc: func(cmd []string) (types.AccessKeys, error) { return types.AccessKeys{ Channels: make([]string, 0), ReadKeys: make([]string, 0), WriteKeys: make([]string, 0), }, nil }, HandlerFunc: handleGetAllCommands, }, { Command: "command", Module: constants.AdminModule, Categories: []string{}, Description: "Commands pertaining to echovault commands", Sync: false, KeyExtractionFunc: func(cmd []string) (types.AccessKeys, error) { return types.AccessKeys{ Channels: make([]string, 0), ReadKeys: make([]string, 0), WriteKeys: make([]string, 0), }, nil }, SubCommands: []types.SubCommand{ { Command: "docs", Module: constants.AdminModule, Categories: []string{constants.SlowCategory, constants.ConnectionCategory}, Description: "Get command documentation", Sync: false, KeyExtractionFunc: func(cmd []string) (types.AccessKeys, error) { return types.AccessKeys{ Channels: make([]string, 0), ReadKeys: make([]string, 0), WriteKeys: make([]string, 0), }, nil }, HandlerFunc: handleCommandDocs, }, { Command: "count", Module: constants.AdminModule, Categories: []string{constants.SlowCategory}, Description: "Get the dumber of commands in the echovault", Sync: false, KeyExtractionFunc: func(cmd []string) (types.AccessKeys, error) { return types.AccessKeys{ Channels: make([]string, 0), ReadKeys: make([]string, 0), WriteKeys: make([]string, 0), }, nil }, HandlerFunc: handleCommandCount, }, { Command: "list", Module: constants.AdminModule, Categories: []string{constants.SlowCategory}, Description: `(COMMAND LIST [FILTERBY ]) Get the list of command names. Allows for filtering by ACL category or glob pattern.`, Sync: false, KeyExtractionFunc: func(cmd []string) (types.AccessKeys, error) { return types.AccessKeys{ Channels: make([]string, 0), ReadKeys: make([]string, 0), WriteKeys: make([]string, 0), }, nil }, HandlerFunc: handleCommandList, }, }, }, { Command: "save", Module: constants.AdminModule, Categories: []string{constants.AdminCategory, constants.SlowCategory, constants.DangerousCategory}, Description: "(SAVE) Trigger a snapshot save", Sync: true, KeyExtractionFunc: func(cmd []string) (types.AccessKeys, error) { return types.AccessKeys{ Channels: make([]string, 0), ReadKeys: make([]string, 0), WriteKeys: make([]string, 0), }, nil }, HandlerFunc: func(params types.HandlerFuncParams) ([]byte, error) { if err := params.TakeSnapshot(); err != nil { return nil, err } return []byte(constants.OkResponse), nil }, }, { Command: "lastsave", Module: constants.AdminModule, Categories: []string{constants.AdminCategory, constants.FastCategory, constants.DangerousCategory}, Description: "(LASTSAVE) Get unix timestamp for the latest snapshot in milliseconds.", Sync: false, KeyExtractionFunc: func(cmd []string) (types.AccessKeys, error) { return types.AccessKeys{ Channels: make([]string, 0), ReadKeys: make([]string, 0), WriteKeys: make([]string, 0), }, nil }, HandlerFunc: func(params types.HandlerFuncParams) ([]byte, error) { msec := params.GetLatestSnapshotTime() if msec == 0 { return nil, errors.New("no snapshot") } return []byte(fmt.Sprintf(":%d\r\n", msec)), nil }, }, { Command: "rewriteaof", Module: constants.AdminModule, Categories: []string{constants.AdminCategory, constants.SlowCategory, constants.DangerousCategory}, Description: "(REWRITEAOF) Trigger re-writing of append process", Sync: false, KeyExtractionFunc: func(cmd []string) (types.AccessKeys, error) { return types.AccessKeys{ Channels: make([]string, 0), ReadKeys: make([]string, 0), WriteKeys: make([]string, 0), }, nil }, HandlerFunc: func(params types.HandlerFuncParams) ([]byte, error) { if err := params.RewriteAOF(); err != nil { return nil, err } return []byte(constants.OkResponse), nil }, }, } }