748 lines
14 KiB
Go
748 lines
14 KiB
Go
// Commands from https://redis.io/commands#generic
|
|
|
|
package miniredis
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/alicebob/miniredis/v2/server"
|
|
)
|
|
|
|
// commandsGeneric handles EXPIRE, TTL, PERSIST, &c.
|
|
func commandsGeneric(m *Miniredis) {
|
|
m.srv.Register("COPY", m.cmdCopy)
|
|
m.srv.Register("DEL", m.cmdDel)
|
|
// DUMP
|
|
m.srv.Register("EXISTS", m.cmdExists)
|
|
m.srv.Register("EXPIRE", makeCmdExpire(m, false, time.Second))
|
|
m.srv.Register("EXPIREAT", makeCmdExpire(m, true, time.Second))
|
|
m.srv.Register("KEYS", m.cmdKeys)
|
|
// MIGRATE
|
|
m.srv.Register("MOVE", m.cmdMove)
|
|
// OBJECT
|
|
m.srv.Register("PERSIST", m.cmdPersist)
|
|
m.srv.Register("PEXPIRE", makeCmdExpire(m, false, time.Millisecond))
|
|
m.srv.Register("PEXPIREAT", makeCmdExpire(m, true, time.Millisecond))
|
|
m.srv.Register("PTTL", m.cmdPTTL)
|
|
m.srv.Register("RANDOMKEY", m.cmdRandomkey)
|
|
m.srv.Register("RENAME", m.cmdRename)
|
|
m.srv.Register("RENAMENX", m.cmdRenamenx)
|
|
// RESTORE
|
|
m.srv.Register("TOUCH", m.cmdTouch)
|
|
m.srv.Register("TTL", m.cmdTTL)
|
|
m.srv.Register("TYPE", m.cmdType)
|
|
m.srv.Register("SCAN", m.cmdScan)
|
|
// SORT
|
|
m.srv.Register("UNLINK", m.cmdDel)
|
|
}
|
|
|
|
// generic expire command for EXPIRE, PEXPIRE, EXPIREAT, PEXPIREAT
|
|
// d is the time unit. If unix is set it'll be seen as a unixtimestamp and
|
|
// converted to a duration.
|
|
func makeCmdExpire(m *Miniredis, unix bool, d time.Duration) func(*server.Peer, string, []string) {
|
|
return func(c *server.Peer, cmd string, args []string) {
|
|
if len(args) < 2 {
|
|
setDirty(c)
|
|
c.WriteError(errWrongNumber(cmd))
|
|
return
|
|
}
|
|
if !m.handleAuth(c) {
|
|
return
|
|
}
|
|
if m.checkPubsub(c, cmd) {
|
|
return
|
|
}
|
|
|
|
var opts struct {
|
|
key string
|
|
value int
|
|
nx bool
|
|
xx bool
|
|
gt bool
|
|
lt bool
|
|
}
|
|
opts.key = args[0]
|
|
if ok := optInt(c, args[1], &opts.value); !ok {
|
|
return
|
|
}
|
|
args = args[2:]
|
|
for len(args) > 0 {
|
|
switch strings.ToLower(args[0]) {
|
|
case "nx":
|
|
opts.nx = true
|
|
case "xx":
|
|
opts.xx = true
|
|
case "gt":
|
|
opts.gt = true
|
|
case "lt":
|
|
opts.lt = true
|
|
default:
|
|
setDirty(c)
|
|
c.WriteError(fmt.Sprintf("ERR Unsupported option %s", args[0]))
|
|
return
|
|
}
|
|
args = args[1:]
|
|
}
|
|
if opts.gt && opts.lt {
|
|
setDirty(c)
|
|
c.WriteError("ERR GT and LT options at the same time are not compatible")
|
|
return
|
|
}
|
|
if opts.nx && (opts.xx || opts.gt || opts.lt) {
|
|
setDirty(c)
|
|
c.WriteError("ERR NX and XX, GT or LT options at the same time are not compatible")
|
|
return
|
|
}
|
|
|
|
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
|
|
db := m.db(ctx.selectedDB)
|
|
|
|
// Key must be present.
|
|
if _, ok := db.keys[opts.key]; !ok {
|
|
c.WriteInt(0)
|
|
return
|
|
}
|
|
|
|
oldTTL, ok := db.ttl[opts.key]
|
|
|
|
var newTTL time.Duration
|
|
if unix {
|
|
newTTL = m.at(opts.value, d)
|
|
} else {
|
|
newTTL = time.Duration(opts.value) * d
|
|
}
|
|
|
|
// > NX -- Set expiry only when the key has no expiry
|
|
if opts.nx && ok {
|
|
c.WriteInt(0)
|
|
return
|
|
}
|
|
// > XX -- Set expiry only when the key has an existing expiry
|
|
if opts.xx && !ok {
|
|
c.WriteInt(0)
|
|
return
|
|
}
|
|
// > GT -- Set expiry only when the new expiry is greater than current one
|
|
// (no exp == infinity)
|
|
if opts.gt && (!ok || newTTL <= oldTTL) {
|
|
c.WriteInt(0)
|
|
return
|
|
}
|
|
// > LT -- Set expiry only when the new expiry is less than current one
|
|
if opts.lt && ok && newTTL > oldTTL {
|
|
c.WriteInt(0)
|
|
return
|
|
}
|
|
db.ttl[opts.key] = newTTL
|
|
db.incr(opts.key)
|
|
db.checkTTL(opts.key)
|
|
c.WriteInt(1)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TOUCH
|
|
func (m *Miniredis) cmdTouch(c *server.Peer, cmd string, args []string) {
|
|
if !m.handleAuth(c) {
|
|
return
|
|
}
|
|
if m.checkPubsub(c, cmd) {
|
|
return
|
|
}
|
|
|
|
if len(args) == 0 {
|
|
setDirty(c)
|
|
c.WriteError(errWrongNumber(cmd))
|
|
return
|
|
}
|
|
|
|
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
|
|
db := m.db(ctx.selectedDB)
|
|
|
|
count := 0
|
|
for _, key := range args {
|
|
if db.exists(key) {
|
|
count++
|
|
}
|
|
}
|
|
c.WriteInt(count)
|
|
})
|
|
}
|
|
|
|
// TTL
|
|
func (m *Miniredis) cmdTTL(c *server.Peer, cmd string, args []string) {
|
|
if len(args) != 1 {
|
|
setDirty(c)
|
|
c.WriteError(errWrongNumber(cmd))
|
|
return
|
|
}
|
|
if !m.handleAuth(c) {
|
|
return
|
|
}
|
|
if m.checkPubsub(c, cmd) {
|
|
return
|
|
}
|
|
|
|
key := args[0]
|
|
|
|
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
|
|
db := m.db(ctx.selectedDB)
|
|
|
|
if _, ok := db.keys[key]; !ok {
|
|
// No such key
|
|
c.WriteInt(-2)
|
|
return
|
|
}
|
|
|
|
v, ok := db.ttl[key]
|
|
if !ok {
|
|
// no expire value
|
|
c.WriteInt(-1)
|
|
return
|
|
}
|
|
c.WriteInt(int(v.Seconds()))
|
|
})
|
|
}
|
|
|
|
// PTTL
|
|
func (m *Miniredis) cmdPTTL(c *server.Peer, cmd string, args []string) {
|
|
if len(args) != 1 {
|
|
setDirty(c)
|
|
c.WriteError(errWrongNumber(cmd))
|
|
return
|
|
}
|
|
if !m.handleAuth(c) {
|
|
return
|
|
}
|
|
if m.checkPubsub(c, cmd) {
|
|
return
|
|
}
|
|
|
|
key := args[0]
|
|
|
|
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
|
|
db := m.db(ctx.selectedDB)
|
|
|
|
if _, ok := db.keys[key]; !ok {
|
|
// no such key
|
|
c.WriteInt(-2)
|
|
return
|
|
}
|
|
|
|
v, ok := db.ttl[key]
|
|
if !ok {
|
|
// no expire value
|
|
c.WriteInt(-1)
|
|
return
|
|
}
|
|
c.WriteInt(int(v.Nanoseconds() / 1000000))
|
|
})
|
|
}
|
|
|
|
// PERSIST
|
|
func (m *Miniredis) cmdPersist(c *server.Peer, cmd string, args []string) {
|
|
if len(args) != 1 {
|
|
setDirty(c)
|
|
c.WriteError(errWrongNumber(cmd))
|
|
return
|
|
}
|
|
if !m.handleAuth(c) {
|
|
return
|
|
}
|
|
if m.checkPubsub(c, cmd) {
|
|
return
|
|
}
|
|
|
|
key := args[0]
|
|
|
|
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
|
|
db := m.db(ctx.selectedDB)
|
|
|
|
if _, ok := db.keys[key]; !ok {
|
|
// no such key
|
|
c.WriteInt(0)
|
|
return
|
|
}
|
|
|
|
if _, ok := db.ttl[key]; !ok {
|
|
// no expire value
|
|
c.WriteInt(0)
|
|
return
|
|
}
|
|
delete(db.ttl, key)
|
|
db.incr(key)
|
|
c.WriteInt(1)
|
|
})
|
|
}
|
|
|
|
// DEL and UNLINK
|
|
func (m *Miniredis) cmdDel(c *server.Peer, cmd string, args []string) {
|
|
if !m.handleAuth(c) {
|
|
return
|
|
}
|
|
if m.checkPubsub(c, cmd) {
|
|
return
|
|
}
|
|
|
|
if len(args) == 0 {
|
|
setDirty(c)
|
|
c.WriteError(errWrongNumber(cmd))
|
|
return
|
|
}
|
|
|
|
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
|
|
db := m.db(ctx.selectedDB)
|
|
|
|
count := 0
|
|
for _, key := range args {
|
|
if db.exists(key) {
|
|
count++
|
|
}
|
|
db.del(key, true) // delete expire
|
|
}
|
|
c.WriteInt(count)
|
|
})
|
|
}
|
|
|
|
// TYPE
|
|
func (m *Miniredis) cmdType(c *server.Peer, cmd string, args []string) {
|
|
if len(args) != 1 {
|
|
setDirty(c)
|
|
c.WriteError("usage error")
|
|
return
|
|
}
|
|
if !m.handleAuth(c) {
|
|
return
|
|
}
|
|
if m.checkPubsub(c, cmd) {
|
|
return
|
|
}
|
|
|
|
key := args[0]
|
|
|
|
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
|
|
db := m.db(ctx.selectedDB)
|
|
|
|
t, ok := db.keys[key]
|
|
if !ok {
|
|
c.WriteInline("none")
|
|
return
|
|
}
|
|
|
|
c.WriteInline(t)
|
|
})
|
|
}
|
|
|
|
// EXISTS
|
|
func (m *Miniredis) cmdExists(c *server.Peer, cmd string, args []string) {
|
|
if len(args) < 1 {
|
|
setDirty(c)
|
|
c.WriteError(errWrongNumber(cmd))
|
|
return
|
|
}
|
|
if !m.handleAuth(c) {
|
|
return
|
|
}
|
|
if m.checkPubsub(c, cmd) {
|
|
return
|
|
}
|
|
|
|
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
|
|
db := m.db(ctx.selectedDB)
|
|
|
|
found := 0
|
|
for _, k := range args {
|
|
if db.exists(k) {
|
|
found++
|
|
}
|
|
}
|
|
c.WriteInt(found)
|
|
})
|
|
}
|
|
|
|
// MOVE
|
|
func (m *Miniredis) cmdMove(c *server.Peer, cmd string, args []string) {
|
|
if len(args) != 2 {
|
|
setDirty(c)
|
|
c.WriteError(errWrongNumber(cmd))
|
|
return
|
|
}
|
|
if !m.handleAuth(c) {
|
|
return
|
|
}
|
|
if m.checkPubsub(c, cmd) {
|
|
return
|
|
}
|
|
|
|
var opts struct {
|
|
key string
|
|
targetDB int
|
|
}
|
|
|
|
opts.key = args[0]
|
|
opts.targetDB, _ = strconv.Atoi(args[1])
|
|
|
|
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
|
|
if ctx.selectedDB == opts.targetDB {
|
|
c.WriteError("ERR source and destination objects are the same")
|
|
return
|
|
}
|
|
db := m.db(ctx.selectedDB)
|
|
targetDB := m.db(opts.targetDB)
|
|
|
|
if !db.move(opts.key, targetDB) {
|
|
c.WriteInt(0)
|
|
return
|
|
}
|
|
c.WriteInt(1)
|
|
})
|
|
}
|
|
|
|
// KEYS
|
|
func (m *Miniredis) cmdKeys(c *server.Peer, cmd string, args []string) {
|
|
if len(args) != 1 {
|
|
setDirty(c)
|
|
c.WriteError(errWrongNumber(cmd))
|
|
return
|
|
}
|
|
if !m.handleAuth(c) {
|
|
return
|
|
}
|
|
if m.checkPubsub(c, cmd) {
|
|
return
|
|
}
|
|
|
|
key := args[0]
|
|
|
|
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
|
|
db := m.db(ctx.selectedDB)
|
|
|
|
keys, _ := matchKeys(db.allKeys(), key)
|
|
c.WriteLen(len(keys))
|
|
for _, s := range keys {
|
|
c.WriteBulk(s)
|
|
}
|
|
})
|
|
}
|
|
|
|
// RANDOMKEY
|
|
func (m *Miniredis) cmdRandomkey(c *server.Peer, cmd string, args []string) {
|
|
if len(args) != 0 {
|
|
setDirty(c)
|
|
c.WriteError(errWrongNumber(cmd))
|
|
return
|
|
}
|
|
if !m.handleAuth(c) {
|
|
return
|
|
}
|
|
if m.checkPubsub(c, cmd) {
|
|
return
|
|
}
|
|
|
|
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
|
|
db := m.db(ctx.selectedDB)
|
|
|
|
if len(db.keys) == 0 {
|
|
c.WriteNull()
|
|
return
|
|
}
|
|
nr := m.randIntn(len(db.keys))
|
|
for k := range db.keys {
|
|
if nr == 0 {
|
|
c.WriteBulk(k)
|
|
return
|
|
}
|
|
nr--
|
|
}
|
|
})
|
|
}
|
|
|
|
// RENAME
|
|
func (m *Miniredis) cmdRename(c *server.Peer, cmd string, args []string) {
|
|
if len(args) != 2 {
|
|
setDirty(c)
|
|
c.WriteError(errWrongNumber(cmd))
|
|
return
|
|
}
|
|
if !m.handleAuth(c) {
|
|
return
|
|
}
|
|
if m.checkPubsub(c, cmd) {
|
|
return
|
|
}
|
|
|
|
opts := struct {
|
|
from string
|
|
to string
|
|
}{
|
|
from: args[0],
|
|
to: args[1],
|
|
}
|
|
|
|
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
|
|
db := m.db(ctx.selectedDB)
|
|
|
|
if !db.exists(opts.from) {
|
|
c.WriteError(msgKeyNotFound)
|
|
return
|
|
}
|
|
|
|
db.rename(opts.from, opts.to)
|
|
c.WriteOK()
|
|
})
|
|
}
|
|
|
|
// RENAMENX
|
|
func (m *Miniredis) cmdRenamenx(c *server.Peer, cmd string, args []string) {
|
|
if len(args) != 2 {
|
|
setDirty(c)
|
|
c.WriteError(errWrongNumber(cmd))
|
|
return
|
|
}
|
|
if !m.handleAuth(c) {
|
|
return
|
|
}
|
|
if m.checkPubsub(c, cmd) {
|
|
return
|
|
}
|
|
|
|
opts := struct {
|
|
from string
|
|
to string
|
|
}{
|
|
from: args[0],
|
|
to: args[1],
|
|
}
|
|
|
|
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
|
|
db := m.db(ctx.selectedDB)
|
|
|
|
if !db.exists(opts.from) {
|
|
c.WriteError(msgKeyNotFound)
|
|
return
|
|
}
|
|
|
|
if db.exists(opts.to) {
|
|
c.WriteInt(0)
|
|
return
|
|
}
|
|
|
|
db.rename(opts.from, opts.to)
|
|
c.WriteInt(1)
|
|
})
|
|
}
|
|
|
|
// SCAN
|
|
func (m *Miniredis) cmdScan(c *server.Peer, cmd string, args []string) {
|
|
if len(args) < 1 {
|
|
setDirty(c)
|
|
c.WriteError(errWrongNumber(cmd))
|
|
return
|
|
}
|
|
if !m.handleAuth(c) {
|
|
return
|
|
}
|
|
if m.checkPubsub(c, cmd) {
|
|
return
|
|
}
|
|
|
|
var opts struct {
|
|
cursor int
|
|
count int
|
|
withMatch bool
|
|
match string
|
|
withType bool
|
|
_type string
|
|
}
|
|
|
|
if ok := optIntErr(c, args[0], &opts.cursor, msgInvalidCursor); !ok {
|
|
return
|
|
}
|
|
args = args[1:]
|
|
|
|
// MATCH, COUNT and TYPE options
|
|
for len(args) > 0 {
|
|
if strings.ToLower(args[0]) == "count" {
|
|
if len(args) < 2 {
|
|
setDirty(c)
|
|
c.WriteError(msgSyntaxError)
|
|
return
|
|
}
|
|
count, err := strconv.Atoi(args[1])
|
|
if err != nil || count < 0 {
|
|
setDirty(c)
|
|
c.WriteError(msgInvalidInt)
|
|
return
|
|
}
|
|
if count == 0 {
|
|
setDirty(c)
|
|
c.WriteError(msgSyntaxError)
|
|
return
|
|
}
|
|
opts.count = count
|
|
args = args[2:]
|
|
continue
|
|
}
|
|
if strings.ToLower(args[0]) == "match" {
|
|
if len(args) < 2 {
|
|
setDirty(c)
|
|
c.WriteError(msgSyntaxError)
|
|
return
|
|
}
|
|
opts.withMatch = true
|
|
opts.match, args = args[1], args[2:]
|
|
continue
|
|
}
|
|
if strings.ToLower(args[0]) == "type" {
|
|
if len(args) < 2 {
|
|
setDirty(c)
|
|
c.WriteError(msgSyntaxError)
|
|
return
|
|
}
|
|
opts.withType = true
|
|
opts._type, args = strings.ToLower(args[1]), args[2:]
|
|
continue
|
|
}
|
|
setDirty(c)
|
|
c.WriteError(msgSyntaxError)
|
|
return
|
|
}
|
|
|
|
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
|
|
db := m.db(ctx.selectedDB)
|
|
// We return _all_ (matched) keys every time.
|
|
var keys []string
|
|
|
|
if opts.withType {
|
|
keys = make([]string, 0)
|
|
for k, t := range db.keys {
|
|
// type must be given exactly; no pattern matching is performed
|
|
if t == opts._type {
|
|
keys = append(keys, k)
|
|
}
|
|
}
|
|
} else {
|
|
keys = db.allKeys()
|
|
}
|
|
|
|
sort.Strings(keys) // To make things deterministic.
|
|
|
|
if opts.withMatch {
|
|
keys, _ = matchKeys(keys, opts.match)
|
|
}
|
|
|
|
low := opts.cursor
|
|
high := low + opts.count
|
|
// validate high is correct
|
|
if high > len(keys) || high == 0 {
|
|
high = len(keys)
|
|
}
|
|
if opts.cursor > high {
|
|
// invalid cursor
|
|
c.WriteLen(2)
|
|
c.WriteBulk("0") // no next cursor
|
|
c.WriteLen(0) // no elements
|
|
return
|
|
}
|
|
cursorValue := low + opts.count
|
|
if cursorValue >= len(keys) {
|
|
cursorValue = 0 // no next cursor
|
|
}
|
|
keys = keys[low:high]
|
|
|
|
c.WriteLen(2)
|
|
c.WriteBulk(fmt.Sprintf("%d", cursorValue))
|
|
c.WriteLen(len(keys))
|
|
for _, k := range keys {
|
|
c.WriteBulk(k)
|
|
}
|
|
})
|
|
}
|
|
|
|
// COPY
|
|
func (m *Miniredis) cmdCopy(c *server.Peer, cmd string, args []string) {
|
|
if len(args) < 2 {
|
|
setDirty(c)
|
|
c.WriteError(errWrongNumber(cmd))
|
|
return
|
|
}
|
|
if !m.handleAuth(c) {
|
|
return
|
|
}
|
|
if m.checkPubsub(c, cmd) {
|
|
return
|
|
}
|
|
|
|
var opts = struct {
|
|
from string
|
|
to string
|
|
destinationDB int
|
|
replace bool
|
|
}{
|
|
destinationDB: -1,
|
|
}
|
|
|
|
opts.from, opts.to, args = args[0], args[1], args[2:]
|
|
for len(args) > 0 {
|
|
switch strings.ToLower(args[0]) {
|
|
case "db":
|
|
if len(args) < 2 {
|
|
setDirty(c)
|
|
c.WriteError(msgSyntaxError)
|
|
return
|
|
}
|
|
db, err := strconv.Atoi(args[1])
|
|
if err != nil {
|
|
setDirty(c)
|
|
c.WriteError(msgInvalidInt)
|
|
return
|
|
}
|
|
if db < 0 {
|
|
setDirty(c)
|
|
c.WriteError(msgDBIndexOutOfRange)
|
|
return
|
|
}
|
|
opts.destinationDB = db
|
|
args = args[2:]
|
|
case "replace":
|
|
opts.replace = true
|
|
args = args[1:]
|
|
default:
|
|
setDirty(c)
|
|
c.WriteError(msgSyntaxError)
|
|
return
|
|
}
|
|
}
|
|
|
|
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
|
|
fromDB, toDB := ctx.selectedDB, opts.destinationDB
|
|
if toDB == -1 {
|
|
toDB = fromDB
|
|
}
|
|
|
|
if fromDB == toDB && opts.from == opts.to {
|
|
c.WriteError("ERR source and destination objects are the same")
|
|
return
|
|
}
|
|
|
|
if !m.db(fromDB).exists(opts.from) {
|
|
c.WriteInt(0)
|
|
return
|
|
}
|
|
|
|
if !opts.replace {
|
|
if m.db(toDB).exists(opts.to) {
|
|
c.WriteInt(0)
|
|
return
|
|
}
|
|
}
|
|
|
|
m.copy(m.db(fromDB), opts.from, m.db(toDB), opts.to)
|
|
c.WriteInt(1)
|
|
})
|
|
}
|