]> git.saurik.com Git - redis.git/commitdiff
Lua client added thanks to Daniele Alessandri
authorantirez <antirez@gmail.com>
Thu, 26 Mar 2009 16:23:51 +0000 (17:23 +0100)
committerantirez <antirez@gmail.com>
Thu, 26 Mar 2009 16:23:51 +0000 (17:23 +0100)
.gitignore
client-libraries/README
client-libraries/lua/.LICENSE.swp [new file with mode: 0644]
client-libraries/lua/LICENSE [new file with mode: 0644]
client-libraries/lua/README [new file with mode: 0644]
client-libraries/lua/redis.lua [new file with mode: 0644]
redis.conf

index c8a56bc70829e3684685d0013f831629f7bcf6e6..56c27fdd62bc3988241f01005d1030d83c6a271a 100644 (file)
@@ -6,3 +6,4 @@ redis-benchmark
 doc-tools
 mkrelease.sh
 release
+myredis.conf
index 46b1375f9a40335ef97d82c5b3d9eaafd6bb1b21..f35b74a4664a938b4bd3c83c3dee708a667f4809 100644 (file)
@@ -26,4 +26,7 @@ Perl lib source code:
 Redis-php PHP C module:
 http://code.google.com/p/phpredis/
 
+Lua lib source code:
+http://github.com/nrk/redis-lua/tree/master
+
 For all the rest check the Redis tarball or Git repository.
diff --git a/client-libraries/lua/.LICENSE.swp b/client-libraries/lua/.LICENSE.swp
new file mode 100644 (file)
index 0000000..b4773ae
Binary files /dev/null and b/client-libraries/lua/.LICENSE.swp differ
diff --git a/client-libraries/lua/LICENSE b/client-libraries/lua/LICENSE
new file mode 100644 (file)
index 0000000..2da0327
--- /dev/null
@@ -0,0 +1,22 @@
+Copyright (c) 2009\r
+Daniele Alessandri\r
+http://www.clorophilla.net/\r
\r
+Permission is hereby granted, free of charge, to any person obtaining\r
+a copy of this software and associated documentation files (the\r
+"Software"), to deal in the Software without restriction, including\r
+without limitation the rights to use, copy, modify, merge, publish,\r
+distribute, sublicense, and/or sell copies of the Software, and to\r
+permit persons to whom the Software is furnished to do so, subject to\r
+the following conditions:\r
\r
+The above copyright notice and this permission notice shall be\r
+included in all copies or substantial portions of the Software.\r
\r
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,\r
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\r
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\r
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\r
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\r
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\r
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
diff --git a/client-libraries/lua/README b/client-libraries/lua/README
new file mode 100644 (file)
index 0000000..98a5534
--- /dev/null
@@ -0,0 +1,4 @@
+redis-lua\r
+-------------------------------------------------------------------------------\r
+\r
+A Lua client library for the redis key value storage system.
\ No newline at end of file
diff --git a/client-libraries/lua/redis.lua b/client-libraries/lua/redis.lua
new file mode 100644 (file)
index 0000000..757aae1
--- /dev/null
@@ -0,0 +1,322 @@
+module('Redis', package.seeall)
+
+require('socket')       -- requires LuaSocket as a dependency
+
+-- ############################################################################
+
+local protocol = {
+    newline = '\r\n', ok = 'OK', err = 'ERR', null = 'nil', 
+}
+
+-- ############################################################################
+
+local function toboolean(value)
+    return value == 1
+end
+
+local function _write(self, buffer)
+    local _, err = self.socket:send(buffer)
+    if err then error(err) end
+end
+
+local function _read(self, len)
+    if len == nil then len = '*l' end
+    local line, err = self.socket:receive(len)
+    if not err then return line else error('Connection error: ' .. err) end
+end
+
+-- ############################################################################
+
+local function _read_response(self)
+    if options and options.close == true then return end
+
+    local res    = _read(self)
+    local prefix = res:sub(1, -#res)
+    local response_handler = protocol.prefixes[prefix]
+
+    if not response_handler then 
+        error("Unknown response prefix: " .. prefix)
+    else
+        return response_handler(self, res)
+    end
+end
+
+
+local function _send_raw(self, buffer)
+    -- TODO: optimize
+    local bufferType = type(buffer)
+
+    if bufferType == 'string' then
+        _write(self, buffer)
+    elseif bufferType == 'table' then
+        _write(self, table.concat(buffer))
+    else
+        error('Argument error: ' .. bufferType)
+    end
+
+    return _read_response(self)
+end
+
+local function _send_inline(self, command, ...)
+    if arg.n == 0 then
+        _write(self, command .. protocol.newline)
+    else
+        local arguments = arg
+        arguments.n = nil
+
+        if #arguments > 0 then 
+            arguments = table.concat(arguments, ' ')
+        else 
+            arguments = ''
+        end
+
+        _write(self, command .. ' ' .. arguments .. protocol.newline)
+    end
+
+    return _read_response(self)
+end
+
+local function _send_bulk(self, command, ...)
+    local arguments = arg
+    local data      = tostring(table.remove(arguments))
+    arguments.n = nil
+
+    -- TODO: optimize
+    if #arguments > 0 then 
+        arguments = table.concat(arguments, ' ')
+    else 
+        arguments = ''
+    end
+
+    return _send_raw(self, { 
+        command, ' ', arguments, ' ', #data, protocol.newline, data, protocol.newline 
+    })
+end
+
+
+local function _read_line(self, response)
+    return response:sub(2)
+end
+
+local function _read_error(self, response)
+    local err_line = response:sub(2)
+
+    if err_line:sub(1, 3) == protocol.err then
+        error("Redis error: " .. err_line:sub(5))
+    else
+        error("Redis error: " .. err_line)
+    end
+end
+
+local function _read_bulk(self, response)
+    local str = response:sub(2)
+    local len = tonumber(str)
+
+    if not len then 
+        error('Cannot parse ' .. str .. ' as data length.')
+    else
+        if len == -1 then return nil end
+        local data = _read(self, len + 2)
+        return data:sub(1, -3);
+    end
+end
+
+local function _read_multibulk(self, response)
+    local str = response:sub(2)
+
+    -- TODO: add a check if the returned value is indeed a number
+    local list_count = tonumber(str)
+
+    if list_count == -1 then 
+        return nil
+    else
+        local list = {}
+
+        if list_count > 0 then 
+            for i = 1, list_count do
+                table.insert(list, i, _read_bulk(self, _read(self)))
+            end
+        end
+
+        return list
+    end
+end
+
+local function _read_integer(self, response)
+    local res = response:sub(2)
+    local number = tonumber(res)
+
+    if not number then
+        if res == protocol.null then
+            return nil
+        else
+            error('Cannot parse ' .. res .. ' as numeric response.')
+        end
+    end
+
+    return number
+end
+
+-- ############################################################################
+
+protocol.prefixes = {
+    ['+'] = _read_line, 
+    ['-'] = _read_error, 
+    ['$'] = _read_bulk, 
+    ['*'] = _read_multibulk, 
+    [':'] = _read_integer, 
+}
+
+-- ############################################################################
+
+local methods = {
+    -- miscellaneous commands
+    ping    = {
+        'PING', _send_inline, function(response) 
+            if response == 'PONG' then return true else return false end
+        end
+    }, 
+    echo    = { 'ECHO', _send_bulk }, 
+    -- TODO: the server returns an empty -ERR on authentication failure
+    auth    = { 'AUTH' }, 
+
+    -- connection handling
+    quit    = { 'QUIT', function(self, command) 
+            _write(self, command .. protocol.newline)
+        end
+    }, 
+
+    -- commands operating on string values
+    set             = { 'SET', _send_bulk }, 
+    set_preserve    = { 'SETNX', _send_bulk, toboolean }, 
+    get             = { 'GET' }, 
+    get_multiple    = { 'MGET' }, 
+    increment       = { 'INCR' }, 
+    increment_by    = { 'INCRBY' }, 
+    decrement       = { 'DECR' }, 
+    decrement_by    = { 'DECRBY' }, 
+    exists          = { 'EXISTS', _send_inline, toboolean }, 
+    delete          = { 'DEL', _send_inline, toboolean }, 
+    type            = { 'TYPE' }, 
+
+    -- commands operating on the key space
+    keys            = { 
+        'KEYS',  _send_inline, function(response) 
+            local keys = {}
+            response:gsub('%w+', function(key) 
+                table.insert(keys, key)
+            end)
+            return keys
+        end
+    },
+    random_key      = { 'RANDOMKEY' }, 
+    rename          = { 'RENAME' }, 
+    rename_preserve = { 'RENAMENX' }, 
+    database_size   = { 'DBSIZE' }, 
+
+    -- commands operating on lists
+    push_tail   = { 'RPUSH', _send_bulk }, 
+    push_head   = { 'LPUSH', _send_bulk }, 
+    list_length = { 'LLEN', _send_inline, function(response, key)
+            --[[ TODO: redis seems to return a -ERR when the specified key does 
+                       not hold a list value, but this behaviour is not 
+                       consistent with the specs docs. This might be due to the 
+                       -ERR response paradigm being new, which supersedes the 
+                       check for negative numbers to identify errors. ]]
+            if response == -2 then 
+                error('Key ' .. key .. ' does not hold a list value')
+            end
+            return response
+        end
+    }, 
+    list_range  = { 'LRANGE' }, 
+    list_trim   = { 'LTRIM' }, 
+    list_index  = { 'LINDEX' }, 
+    list_set    = { 'LSET', _send_bulk }, 
+    list_remove = { 'LREM', _send_bulk }, 
+    pop_first   = { 'LPOP' }, 
+    pop_last    = { 'RPOP' }, 
+
+    -- commands operating on sets
+    set_add                = { 'SADD' }, 
+    set_remove             = { 'SREM' }, 
+    set_cardinality        = { 'SCARD' }, 
+    set_is_member          = { 'SISMEMBER' }, 
+    set_intersection       = { 'SINTER' }, 
+    set_intersection_store = { 'SINTERSTORE' }, 
+    set_members            = { 'SMEMBERS' }, 
+
+    -- multiple databases handling commands
+    select_database = { 'SELECT' }, 
+    move_key        = { 'MOVE' }, 
+    flush_database  = { 'FLUSHDB' }, 
+    flush_databases = { 'FLUSHALL' }, 
+
+    -- sorting
+    --[[
+        TODO: should we pass sort parameters as a table? e.g: 
+                params = { 
+                    by    = 'weight_*', 
+                    get   = 'object_*', 
+                    limit = { 0, 10 },
+                    sort  = { 'desc', 'alpha' }
+                }
+    --]]
+    sort    = { 'SORT' }, 
+
+    -- persistence control commands
+    save            = { 'SAVE' }, 
+    background_save = { 'BGSAVE' }, 
+    last_save       = { 'LASTSAVE' }, 
+    shutdown        = { 'SHUTDOWN', function(self, command) 
+            _write(self, command .. protocol.newline)
+        end
+    }, 
+
+    -- remote server control commands
+    info    = { 
+        'INFO', _send_inline, function(response) 
+            local info = {}
+            response:gsub('([^\r\n]*)\r\n', function(kv) 
+                local k,v = kv:match(('([^:]*):([^:]*)'):rep(1))
+                info[k] = v
+            end)
+            return info
+        end
+    },
+}
+
+function connect(host, port)
+    local client_socket = socket.connect(host, port)
+
+    if not client_socket then
+        error('Could not connect to ' .. host .. ':' .. port)
+    end
+
+    local redis_client = {
+        socket  = client_socket, 
+        raw_cmd = function(self, buffer)
+            return _send_raw(self, buffer .. protocol.newline)
+        end, 
+    }
+
+    return setmetatable(redis_client, {
+        __index = function(self, method)
+            local redis_meth = methods[method]
+            if redis_meth then
+                return function(self, ...) 
+                    if not redis_meth[2] then 
+                        table.insert(redis_meth, 2, _send_inline)
+                    end
+
+                    local response = redis_meth[2](self, redis_meth[1], ...)
+                    if redis_meth[3] then
+                        return redis_meth[3](response, ...)
+                    else
+                        return response
+                    end
+                end
+            end
+        end
+    })
+end
index c1b4297c92f975b81163c01aeafefe0bfd6d5777..f217352eb3f0b18ac64784e89ec060fce4a4fa73 100644 (file)
@@ -62,6 +62,17 @@ databases 16
 
 # slaveof <masterip> <masterport>
 
+################################## SECURITY ###################################
+
+# Require clients to issue AUTH <PASSWORD> before processing any other
+# commands.  This might be useful in environments in which you do not trust
+# others with access to the host running redis-server.
+#
+# This should stay commented out for backward compatibility and because most
+# people do not need auth (e.g. they run their own servers).
+
+#requirepass foobared
+
 ############################### ADVANCED CONFIG ###############################
 
 # Glue small output buffers together in order to send small replies in a
@@ -74,12 +85,3 @@ glueoutputbuf yes
 # pool so it uses more CPU and can be a bit slower. Usually it's a good
 # idea.
 shareobjects no
-
-# Require clients to issue AUTH <PASSWORD> before processing any other
-# commands.  This might be useful in environments in which you do not trust
-# others with access to the host running redis-server.
-#
-# This should stay commented out for backward compatibility and because most
-# people do not need auth (e.g. they run their own servers).
-
-#requirepass foobared