]> git.za3k.com Git - za3k.git/commitdiff
New kv
authorZachary Vance <za3k@za3k.com>
Sat, 7 Mar 2020 04:33:57 +0000 (20:33 -0800)
committerZachary Vance <za3k@za3k.com>
Sat, 7 Mar 2020 04:33:57 +0000 (20:33 -0800)
computercraft/kv

index 2d856b7c002463ff935e50ee7d9353ab2a68efbe..c534a9e024dd882913cb11ca68f7bb7f38809174 100644 (file)
-local args = { ... }
-local PROTOCOL = "kv"
-local BSPROTOCOL = "kv-bs"
-local NAMESPACE = "" -- global prefix for protocol, ex: "system2."
-local KEYPREFIX = "" -- local prefix for put/get, ex: "za3k."
-if KEYPREFIX == "" and fs.exists(".kv-prefix") then
-  f = fs.open(".kv-prefix", "r")
-  KEYPREFIX = f.readAll()
+-- Note for programmers: you want to make Client(<prefix>).
+-- Then values stored there magically exist
+function fileRead(path, expect)
+  if not fs.exists(path)
+    assert(not expect, "file does not exist: "..path)
+    return
+  end
+  local f = fs.open(path, "r")
+  local content = f.readAll()
+  f.close()
+  return content
+end
+function fileWrite(path, content)
+  local f = fs.open(path, "w")
+  f.write(content)
   f.close()
 end
-local SERVER = NAMESPACE .. "kv-server"
-local KVKEY = NAMESPACE .. "kv"
-local REGFILE = ".register"
+
+local PROTOCOL = "kv"
+local BSPROTOCOL = "kv-bs"
+local KEYPREFIXFILE = ".kv-prefix"
+local KEYPREFIX = fileRead(KEYPREFIXFILE) or "" -- local prefix for put/get, ex: "za3k."
+local SERVER = "kv-server"
+local KVKEY = "kv"
+local KVFILE = "kv"
 local RUNFILE = ".kv-run"
-local shouldRegister = true
-local findModem = function()
+BOOTSTRAP = '-- To bootstrap run\n  rednet.open("back")\n  _,b,_=rednet.receive("'..BSPROTOCOL..'")\n  f=fs.open("'..KVFILE..'","w")\n  f.write(b)\n  f.close()'
+
+-- Magic object that is persistent, backed by one file
+FileDB = function(dbFile, immediateWrite)
+  local content = fileRead(path, expect)
+  if content then
+    db = textutils.unserialise(content)
+    print("loaded "..dbFile)
+  else
+    db = {}
+  end
+  local o = { ["dirty"]=false }
+  function o.save()
+    print("write "..dbFile)
+    fileWrite(dbFile, textutils.serialise(db))
+  end
+  function o.flush()
+    if o.dirty then
+      o.save()
+      o.dirty = false
+    end
+  end
+  setmetatable(o, {
+  __index=db,
+  __newindex=function(d,k,v)
+    db[k]=v
+    if immediateWrite then
+      o.save()
+    else
+      o.dirty = true
+    end
+  end})
+  return o
+end
+
+local findModem = function(expect)
   for _,side in ipairs(rs.getSides()) do
     if peripheral.isPresent(side) and peripheral.getType(side) == "modem" then
       return side
     end
   end
-  print("no modem available")
+  assert(not expect, "no modem available")
 end
 
-local printUsage = function()
-  print("kv update")
-  print("  Update the 'kv' program to the lastest version")
-  print("kv get <keyname> [<filename>]")
-  print("  Get a program over wifi")
-  print("kv put [<filename>] <keyname>")
-  print("  Store a program over wifi")
-  print("kv run <keyname> [...]")
-  print("  Immediately run the named program")
-  print("kv host [FILE]")
-  print("  Host a database server.")
-  print("kv host-dump FILE [PREFIX]")
-  print("  (as host) Prints all kv-pairs")
-  print("Edit .kv-prefix to set a global prefix")
-end
-local host = function(dbFile, quineDisable, noImmediate)
-  db = loadDB(dbFile)
-  if db == nil then
-    db = {}
+local hostHandleGet(host, message)
+  print((message.label or "<old>").." get "..message.key)
+  local value = host.db[message.key]
+  return {
+    ["key"]=message.key,
+    ["value"]=value
+  }
+end
+local hostHandlePut(host, message)
+  print((message.label or "<old>").." put "..message.key)
+  host.db[message.key] = message.value
+  return nil
+end
+local hostHandleList(host, message)
+  print((message.label or "<old>").." list "..(message.filter or "all"))
+  local keys = {}
+  local filter = message.filter
+  for k,v in pairs(host.db):
+    if not message.filter or string.find(k, prefix, true)==1 then
+      key[#keys+1] = k
+    end
+  end
+  return {
+    ["keys"]=keys
+  }
+end
+local hostHandlePeriodic(host)
+  local bs = host.db[KVKEY]
+  if bs then
+    --print("broadcast bs")
+    rednet.broadcast(bs, BSPROTOCOL)
+  end
+  if host.db.flush then
+    host.db.flush()
+  end
+end
+local hostHandlers = {
+  ["get"]=hostHandleGet
+  ["put"]=hostHandlePut
+  ["list"]=hostHandleList
+}
+local Host = function(dbFile, quineDisable, noImmediate)
+  local h = {}
+  if dbFile then
+    h.db = FileDB(dbFile, not noImmediate)
   else
-    print("loaded "..dbFile)
+    h.db = {}
   end
-  if not quineDisable then db[KVKEY] = readFile(KVKEY) end
-  local modemSide = findModem()
-  rednet.open(modemSide)
-  rednet.host(PROTOCOL, SERVER)
-  print("Hosting kv database")
-  print("To bootstrap run:")
-  print("  rednet.open(\"back\")")
-  print("  _,b,_=rednet.receive(\""..BSPROTOCOL.."\")")
-  print("  f=fs.open(\""..KVKEY.."\",\"w\")")
-  print("  f.write(b)")
-  print("  f.close()")
-  local dirty = false
-  while true do
-    clientID, message, protocol = rednet.receive(PROTOCOL, 60)
-    if clientID == nil then
-      -- Broadcast a bootstrap periodically
-      if not quineDisable then
-        --print("broadcast bs")
-        local bs = db[KVKEY]
-        rednet.broadcast(bs, BSPROTOCOL)
-      end
-      if dbFile and dirty then
-        print("write "..dbFile)
-        saveDB(dbFile, db)
-        dirty = false
-      end
-    elseif message.action == "get" then
-      print("get "..message.key)
-      local value = db[message.key]
-      response = {
-        ["action"]="getResponse",
-        ["key"]=message.key,
-        ["value"]=value
-      }
-      rednet.send(clientID, response, PROTOCOL)
-    elseif message.action == "put" then
-      print("put "..message.key)
-      db[message.key] = message.value
-      if noImmediate then
-        dirty = true
+  if not quineDisable then h.db[KVKEY] = fileRead(KVFILE) end
+
+  function h.start(h)
+    rednet.open(findModem(true))
+    rednet.host(PROTOCOL, SERVER)
+    print("Hosting kv database")
+  end
+  function h.loop(h)
+    print(BOOTSTRAP)
+    while true do
+      clientID, message, protocol = rednet.receive(PROTOCOL, 60)
+      if clientID == nil then
+        hostHandlePeriodic(h)
       else
-        print("write "..dbFile)
-        saveDB(dbFile, db)
-        dirty = false
+        for action, handler in pairs(hostHandlers) do
+          if message.action == action then
+            response = handler(message)
+            if response then
+              response.action = action.."Reponse"
+              response.toID = clientID
+              rednet.send(clientID, response, PROTOCOL)
+            end
+          end
+        end
       end
     end
-  end  
-  rednet.unhost(PROTOCOL, SERVER)
-  rednet.close(modemSide)
-end
-local get = function(k, prefix)
-  local modemSide = findModem()
-  prefix = prefix or KEYPREFIX
-  local key = NAMESPACE .. prefix .. k
-  rednet.open(modemSide)
-  local serverID = rednet.lookup(PROTOCOL, SERVER)
-  local message = {
-    ["action"] = "get",
-    ["key"] = key,
-  }
+  end
+  function h.stop(h)
+    rednet.unhost(PROTOCOL, SERVER)
+    rednet.close()
+  end
+
+  h:start()
+  return h
+end
+
+local clientCall = function(action, message, expectResponse)
+  message.action = action
+  message.label = os.getComputerLabel()
   rednet.send(serverID, message, PROTOCOL)
-  local value = nil
-  while true do
+  if not expectResponse then return end
+  local response = nil
+  while not response do
     peerID, message, protocol = rednet.receive(PROTOCOL, 10)
     if peerID == nil then
-      break
-    elseif peerID == serverID and message.action == "getResponse" and message.key == key then
-      value = message.value
-      break
+      error("server never responded")
+    elseif peerID == serverID and message.action == (action.."Reponse") and message.key == key then -- TODO: update to message.toID
+      response = message
     end
   end
-  rednet.close()
-  return value
-end
-local put = function(k, v, prefix)
-  prefix = prefix or KEYPREFIX
-  local key = NAMESPACE .. prefix .. k
-  local modemSide = findModem()
-  rednet.open(modemSide)
-  local serverID = rednet.lookup(PROTOCOL, SERVER)
-  message = {
-    ["action"] = "put",
-    ["key"] = key,
-    ["value"] = v
-  }
-  rednet.send(serverID, message, PROTOCOL)
-  rednet.close()
+  return response
 end
-writeFile = function(path, content)
-  local f = fs.open(path, "w")
-  f.write(content)
-  f.close()
+local clientGet = function(key, expect)
+  local response = clientCall("get", {
+    ["key"] = key,
+  }, true)
+  assert(reponse.value or not expect, ("key '"..key.."' does not exist"))
+  return reponse.value
 end
-readFile = function(path)
-  local f = fs.open(path, "r")
-  content = f.readAll()
-  f.close()
-  return content
+local clientPut = function(k, v)
+  local response = clientCall("put", {
+    ["key"] = k,
+    ["value"] = v,
+  }, false)
 end
-loadDB = function(path)
-  if path == nil or not fs.exists(path) then
-    return nil
-  else
-    return textutils.unserialise(readFile(path))
-  end
+local clientList = function(filter)
+  local reponse = clientCall("list", {
+    ["filter"] = filter
+  }, true)
+  return reponse.keys
 end
-saveDB = function(path, db)
-  writeFile(path, textutils.serialise(db))
+
+-- Make this to use the client in your own code
+Client = function(prefix)
+  local c = {}
+  c.prefix = prefix or KEYPREFIX
+  rednet.open(findModem(true))
+  c.serverID = rednet.lookup(PROTOCOL, SERVER)
+  function c.put(c, k, v) return clientPut(c.prefix .. key, value) end
+  function c.get(c, k, expect) return clientGet(c.prefix .. key, expect) end
+  function c.list(c, p) return clientList(c.prefix..(p or "")) end
+  function c.close rednet.close() end
+  setmetatable(c, {
+  __index = function(c, key) return clientGet(c.prefix..key) end
+  __setindex = function(c, key, value) return clientPut(c.prefix..key, value) end
+  })
+  return c
 end
+
 local run = function(content, ...)
-  writeFile(RUNFILE, content)
+  fileWrite(RUNFILE, content)
   shell.run(RUNFILE, ...)
 end
-local autoregister = function()
-  if shouldRegister and not fs.exists(REGFILE) then
-    writeFile(REGFILE, "registered: true")
-    local id = os.getComputerID()
-    local label = os.getComputerLabel()
-    put("registration."..id..".label", label)
-    put("registration."..id..".namespace", NAMESPACE)
-  end
-end
 
-if #args < 1 then
-  printUsage(); return
-elseif args[1] == "host" then
-  if #args == 1 then dbpath = nil
-  elseif #args == 2 then dbpath = args[2]
-  else printUsage(); return
-  end
-  host(dbpath)
-elseif args[1] == "get" then
-  autoregister()
-  if #args == 2 then from, to = args[2], args[2]
-  elseif #args == 3 then from, to = args[2], args[3]
-  else printUsage(); return
-  end
-  content = get(from)
-  if content == nil then print("content not found"); return end
-  writeFile(to, content)
-elseif args[1] == "put" then
-  autoregister()
-  if #args == 2 then from, to = args[2], args[2]
-  elseif #args == 3 then from, to = args[2], args[3]
-  else printUsage(); return
-  end
-  if not fs.exists(from) then print("file does not exist"); return end
-  content = readFile(from)
-  put(to, content)    
-elseif args[1] == "run" then
-  autoregister()
-  if #args >= 2 then
-    local from = args[2]
-    table.remove(args, 1)
-    table.remove(args, 1)
-    local runArgs = args
-  else printUsage(); return
-  end
-  content = get(from)
-  if content == nil then print("content not found"); return end
-  run(content, unpack(runArgs))
-elseif args[1] == "update" then
-  autoregister()
-  if #args == 1 then
-    old = readFile(KVKEY)
-    content = get(KVKEY, "")
-    if old == content then
-      print("Already up to date")
+USAGE = [[
+kv update
+  Update the 'kv' program to the lastest version
+kv get <keyname> [<filename>]
+  Get a program over wifi
+kv put [<filename>] <keyname>
+  Store a program over wifi
+kv run <keyname> [...]
+  Immediately run the named program
+kv host [FILE]
+  Host a database server.
+kv run list [FILTER]
+  List all database keys
+
+Edit .kv-prefix to set a global prefix")
+]]
+local args = { ... }
+template = {
+  -- subcommand, minArgs, maxArgs, handler
+  {"host", 0, 1, function(dbpath)
+    local h = Host(dbpath)
+  end},
+  {"list", 0, 1, function(filter)
+    local c = Client()
+    local keys = c:list(filter)
+    c:close()
+    for _, k in ipairs(keys) do
+      print(k)
+    end
+  end},
+  {"get", 1, 2, function(key, path)
+    local c = Client()
+    local content = c:get(key, true)
+    fileWrite(path or key, content)
+    c:close()
+  end},
+  {"put", 1, 2, function(path, key)
+    local c = Client()
+    local content = fileRead(path, true)
+    c:put(key or path, content)
+    c:close()
+  end},
+  {"run", 1, 999, function(key, ...)
+    local c = Client()
+    local content = c:get(key)
+    c:close()
+    run(content, ...)
+  end},
+  {"update", 0, 0, function()
+    local c = Client("") -- update never uses a prefix
+    local old = fileRead(KVFILE)
+    local new = c:get(KVKEY, true)
+    c:close()
+    if old == new then
+      print("already up to date")
     else
       print("updating")
-      writeFile(KVKEY, content)
-    end
-  else
-    printUsage(); return
+      fileWrite(KVFILE, new)
+    end      
+  end},
+}
+for _, t in ipairs(template) do
+  if args[1] == t[1] and #args-1 >= t[2] and #args-1 <= t[3] then
+    table.remove(args, 1)
+    t[4](unpack(args))
   end
-elseif args[1] == "host-dump" then
-  if #args == 2 then path, prefix = args[2], nil
-  elseif #args == 3 then path, prefix = args[2], args[3]
-  else printUsage(); return
-  end  
-  db = loadDB(path)
-  if not db then print("DB file not found"); return end
-  for k,v in pairs(db) do
-    if not prefix or (k and string.find(k, prefix, 1, true)==1) then
-      print(k, v)
-    end
-  end 
-else printUsage(); return
 end
+print(USAGE); return