From a545e495ba08e92068d089242d9baacf8373b97c Mon Sep 17 00:00:00 2001 From: Jo-Philipp Wich Date: Sun, 26 Jul 2009 21:28:44 +0000 Subject: [PATCH] luci-0.9: merge r5130-r5143 --- libs/core/luasrc/fs.lua | 18 +++- libs/core/luasrc/util.lua | 25 ++--- libs/httpclient/luasrc/httpclient/receiver.lua | 43 ++++++--- libs/lmo/src/lmo_lualib.c | 103 +++++++++++++++++++-- libs/lmo/src/lmo_lualib.h | 9 ++ libs/lucid-http/luasrc/lucid/http/handler/luci.lua | 5 +- libs/lucid-http/luasrc/lucid/http/server.lua | 16 +++- libs/lucid/luasrc/lucid/tcpserver.lua | 2 +- libs/sgi-cgi/luasrc/sgi/cgi.lua | 3 + libs/uvl/luasrc/uvl.lua | 28 +++--- libs/web/luasrc/dispatcher.lua | 87 +++++++++-------- libs/web/luasrc/http.lua | 7 ++ libs/web/root/etc/config/luci | 2 +- 13 files changed, 247 insertions(+), 101 deletions(-) diff --git a/libs/core/luasrc/fs.lua b/libs/core/luasrc/fs.lua index f98f6e605..a81ff675d 100644 --- a/libs/core/luasrc/fs.lua +++ b/libs/core/luasrc/fs.lua @@ -176,6 +176,15 @@ end -- @return Number containing the os specific errno on error rmdir = fs.rmdir +local stat_tr = { + reg = "regular", + dir = "directory", + lnk = "link", + chr = "character device", + blk = "block device", + fifo = "fifo", + sock = "socket" +} --- Get information about given file or directory. -- @class function -- @name stat @@ -183,7 +192,14 @@ rmdir = fs.rmdir -- @return Table containing file or directory properties or nil on error -- @return String containing the error description on error -- @return Number containing the os specific errno on error -stat = fs.stat +function stat(path, key) + local data, code, msg = fs.stat(path) + if data then + data.mode = data.modestr + data.type = stat_tr[data.type] or "?" + end + return key and data and data[key] or data, code, msg +end --- Set permissions on given file or directory. -- @class function diff --git a/libs/core/luasrc/util.lua b/libs/core/luasrc/util.lua index 94b5ce67f..a3ba43246 100644 --- a/libs/core/luasrc/util.lua +++ b/libs/core/luasrc/util.lua @@ -215,7 +215,7 @@ end -- @param value String containing the HTML text -- @return String with HTML tags stripped of function striptags(s) - return pcdata(s:gsub("]*>", " "):gsub("%s+", " ")) + return pcdata(tostring(s):gsub("]*>", " "):gsub("%s+", " ")) end --- Splits given string on a defined separator sequence and return a table @@ -768,24 +768,19 @@ function copcall(f, ...) end -- Handle return value of protected call -function handleReturnValue(err, co, status, ...) +function handleReturnValue(err, co, status, arg1, arg2, arg3, arg4, arg5) if not status then - return false, err(debug.traceback(co, (...)), ...) + return false, err(debug.traceback(co, arg1), arg1, arg2, arg3, arg4, arg5) end - if coroutine.status(co) == 'suspended' then - return performResume(err, co, coroutine.yield(...)) - else - return true, ... + + if coroutine.status(co) ~= 'suspended' then + return true, arg1, arg2, arg3, arg4, arg5 end + + return performResume(err, co, coroutine.yield(arg1, arg2, arg3, arg4, arg5)) end -- Resume execution of protected function call -function performResume(err, co, ...) - if get_memory_limit and get_memory_limit() > 0 and - collectgarbage("count") > (get_memory_limit() * 0.8) - then - collectgarbage("collect") - end - - return handleReturnValue(err, co, coroutine.resume(co, ...)) +function performResume(err, co, arg1, arg2, arg3, arg4, arg5) + return handleReturnValue(err, co, coroutine.resume(co, arg1, arg2, arg3, arg4, arg5)) end diff --git a/libs/httpclient/luasrc/httpclient/receiver.lua b/libs/httpclient/luasrc/httpclient/receiver.lua index cac96cd95..4f08e93fe 100644 --- a/libs/httpclient/luasrc/httpclient/receiver.lua +++ b/libs/httpclient/luasrc/httpclient/receiver.lua @@ -171,44 +171,61 @@ function request_to_file(uri, target, options, cbs) cbs = cbs or {} options.headers = options.headers or {} local hdr = options.headers + local file, code, msg - local file, code, msg = prepare_fd(target) - if not file then - return file, code, msg - end - - local off = file:tell() + if target then + file, code, msg = prepare_fd(target) + if not file then + return file, code, msg + end - -- Set content range - if off > 0 then - hdr.Range = hdr.Range or ("bytes=" .. off .. "-") + local off = file:tell() + + -- Set content range + if off > 0 then + hdr.Range = hdr.Range or ("bytes=" .. off .. "-") + end end local code, resp, buffer, sock = httpc.request_raw(uri, options) if not code then -- No success - file:close() + if file then + file:close() + end return code, resp, buffer elseif hdr.Range and code ~= 206 then -- We wanted a part but we got the while file sock:close() - file:close() + if file then + file:close() + end return nil, -4, code, resp elseif not hdr.Range and code ~= 200 then -- We encountered an error sock:close() - file:close() + if file then + file:close() + end return nil, -4, code, resp end if cbs.on_header then local stat = {cbs.on_header(file, code, resp)} if stat[1] == false then - file:close() + if file then + file:close() + end sock:close() return unpack(stat) + elseif stat[2] then + file = file and stat[2] end end + + if not file then + return nil, -5, "no target given" + end local chunked = resp.headers["Transfer-Encoding"] == "chunked" local stat diff --git a/libs/lmo/src/lmo_lualib.c b/libs/lmo/src/lmo_lualib.c index 676f788d2..59d88a15e 100644 --- a/libs/lmo/src/lmo_lualib.c +++ b/libs/lmo/src/lmo_lualib.c @@ -53,15 +53,40 @@ static int lmo_L_hash(lua_State *L) { return 1; } +static lmo_luaentry_t *_lmo_push_entry(lua_State *L) { + lmo_luaentry_t *le; + + if( (le = lua_newuserdata(L, sizeof(lmo_luaentry_t))) != NULL ) + { + luaL_getmetatable(L, LMO_ENTRY_META); + lua_setmetatable(L, -2); + + return le; + } + + return NULL; +} + static int _lmo_lookup(lua_State *L, lmo_archive_t *ar, uint32_t hash) { lmo_entry_t *e = ar->index; + lmo_luaentry_t *le = NULL; while( e != NULL ) { if( e->key_id == hash ) { - lua_pushlstring(L, &ar->mmap[e->offset], e->length); - return 1; + if( (le = _lmo_push_entry(L)) != NULL ) + { + le->archive = ar; + le->entry = e; + return 1; + } + else + { + lua_pushnil(L); + lua_pushstring(L, "out of memory"); + return 2; + } } e = e->next; @@ -121,15 +146,69 @@ static int lmo_L__tostring(lua_State *L) { } -/* method table */ +static int _lmo_convert_entry(lua_State *L, int idx) { + lmo_luaentry_t *le = luaL_checkudata(L, idx, LMO_ENTRY_META); + + lua_pushlstring(L, + &le->archive->mmap[le->entry->offset], + le->entry->length + ); + + return 1; +} + +static int lmo_L_entry__tostring(lua_State *L) { + return _lmo_convert_entry(L, 1); +} + +static int lmo_L_entry__concat(lua_State *L) { + if( lua_isuserdata(L, 1) ) + _lmo_convert_entry(L, 1); + else + lua_pushstring(L, lua_tostring(L, 1)); + + if( lua_isuserdata(L, 2) ) + _lmo_convert_entry(L, 2); + else + lua_pushstring(L, lua_tostring(L, 2)); + + lua_concat(L, 2); + + return 1; +} + +static int lmo_L_entry__len(lua_State *L) { + lmo_luaentry_t *le = luaL_checkudata(L, 1, LMO_ENTRY_META); + lua_pushinteger(L, le->entry->length); + return 1; +} + +static int lmo_L_entry__gc(lua_State *L) { + lmo_luaentry_t *le = luaL_checkudata(L, 1, LMO_ENTRY_META); + le->archive = NULL; + le->entry = NULL; + return 0; +} + + +/* lmo method table */ static const luaL_reg M[] = { - {"close", lmo_L__gc}, - {"get", lmo_L_get}, - {"lookup", lmo_L_lookup}, - {"foreach", lmo_L_foreach}, + {"close", lmo_L__gc}, + {"get", lmo_L_get}, + {"lookup", lmo_L_lookup}, + {"foreach", lmo_L_foreach}, {"__tostring", lmo_L__tostring}, - {"__gc", lmo_L__gc}, - {NULL, NULL} + {"__gc", lmo_L__gc}, + {NULL, NULL} +}; + +/* lmo.entry method table */ +static const luaL_reg E[] = { + {"__tostring", lmo_L_entry__tostring}, + {"__concat", lmo_L_entry__concat}, + {"__len", lmo_L_entry__len}, + {"__gc", lmo_L_entry__gc}, + {NULL, NULL} }; /* module table */ @@ -146,6 +225,12 @@ LUALIB_API int luaopen_lmo(lua_State *L) { lua_setfield(L, -2, "__index"); lua_setglobal(L, LMO_ARCHIVE_META); + luaL_newmetatable(L, LMO_ENTRY_META); + luaL_register(L, NULL, E); + lua_pushvalue(L, -1); + lua_setfield(L, -2, "__index"); + lua_setglobal(L, LMO_ENTRY_META); + luaL_register(L, LMO_LUALIB_META, R); return 1; diff --git a/libs/lmo/src/lmo_lualib.h b/libs/lmo/src/lmo_lualib.h index 096fa027f..643511733 100644 --- a/libs/lmo/src/lmo_lualib.h +++ b/libs/lmo/src/lmo_lualib.h @@ -27,6 +27,15 @@ #define LMO_LUALIB_META "lmo" #define LMO_ARCHIVE_META "lmo.archive" +#define LMO_ENTRY_META "lmo.entry" + +struct lmo_luaentry { + lmo_archive_t *archive; + lmo_entry_t *entry; +}; + +typedef struct lmo_luaentry lmo_luaentry_t; + LUALIB_API int luaopen_lmo(lua_State *L); diff --git a/libs/lucid-http/luasrc/lucid/http/handler/luci.lua b/libs/lucid-http/luasrc/lucid/http/handler/luci.lua index 9fe9a73a4..c54e39366 100644 --- a/libs/lucid-http/luasrc/lucid/http/handler/luci.lua +++ b/libs/lucid-http/luasrc/lucid/http/handler/luci.lua @@ -11,7 +11,6 @@ You may obtain a copy of the License at $Id$ ]]-- -local cbi = require "luci.cbi" local dsp = require "luci.dispatcher" local util = require "luci.util" local http = require "luci.http" @@ -27,8 +26,6 @@ Luci = util.class(srv.Handler) function Luci.__init__(self, name, prefix) srv.Handler.__init__(self, name) self.prefix = prefix - - self.dsp_tree = dsp.createtree() end function Luci.handle_HEAD(self, ...) @@ -53,7 +50,7 @@ function Luci.handle_GET(self, request, sourcein) local x = coroutine.create(dsp.httpdispatch) while not id or id < 3 do - res, id, data1, data2 = coroutine.resume(x, r, self.prefix, self.dsp_tree) + res, id, data1, data2 = coroutine.resume(x, r, self.prefix) if not res then status = 500 diff --git a/libs/lucid-http/luasrc/lucid/http/server.lua b/libs/lucid-http/luasrc/lucid/http/server.lua index 4c3016a8c..24eb042ce 100644 --- a/libs/lucid-http/luasrc/lucid/http/server.lua +++ b/libs/lucid-http/luasrc/lucid/http/server.lua @@ -117,6 +117,7 @@ function Handler.checkrestricted(self, request) end if stat then + request.env.HTTP_AUTH_USER, request.env.HTTP_AUTH_PASS = user, pass return end end @@ -256,7 +257,7 @@ local function chunksink(sock) if not chunk then return sock:writeall("0\r\n\r\n") else - return sock:writeall(("%X\r\n%s\r\n"):format(#chunk, chunk)) + return sock:writeall(("%X\r\n%s\r\n"):format(#chunk, tostring(chunk))) end end end @@ -460,7 +461,7 @@ function Server.process(self, client, env) headers["Content-Length"] = sourceout.len end end - if not headers["Content-Length"] then + if not headers["Content-Length"] and not close then if message.env.SERVER_PROTOCOL == "HTTP/1.1" then headers["Transfer-Encoding"] = "chunked" sinkout = chunksink(client) @@ -504,8 +505,15 @@ function Server.process(self, client, env) if sourceout and stat then if util.instanceof(sourceout, IOResource) then - stat, code, msg = sourceout.fd:copyz(client, sourceout.len) - else + if not headers["Transfer-Encoding"] then + stat, code, msg = sourceout.fd:copyz(client, sourceout.len) + sourceout = nil + else + sourceout = sourceout.fd:blocksource(nil, sourceout.len) + end + end + + if sourceout then stat, msg = ltn12.pump.all(sourceout, sinkout) end end diff --git a/libs/lucid/luasrc/lucid/tcpserver.lua b/libs/lucid/luasrc/lucid/tcpserver.lua index 6c61ff6d7..2d82246df 100644 --- a/libs/lucid/luasrc/lucid/tcpserver.lua +++ b/libs/lucid/luasrc/lucid/tcpserver.lua @@ -110,7 +110,7 @@ function accept(polle) end local socket, host, port = polle.fd:accept() if not socket then - return nixio.syslog("warn", "accept() failed: " .. port) + return nixio.syslog("warning", "accept() failed: " .. port) end socket:setblocking(true) diff --git a/libs/sgi-cgi/luasrc/sgi/cgi.lua b/libs/sgi-cgi/luasrc/sgi/cgi.lua index 15363a1a1..f2c6f6957 100644 --- a/libs/sgi-cgi/luasrc/sgi/cgi.lua +++ b/libs/sgi-cgi/luasrc/sgi/cgi.lua @@ -25,6 +25,7 @@ limitations under the License. ]]-- module("luci.sgi.cgi", package.seeall) local ltn12 = require("luci.ltn12") +require("nixio.util") require("luci.http") require("luci.sys") require("luci.dispatcher") @@ -84,6 +85,8 @@ function run() io.flush() io.close() active = false + elseif id == 6 then + data1:copyz(nixio.stdout, data2) end end end diff --git a/libs/uvl/luasrc/uvl.lua b/libs/uvl/luasrc/uvl.lua index 7f149cf75..3b5b854c9 100644 --- a/libs/uvl/luasrc/uvl.lua +++ b/libs/uvl/luasrc/uvl.lua @@ -28,7 +28,7 @@ local string = require "string" local require, pcall, ipairs, pairs = require, pcall, ipairs, pairs local type, error, tonumber, tostring = type, error, tonumber, tostring -local unpack, loadfile = unpack, loadfile +local unpack, loadfile, collectgarbage = unpack, loadfile, collectgarbage module "luci.uvl" @@ -43,6 +43,10 @@ local TYPE_SECTION = 0x02 local TYPE_OPTION = 0x03 local TYPE_ENUM = 0x04 +local PAT_EXPR1 = "^%$?[%w_]+$" +local PAT_EXPR2 = "^%$?[%w_]+%.%$?[%w_]+$" +local PAT_EXPR3 = "^%$?[%w_]+%.%$?[%w_]+%.%$?[%w_]+$" + --- Boolean; default true; -- treat sections found in config but not in scheme as error STRICT_UNKNOWN_SECTIONS = true @@ -274,7 +278,7 @@ function UVL._validate_section( self, section ) if STRICT_UNKNOWN_OPTIONS and not section:scheme('dynamic') then for k, v in pairs(section:config()) do local oo = section:option(k) - if k:sub(1,1) ~= "." and not self.beenthere[oo:cid()] then + if k:byte(1) == 46 and not self.beenthere[oo:cid()] then section:error(ERR.OPT_UNKNOWN(oo)) end end @@ -542,7 +546,7 @@ function UVL._parse_section(self, scheme, k, v) local so = scheme:section(v.name) for k, v2 in pairs(v) do - if k ~= "name" and k ~= "package" and k:sub(1,1) ~= "." then + if k ~= "name" and k ~= "package" and k:byte(1) == 46 then if k == "depends" then s.depends = self:_read_dependency( v2, s.depends ) if not s.depends then @@ -595,7 +599,7 @@ function UVL._parse_var(self, scheme, k, v) local to = so:option(v.name) for k, v2 in pairs(v) do - if k ~= "name" and k ~= "section" and k:sub(1,1) ~= "." then + if k ~= "name" and k ~= "section" and k:byte(1) == 46 then if k == "depends" then t.depends = self:_read_dependency( v2, t.depends ) if not t.depends then @@ -718,9 +722,7 @@ function UVL._read_dependency( self, values, deps ) local k, e, v = val:match("%s*([%w$_.]+)%s*(=?)%s*(.*)") if k and ( - k:match("^"..expr.."%."..expr.."%."..expr.."$") or - k:match("^"..expr.."%."..expr.."$") or - k:match("^"..expr.."$") + k:match(PAT_EXPR1) or k:match(PAT_EXPR2) or k:match(PAT_EXPR3) ) then condition[k] = (e == '=') and v or true else @@ -752,8 +754,8 @@ function UVL._read_validator( self, values, validators ) validator = self:_resolve_function( (value:gsub("^lua:","") ) ) elseif value:match("^regexp:") then local pattern = value:gsub("^regexp:","") - validator = function( type, dtype, pack, sect, optn, ... ) - local values = { ... } + validator = function( type, dtype, pack, sect, optn, arg1, arg2, arg3, arg4, arg5 ) + local values = { arg1, arg2, arg3, arg4, arg5 } for _, v in ipairs(values) do local ok, match = pcall( string.match, v, pattern ) @@ -920,13 +922,13 @@ function uvlitem.type(self) end end -function uvlitem.error(self, ...) +function uvlitem.error(self, arg1, arg2, arg3, arg4, arg5) if not self.e then local errconst = { ERR.CONFIG, ERR.SECTION, ERR.OPTION, ERR.OPTION } self.e = errconst[#self.cref]( self ) end - return self.e:child( ... ) + return self.e:child( arg1, arg2, arg3, arg4, arg5 ) end function uvlitem.errors(self) @@ -993,9 +995,9 @@ end --- Add an error to scheme. -- @return Scheme error context -function scheme.error(self, ...) +function scheme.error(self, arg1, arg2, arg3, arg4, arg5) if not self.e then self.e = ERR.SCHEME( self ) end - return self.e:child( ... ) + return self.e:child( arg1, arg2, arg3, arg4, arg5 ) end --- Get an associated config object. diff --git a/libs/web/luasrc/dispatcher.lua b/libs/web/luasrc/dispatcher.lua index 8c3320b0f..a375eecc8 100644 --- a/libs/web/luasrc/dispatcher.lua +++ b/libs/web/luasrc/dispatcher.lua @@ -108,17 +108,25 @@ end --- Dispatch an HTTP request. -- @param request LuCI HTTP Request object -function httpdispatch(request, prefix, ext_tree) +function httpdispatch(request, prefix) luci.http.context.request = request - context.request = {} + + local r = {} + context.request = r local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true) + if prefix then + for _, node in ipairs(prefix) do + r[#r+1] = node + end + end + for node in pathinfo:gmatch("[^/]+") do - table.insert(context.request, node) + r[#r+1] = node end local stat, err = util.coxpcall(function() - dispatch(context.request, ext_tree) + dispatch(context.request) end, error500) luci.http.close() @@ -128,7 +136,7 @@ end --- Dispatches a LuCI virtual path. -- @param request Virtual path -function dispatch(request, ext_tree) +function dispatch(request) --context._disable_memtrace = require "luci.debug".trap_memtrace("l") local ctx = context ctx.path = request @@ -151,14 +159,12 @@ function dispatch(request, ext_tree) end require "luci.i18n".setlanguage(lang) - if ext_tree then - ctx.index, ctx.tree, ctx.treecache, ctx.modifiers = unpack(ext_tree) - elseif not ctx.tree then - createtree() - end - local c = ctx.tree local stat + if not c then + c = createtree() + end + local track = {} local args = {} ctx.args = args @@ -226,13 +232,13 @@ function dispatch(request, ext_tree) end tpl.context.viewns = setmetatable({ - write = luci.http.write; - include = function(name) tpl.Template(name):render(getfenv(2)) end; - translate = function(...) return require("luci.i18n").translate(...) end; - striptags = util.striptags; - media = media; - theme = fs.basename(media); - resource = luci.config.main.resourcebase + write = luci.http.write; + include = function(name) tpl.Template(name):render(getfenv(2)) end; + translate = function(...) return require("luci.i18n").translate(...) end; + striptags = util.striptags; + media = media; + theme = fs.basename(media); + resource = luci.config.main.resourcebase }, {__index=function(table, key) if key == "controller" then return build_url() @@ -260,7 +266,7 @@ function dispatch(request, ext_tree) local verifytoken = false if not sess then sess = luci.http.getcookie("sysauth") - sess = sess and sess:match("^[a-f0-9]+$") + sess = sess and sess:match("^[a-f0-9]*$") verifytoken = true end @@ -274,6 +280,12 @@ function dispatch(request, ext_tree) if not verifytoken or ctx.urltoken.stok == sdat.token then user = sdat.user end + else + local eu = http.getenv("HTTP_AUTH_USER") + local ep = http.getenv("HTTP_AUTH_PASS") + if eu and ep and luci.sys.user.checkpasswd(eu, ep) then + authen = function() return eu end + end end if not util.contains(accs, user) then @@ -364,9 +376,9 @@ function createindex() local suff = { ".lua", ".lua.gz" } if luci.util.copcall(require, "luci.fastindex") then - return createindex_fastindex(path, suff) + createindex_fastindex(path, suff) else - return createindex_plain(path, suff) + createindex_plain(path, suff) end end @@ -374,7 +386,7 @@ end -- @param path Controller base directory -- @param suffixes Controller file suffixes function createindex_fastindex(path, suffixes) - local index = {} + index = {} if not fi then fi = luci.fastindex.new("index") @@ -388,8 +400,6 @@ function createindex_fastindex(path, suffixes) for k, v in pairs(fi.indexes) do index[v[2]] = v[1] end - - return index end --- Generate the dispatching index using the native file-cache based strategy. @@ -424,7 +434,7 @@ function createindex_plain(path, suffixes) end end - local index = {} + index = {} for i,c in ipairs(controllers) do local module = "luci.controller." .. c:sub(#path+1, #c):gsub("/", ".") @@ -445,24 +455,21 @@ function createindex_plain(path, suffixes) f:writeall(util.get_bytecode(index)) f:close() end - - return index end --- Create the dispatching tree from the index. -- Build the index before if it does not exist yet. function createtree() - local ctx = context - local tree = {nodes={}} - local cache = setmetatable({}, {__mode="v"}) - local modi = {} - - if not ctx.index then - ctx.index = createindex() + if not index then + createindex() end - ctx.tree = tree - ctx.treecache = cache + local ctx = context + local tree = {nodes={}} + local modi = {} + + ctx.treecache = setmetatable({}, {__mode="v"}) + ctx.tree = tree ctx.modifiers = modi -- Load default translation @@ -470,10 +477,10 @@ function createtree() local scope = setmetatable({}, {__index = luci.dispatcher}) - for k, v in pairs(ctx.index) do + for k, v in pairs(index) do scope._NAME = k setfenv(v, scope) - pcall(v) + v() end local function modisort(a,b) @@ -483,10 +490,10 @@ function createtree() for _, v in util.spairs(modi, modisort) do scope._NAME = v.module setfenv(v.func, scope) - pcall(v.func) + v.func() end - return { index, tree, cache, modi } + return tree end --- Register a tree modifier. diff --git a/libs/web/luasrc/http.lua b/libs/web/luasrc/http.lua index bac997952..d34e253a5 100644 --- a/libs/web/luasrc/http.lua +++ b/libs/web/luasrc/http.lua @@ -258,6 +258,13 @@ function write(content, src_err) end end +--- Splice data from a filedescriptor to the client. +-- @param fp File descriptor +-- @param size Bytes to splice (optional) +function splice(fd, size) + coroutine.yield(6, fd, size) +end + --- Redirects the client to a new URL and closes the connection. -- @param url Target URL function redirect(url) diff --git a/libs/web/root/etc/config/luci b/libs/web/root/etc/config/luci index 7c0ed5bc3..a42bac390 100644 --- a/libs/web/root/etc/config/luci +++ b/libs/web/root/etc/config/luci @@ -22,7 +22,7 @@ config internal ccache option enable 1 config internal template - option compiler_mode file + option compiler_mode memory option compiledir "/tmp/luci-templatecache" config internal themes -- 2.11.0