Browse Source

Add LDoc configuration and slightly extend doxify.sh.

Add documentation in various places.
experimental/hubcheck
Wouter R 8 years ago
parent
commit
97564de8fc
  1. 16
      config.ld
  2. 11
      doxify.sh
  3. 46
      src/conf_defaults.lua
  4. 2
      src/rest/request.lua
  5. 63
      src/rest/response.lua
  6. 133
      src/script/d3d-updater.lua
  7. 14
      src/util/logger.lua
  8. 104
      src/util/settings.lua
  9. 21
      src/util/utils.lua

16
config.ld

@ -0,0 +1,16 @@
file = {
'src',
exclude = {'src/test'}
}
dir = 'docs'
format = 'markdown'
readme = 'README.md'
title = "WiFibox firmware documentation"
project = "Wifibox firmware"
description = "This project provides the REST API which is part of the Doodle3D wifibox."
alias('p', 'param')
all = true

11
doxify.sh

@ -10,7 +10,16 @@ HTML_PATH=$WIFIBOX_BASE_DIR/docs
SRC_DIR=$WIFIBOX_BASE_DIR/src
FILESPEC=$WIFIBOX_BASE_DIR/src #replace by config.ld so we can also specify README.md?
$LDOC -d $HTML_PATH $FILESPEC -a -f markdown $@
LUA_VERSION=`lua -v 2>&1 | awk -F" " '{print $2}'`
echo $LUA_VERSION | grep -q "^5.2"
if [ $? -ne 0 ]; then
echo "Lua 5.2 is needed to run this script (as well as luarocks), you have $LUA_VERSION."
exit 1
fi
#$LDOC -d $HTML_PATH $FILESPEC -a -f markdown $@
$LDOC .
if [ $? -eq 127 ]; then
echo "$0: It looks like the ldoc program could not be found, please configure the LDOC variable correctly and make sure ldoc is installed on your system."

46
src/conf_defaults.lua

@ -1,34 +1,40 @@
--[[--
TODO: finish documentation
This file contains all valid configuration keys, their default values and optional constraints.
The table names are used as configuration key names, where underscores ('_') may be used to denote semi-categories.
The settings interface replaces periods ('.') by underscores so for instance 'network.ap.address' will
be translated to 'network_ap_address'. Multi-word names should be notated as camelCase.
Valid fields for the tables are:
- default: the default value (used when the key is not set in UCI config)
- type: used for basic type checking, one of bool, int, float or string
- description: A descriptive text usable by API clients
- min, max, regex: optional constraints (min and max constrain value for numbers, or length for strings)
NOTE that the all-caps definitions will be changed into configuration keys, or moved to a different location
]]--
---
-- This file contains all valid configuration keys, their default values and optional constraints.
-- The table names are used as configuration key names, where underscores ('`_`') may be used to denote semi-categories.
-- The settings interface replaces periods ('`.`') by underscores so for instance `network.ap.address` will
-- be translated to `network_ap_address`. Multi-word names should be notated as camelCase.
--
-- Valid fields for the tables are:
--
-- - _default_: the default value (used when the key is not set in UCI config)
-- - _type_: used for basic type checking, one of bool, int, float or string
-- - _description_: A descriptive text usable by API clients
-- - _min_, _max_, _regex_: optional constraints (min and max constrain value for numbers, or length for strings)
-- - _isValid_: an optional function which should return true for valid values and false for invalid ones
--
-- The configuration keys themselves document themselves rather well, hence they are not included in the generated documentation.
--
-- NOTE: the all-caps definitions should be changed into configuration keys, or moved to a better location.
local printer = require('util.printer')
local log = require('util.logger')
local utils = require('util.utils')
local M = {}
--NOTE: pcall protects from invocation exceptions, which is what we need except
--during debugging. This flag replaces them with a normal call so we can inspect stack traces.
--- This constant should only be true during development. It replaces `pcall` by regular `call`.
-- Pcall protects the script from invocation exceptions, which is what we need except during debugging.
-- When this flag is true, normal calls will be used so we can inspect stack traces.
M.DEBUG_PCALLS = false
--This enables debugging of the REST API from the command-line, specify the path and optionally the request method as follows: 'p=/mod/func rq=POST'
--- This constant enables debugging of the REST API from the command-line by emulating GET/POST requests.
-- Specify the path and optionally the request method as follows: `d3dapi p=/mod/func r=POST`.
M.DEBUG_API = true
--REST responses will contain 'module' and 'function' keys describing what was requested
--- If enabled, REST responses will contain 'module' and 'function' keys describing what was requested.
M.API_INCLUDE_ENDPOINT_INFO = false
M.API_BASE_URL_PATH = 'doodle3d.com' -- includes any base path if necessary (e.g. 'localhost/~user')
--- This base path is used in @{rest.response}. It includes any base path if necessary (e.g. 'localhost/~user').
M.API_BASE_URL_PATH = 'doodle3d.com'
M.network_ap_ssid = {
default = 'Doodle3D-%%MAC_ADDR_TAIL%%',
@ -50,7 +56,7 @@ M.network_ap_key = {
type = 'string',
description = 'Access Point security key',
isValid = function(value)
if value == "" then
if value == "" then
return true;
elseif value:len() < 8 then
return false, "too short"

2
src/rest/request.lua

@ -1,3 +1,5 @@
---
-- This object represents an HTTP request object, part of the REST API.
local util = require('util.utils') -- required for string:split()
local urlcode = require('util.urlcode')
local confDefaults = require('conf_defaults')

63
src/rest/response.lua

@ -1,3 +1,7 @@
---
-- The REST response object handles all operations necessary to generate a HTTP response.
-- It knows about the request, ensures the correct HTTP headers are present and
-- allows to set a status (one of `success`, `fail` or `error`) and addtional data.
local JSON = require('util/JSON')
local settings = require('util.settings')
local defaults = require('conf_defaults')
@ -10,6 +14,9 @@ local REQUEST_ID_ARGUMENT = 'rq_id'
M.httpStatusCode, M.httpStatusText, M.contentType = nil, nil, nil
M.binaryData, M.binarySavename = nil, nil
--- Print a HTTP header line with the given type and value.
-- @string headerType
-- @string headerValue
local function printHeaderLine(headerType, headerValue)
io.write(headerType .. ": " .. headerValue .. "\r\n")
end
@ -21,7 +28,10 @@ setmetatable(M, {
end
})
--requestObject should always be passed (except on init failure, when it is not yet available)
--- Instantiates a new response object initialized with the given @{request} object.
-- The request object should always be passed, except on init failure, when it is not yet available.
-- @tparam request requestObject The object representing the HTTP request.
-- @treturn response A newly instantiated response object.
function M.new(requestObject)
local self = setmetatable({}, M)
self.body = { status = nil, data = {} }
@ -29,9 +39,9 @@ function M.new(requestObject)
self:setContentType('text/plain;charset=UTF-8')
--self:setContentType('application/json;charset=UTF-8')
-- a queue for functions to be executed when the response has bin given
-- needed for api calls like network/associate, which requires a restart of the webserver
self.postResponseQueue = {}
-- A queue for functions to be executed when the response has been given.
-- Needed for api calls like network/associate, which requires a restart of the webserver.
self.postResponseQueue = {}
if requestObject ~= nil then
local rqId = requestObject:get(REQUEST_ID_ARGUMENT)
@ -46,25 +56,37 @@ function M.new(requestObject)
return self
end
--- Set HTTP status (the default is `200 OK`).
-- @number code The status code.
-- @string text The status text, this should of course correspond to the given code.
function M:setHttpStatus(code, text)
if code ~= nil then self.httpStatusCode = code end
if text ~= nil then self.httpStatusText = text end
end
--- Set the HTTP content type (the default is `text/plain;charset=UTF-8`).
-- @string contentType The content type to set.
function M:setContentType(contentType)
if contentType ~= nil then self.contentType = contentType end
end
--- Set REST status to success.
-- @string[opt] msg An optional human-readable message to include with the status.
function M:setSuccess(msg)
self.body.status = 'success'
if msg ~= '' then self.body.msg = msg end
end
--- Set REST status to failure.
-- @string[opt] msg An optional human-readable message to include with the status.
function M:setFail(msg)
self.body.status = 'fail'
if msg ~= '' then self.body.msg = msg end
end
--- Set REST status to error.
-- A reference to the API documentation will also be included.
-- @string[opt] msg An optional human-readable message to include with the status.
function M:setError(msg)
self.body.status = 'error'
if msg ~= '' then self.body.msg = msg end
@ -72,17 +94,31 @@ function M:setError(msg)
self:addData('more_info', 'http://' .. defaults.API_BASE_URL_PATH .. '/wiki/wiki/communication-api')
end
--NOTE: with this method, to add nested data, it is necessary to precreate the table and add it with its root key
--(e.g.: response:addData('data', {f1=3, f2='x'}))
--- Adds a data item to the response, this will be included under the `data` item of the json text.
--
-- NOTE: To add nested data with this method, it is necessary to precreate the table
-- and then add that with its root key. (e.g., `response:addData('f_values', {f1=3, f2='x'})`)
--
-- After calling this, any binary data set by @{M:setBinaryFileData} will not be sent anymore.
--
-- @string k The key of the item to set.
-- @param v The value to set.
function M:addData(k, v)
self.body.data[k] = v
self.binaryData = nil
end
--- Queue a function for execution after the response has been passed back to the webserver.
--
-- Note that this is not useful in many cases since the webserver will not actually send
-- the response until this script finishes. So for instance if the queue contains code
-- to restart the webserver, the response will never be sent out.
-- @func fn The function to queue.
function M:addPostResponseFunction(fn)
table.insert(self.postResponseQueue, fn)
end
--- Call all function on the post-response queue, see @{M:addPostResponseFunction} for details and a side-note.
function M:executePostResponseQueue()
--local utils = require('util.utils')
--local log = require('util.logger')
@ -91,16 +127,23 @@ function M:executePostResponseQueue()
for i,fn in ipairs(self.postResponseQueue) do fn() end
end
--- Returns an API url pointing to @{conf_defaults.API_BASE_URL_PATH}, which is quite useless.
-- @string mod
-- @string func
-- @treturn string A not-so-useful URL.
function M:apiURL(mod, func)
if not mod then return nil end
if func then func = '/' .. func else func = "" end
return 'http://' .. defaults.API_BASE_URL_PATH .. '/cgi-bin/d3dapi/' .. mod .. func
end
--- Returns the body data contained in this object as [JSON](http://www.json.org/).
-- @treturn string The JSON data.
function M:serializeAsJson()
return JSON:encode(self.body)
end
--- Writes HTTP headers, followed by an HTTP body containing JSON data to stdout.
function M:send()
printHeaderLine("Status", self.httpStatusCode .. " " .. self.httpStatusText)
printHeaderLine("Content-Type", self.contentType)
@ -116,6 +159,14 @@ function M:send()
end
end
--- Sets the response object to return binary data instead of JSON as its body.
--
-- After calling this, REST data and status will not be sent anymore.
-- @string rFile The file on the local file system to read the data from.
-- @string saveName The file name to suggest the user to save the data in.
-- @string contentType The content type of the data.
-- @treturn bool|nil True, or nil in which case the second argument will be set.
-- @treturn ?string An error message if the first argument is nil.
function M:setBinaryFileData(rFile, saveName, contentType)
if type(rFile) ~= 'string' or rFile:len() == 0 then return false end

133
src/script/d3d-updater.lua

@ -1,5 +1,8 @@
#!/usr/bin/env lua
--- This script provides an interface to upgrade or downgrade the Doodle3D wifibox.
-- It can both be used as a standalone command-line tool and as a Lua library.
-- TODO/NOTES:
-- add to status: validImage: none|<version> (can use checkValidImage for this)
-- any more TODO's across this file?
@ -7,9 +10,8 @@
-- MAYBE/LATER:
-- add API calls to retrieve a list of all versions with their info (i.e., the result of getAvailableVersions)
-- wget: add provision (in verbose mode?) to use -v instead of -q and disable output redirection
-- wget: add provision (in verbose mode?) to use '-v' instead of '-q' and disable output redirection
-- document index file format (Version first, then in any order: Files: sysup; factory, FileSize: sysup; factory, MD5: sysup; factory, ChangelogStart:, ..., ChangelogEnd:)
-- remove /etc/wifibox-version on macbook...
-- copy improved fileSize back to utils (add unit tests!)
-- create new utils usable by updater as well as api? (remove dependencies on uci and logger etc)
-- note: take care not to print any text in module functions, as this breaks http responses
@ -17,18 +19,39 @@
local M = {}
-- NOTE: 'INSTALLED' will never be returned (and probably neither will 'INSTALLING') since in that case the device is flashing or rebooting
M.STATE = { NONE = 1, DOWNLOADING = 2, DOWNLOAD_FAILED = 3, IMAGE_READY = 4, INSTALLING = 5, INSTALLED = 6, INSTALL_FAILED = 7 }
--- Possible states the updater can be in, they are stored in @{STATE_FILE}.
-- @table STATE
M.STATE = {
NONE = 1, -- @{STATE_FILE} does not exist
DOWNLOADING = 2, -- downloading is started but not finished yet
DOWNLOAD_FAILED = 3, -- download failed (often occurs when the wifibox is not connected to internet)
IMAGE_READY = 4, -- download succeeded and the image is valid
INSTALLING = 5, -- image is being installed (this state will probably never be returned since the box is flashing/rebooting)
INSTALLED = 6, -- image has been installed successfully (this state will never be returned since the box will reboot)
INSTALL_FAILED = 7 -- installation failed
}
-- Names for the states in @{STATE}, these are returned through the REST API.
M.STATE_NAMES = {
[M.STATE.NONE] = 'none', [M.STATE.DOWNLOADING] = 'downloading', [M.STATE.DOWNLOAD_FAILED] = 'download_failed', [M.STATE.IMAGE_READY] = 'image_ready',
[M.STATE.INSTALLING] = 'installing', [M.STATE.INSTALLED] = 'installed', [M.STATE.INSTALL_FAILED] = 'install_failed'
}
--- The base URL to use for finding update files.
-- This URL will usually contain both an OpenWRT feed directory and an `images`-directory.
-- This script uses only the latter, and expects to find the file @{IMAGE_INDEX_FILE} there.
M.DEFAULT_BASE_URL = 'http://doodle3d.com/updates'
--M.DEFAULT_BASE_URL = 'http://localhost/~USERNAME/wifibox/updates'
--- The index file containing metadata on update images.
M.IMAGE_INDEX_FILE = 'wifibox-image.index'
--- Path to the updater cache.
M.CACHE_PATH = '/tmp/d3d-updater'
--- Name of the file to store current state in, this file resides in @{CACHE_PATH}.
M.STATE_FILE = 'update-state'
M.WGET_OPTIONS = "-q -t 1 -T 30"
--M.WGET_OPTIONS = "-v -t 1 -T 30"
@ -43,7 +66,10 @@ local baseUrl = M.DEFAULT_BASE_URL -- default, can be overwritten by M.setBaseUr
-- LOCAL FUNCTIONS --
---------------------
-- use level==1 for important messages, 0 for regular messages and -1 for less important messages
--- Log a message with the given level, if logging is enabled for that level.
-- Messages will be written to [stdout](http://www.cplusplus.com/reference/cstdio/stdout/), or logged using the logger set with @{setLogger}.
-- @number lvl Level to log to, use 1 for important messages, 0 for regular messages and -1 for less important messages.
-- @string msg The message to log.
local function P(lvl, msg)
if log then
if lvl == -1 then log:debug(msg)
@ -54,15 +80,28 @@ local function P(lvl, msg)
end
end
--- Log a debug message, this function wraps @{P}.
-- The message will be logged with level -1 and be prefixed with '(DBG)'.
-- @string msg The message to log.
local function D(msg) P(-1, (log and msg or "(DBG) " .. msg)) end
--- Log an error.
-- Messages will be written to [stderr](http://www.cplusplus.com/reference/cstdio/stderr/), or logged using the logger set with @{setLogger}.
-- @string msg The message to log.
local function E(msg)
if log then log:error(msg)
else io.stderr:write(msg .. '\n')
end
end
-- splits the return status from os.execute (see: http://stackoverflow.com/questions/16158436/how-to-shift-and-mask-bits-from-integer-in-lua)
--- Splits the return status from `os.execute`, which consists of two bytes.
--
-- `os.execute` internally calls [system](http://linux.die.net/man/3/system),
-- which usually returns the command exit status as high byte (see [WEXITSTATUS](http://linux.die.net/man/2/wait)).
-- Furthermore, see [shifting bits in Lua](http://stackoverflow.com/questions/16158436/how-to-shift-and-mask-bits-from-integer-in-lua).
-- @number exitStatus The combined exit status.
-- @treturn number The command exit status.
-- @treturn number The `os.execute`/[system](http://linux.die.net/man/3/system) return status.
local function splitExitStatus(exitStatus)
if exitStatus == -1 then return -1,-1 end
local cmdStatus = math.floor(exitStatus / 256)
@ -70,6 +109,9 @@ local function splitExitStatus(exitStatus)
return cmdStatus, systemStatus
end
--- Returns a human-readable message for a [wget exit status](http://www.gnu.org/software/wget/manual/wget.html#Exit-Status).
-- @number exitStatus An exit status from wget.
-- @treturn string|number Either the status followed by a description, or a message indicating the call was interrupted, or just the status if it was not recognized.
local function wgetStatusToString(exitStatus)
local wgetStatus,systemStatus = splitExitStatus(exitStatus)
@ -96,6 +138,9 @@ local function wgetStatusToString(exitStatus)
end
end
--- Creates the updater cache directory.
-- @return bool|nil True, or nil on error.
-- @return ?string A message in case of error.
local function createCacheDirectory()
if os.execute('mkdir -p ' .. M.CACHE_PATH) ~= 0 then
return nil,"Error: could not create cache directory '" .. M.CACHE_PATH .. "'"
@ -103,6 +148,9 @@ local function createCacheDirectory()
return true
end
--- Retrieves the current updater state code and message from @{STATE_FILE}.
-- @treturn STATE The current state code (@{STATE}.NONE if no state has been set).
-- @treturn string The current state message (empty string if no state has been set).
local function getState()
local file,msg = io.open(M.CACHE_PATH .. '/' .. M.STATE_FILE, 'r')
if not file then return M.STATE.NONE,"" end
@ -113,13 +161,23 @@ local function getState()
return code,msg
end
-- trim whitespace from both ends of string (from http://snippets.luacode.org/?p=snippets/trim_whitespace_from_string_76)
--- Trims whitespace from both ends of a string.
-- See [this Lua snippet](http://snippets.luacode.org/?p=snippets/trim_whitespace_from_string_76).
-- @string s The text to trim.
-- @treturn string s, with whitespace trimmed.
local function trim(s)
if type(s) ~= 'string' then return s end
return (s:find('^%s*$') and '' or s:match('^%s*(.*%S)'))
end
-- from utils.lua
--- Read the contents of a file.
--
-- TODO: this file has been copied from @{util.utils}.lua and should be merged again.
-- @string filePath The file to read.
-- @bool trimResult Whether or not to trim the read data.
-- @treturn bool|nil True, or nil on error.
-- @treturn ?string A descriptive message on error.
-- @treturn ?number TODO: find out why this value is returned.
local function readFile(filePath, trimResult)
local f, msg, nr = io.open(filePath, 'r')
if not f then return nil,msg,nr end
@ -134,7 +192,11 @@ local function readFile(filePath, trimResult)
return res
end
-- from utils.lua
--- Reports whether or not a file exists.
--
-- TODO: this file has been copied from @{util.utils}.lua and should be merged again.
-- @string file The file to report about.
-- @treturn bool True if the file exists, false otherwise.
local function exists(file)
if not file or type(file) ~= 'string' or file:len() == 0 then
return nil, "file must be a non-empty string"
@ -145,8 +207,11 @@ local function exists(file)
return r ~= nil
end
-- from utils.lua
--argument: either an open file or a filename
--- Reports the size of a file or file handle.
--
-- TODO: this file has been copied from @{util.utils}.lua and should be merged again.
-- @param file A file path or open file handle.
-- @treturn number Size of the file.
local function fileSize(file)
local size = nil
if type(file) == 'file' then
@ -164,18 +229,27 @@ local function fileSize(file)
return size
end
-- returns return value of command
--- Runs an arbitrary shell command.
-- @string command The command to run.
-- @bool dryRun Only log a message if true, otherwise run the command and log a message.
-- @treturn number Exit status of of command or -1 if dryRun is true.
local function runCommand(command, dryRun)
D("about to run: '" .. command .. "'")
return (not dryRun) and os.execute(command) or -1
end
--- Removes a file.
-- @string filePath The file to remove.
local function removeFile(filePath)
return runCommand('rm ' .. filePath)
end
-- returns return value of wget (or nil if saveDir is nil or empty), filename is optional
-- NOTE: leaving out filename will cause issues with files not being overwritten but suffixed with '.1', '.2',etc instead
--- Downloads a file and stores it locally.
-- @string url The full URL to download.
-- @string saveDir The path at which to save the downloaded file.
-- @string[opt] filename File name to save as, note that leaving this out has issues with files not being overwritten but suffixed with '.1', '.2',etc instead.
-- @treturn number|nil Exit status of wget command or nil on error.
-- @treturn ?string Descriptive message if saveDir is nil or empty.
local function downloadFile(url, saveDir, filename)
if not saveDir or saveDir:len() == 0 then return nil, "saveDir must be non-empty" end
local outArg = (filename:len() > 0) and (' -O' .. filename) or ''
@ -186,6 +260,10 @@ local function downloadFile(url, saveDir, filename)
end
end
--- Parses command-line arguments and returns a table containing information distilled from them.
-- @tab arglist A table in the same form as the [arg table](http://www.lua.org/pil/1.4.html) created by Lua.
-- @treturn tabla|nil A table containing information on what to do, or nil if invalid arguments were specified.
-- @treturn ?string Descriptive message on error.
local function parseCommandlineArguments(arglist)
local result = { verbosity = 0, baseUrl = M.DEFAULT_BASE_URL, action = nil }
local nextIsVersion, nextIsUrl = false, false
@ -233,18 +311,35 @@ end
-- MODULE FUNCTIONS --
----------------------
--- Enables use of the given @{util.logger} object, otherwise `stdout`/`stderr` will be used.
-- @tparam util.logger logger The logger to log future messages to.
function M.setLogger(logger)
log = logger
end
--- Controls whether or not to use pre-existing files over (re-)downloading them.
--
-- Note that the mechanism is currently naive, (e.g., there are no mechanisms like maximum cache age).
-- @bool use If true, try not to download anything unless necessary.
function M.setUseCache(use)
useCache = use
end
--- Sets the base URL to use for finding update images, defaults to @{DEFAULT_BASE_URL}.
-- @string url The new base URL to use.
function M.setBaseUrl(url)
baseUrl = url
end
--- Returns a table with information about current update status of the wifibox.
--
-- The result table will contain at least the current version, current state code and text.
-- If the box has internet access, it will also include the newest version available.
-- If an image is currently being downloaded, progress information will also be included.
--
-- @treturn bool True if status has been determined fully, false if not.
-- @treturn table The result table.
-- @treturn ?string Descriptive message in case the result table is not complete.
function M.getStatus()
if not baseUrl then baseUrl = M.DEFAULT_BASE_URL end
local unknownVersion = { major = 0, minor = 0, patch = 0 }
@ -253,7 +348,7 @@ function M.getStatus()
result.currentVersion = M.getCurrentVersion()
result.stateCode, result.stateText = getState()
result.stateCode = tonumber(result.stateCode)
local verTable,msg = M.getAvailableVersions()
if not verTable then
D("could not obtain available versions (" .. msg .. ")")
@ -263,7 +358,7 @@ function M.getStatus()
local newest = verTable and verTable[#verTable]
result.newestVersion = newest and newest.version or unknownVersion
if result.stateCode == M.STATE.DOWNLOADING then
result.progress = fileSize(M.CACHE_PATH .. '/' .. newest.sysupgradeFilename)
if not result.progress then result.progress = 0 end -- in case the file does not exist yet (which yields nil)
@ -525,6 +620,10 @@ end
-- MAIN --
----------
--- The main function which will be called in standalone mode.
-- At the end of the file, this function will be invoked only if `arg` is defined,
-- so this file can also be used as a library.
-- Command-line arguments are expected to be present in the global `arg` variable.
local function main()
local argTable,msg = parseCommandlineArguments(arg)
@ -662,7 +761,7 @@ local function main()
os.exit(0)
end
-- only execute the main function if an arg table is present, this enables usage both as module and as standalone script
--- Only execute the main function if an arg table is present, this enables usage both as module and as standalone script.
if arg ~= nil then main() end
return M

14
src/util/logger.lua

@ -1,8 +1,5 @@
--[[--
TODO: ldoc: @{} ref in init() tformat
TODO: use macros/type definitions to document rest modules (to auto-match things like 'M.<func>_NAME%')?
TODO: finish documentation
]]
---
-- Logging facilities.
local utils = require('util.utils')
@ -10,7 +7,8 @@ local M = {}
local logLevel, logVerbose, logStream
--- Available log levels.
--- Available log levels
-- @table LEVEL
M.LEVEL = {
'debug', -- for debug messages
'info', -- for informational messages
@ -34,10 +32,10 @@ local function log(level, msg, verbose)
local name = i.name or "(nil)"
local vVal = 'nil'
local m = (type(msg) == 'string') and msg or utils.dump(msg)
if v then logStream:write(now .. " (" .. M.LEVEL[level] .. ") " .. m .. " [" .. name .. "@" .. i.short_src .. ":" .. i.linedefined .. "]\n")
else logStream:write(now .. " (" .. M.LEVEL[level] .. ") " .. m .. "\n") end
logStream:flush()
end
end

104
src/util/settings.lua

@ -1,15 +1,9 @@
--[[--
The settings interface reads and writes configuration keys using UCI.
All keys have pre-defined defaults in @{conf_defaults} which will be used
if no value is stored in the UCI config. The UCI config file is
'/etc/config/wifibox'.
The default values guarantee there will always be a set of reasonable settings
to use and provide a clear overview of all existing configuration keys as well.
By the way, returning correct values in get()/fromUciValue() for booleans has
been fixed at a relatively convenient time purely thanks to the unit tests...
just to indicate how useful they are. :)
]]
---
-- The settings interface reads and writes configuration keys using [UCI](http://wiki.openwrt.org/doc/uci).
-- All keys have pre-defined defaults in @{conf_defaults} which will be used
-- if no value is stored in the UCI config. The UCI config file is `/etc/config/wifibox`.
-- The default values guarantee there will always be a set of reasonable settings
-- to use and provide a clear overview of all existing configuration keys as well.
local uci = require('uci').cursor()
local utils = require('util.utils')
local baseconfig = require('conf_defaults')
@ -18,26 +12,26 @@ local log = require('util.logger')
local M = {}
--- UCI config name (i.e. file under /etc/config)
--- UCI config name (i.e., file under `/etc/config`)
local UCI_CONFIG_NAME = 'wifibox'
--- Absolute path to the UCI config file
local UCI_CONFIG_FILE = '/etc/config/' .. UCI_CONFIG_NAME
--- Section type that will be used in UCI\_CONFIG\_FILE
--- [Section type](http://wiki.openwrt.org/doc/techref/uci#about.uci.structure) that will be used in @{UCI_CONFIG_FILE}
local UCI_CONFIG_TYPE = 'settings'
--- Section name that will be used for 'public' settings (as predefined in conf_defaults.lua) in UCI\_CONFIG\_FILE
--- [Section name](http://wiki.openwrt.org/doc/techref/uci#about.uci.structure) that will be used for 'public' settings (as predefined in conf_defaults.lua) in @{UCI_CONFIG_FILE}
local UCI_CONFIG_SECTION = 'general'
--- Section name that will be used for 'firmware-local' settings in UCI\_CONFIG\_FILE
--- [Section name](http://wiki.openwrt.org/doc/techref/uci#about.uci.structure) that will be used for 'firmware-local' settings in @{UCI_CONFIG_FILE}
local UCI_CONFIG_SYSTEM_SECTION = 'system'
local ERR_NO_SUCH_KEY = "key does not exist"
--- Returns a key with all periods ('.') replaced by underscores ('_').
-- @tparam string key The key for which to substitute dots.
-- @string key The key for which to substitute dots.
-- @return The substituted key, or the key parameter itself if it is not of type 'string'.
local function replaceDots(key)
if type(key) ~= 'string' then return key end
@ -46,7 +40,7 @@ local function replaceDots(key)
end
--- Returns a key with all underscores ('_') replaced by periods ('.').
-- @tparam string key The key for which to substitute underscores.
-- @string key The key for which to substitute underscores.
-- @return The substituted key, or the key parameter itself if it is not of type 'string'.
local function replaceUnderscores(key)
if type(key) ~= 'string' then return key end
@ -57,13 +51,13 @@ end
--- Converts a lua value to equivalent representation for UCI.
-- Boolean values are converted to '1' and '0', everything else is converted to a string.
--
-- @param v The value to convert.
-- @param vType The type of the given value.
-- @p v The value to convert.
-- @p vType The type of the given value.
-- @return A value usable to write to UCI.
local function toUciValue(v, vType)
if vType == 'bool' then return v and '1' or '0' end
if(vType == 'string') then
v = v:gsub('[\n\r]', '\\n')
if(vType == 'string') then
v = v:gsub('[\n\r]', '\\n')
end
return tostring(v)
@ -73,33 +67,33 @@ end
-- For boolean, '1' is converted to true and everything else to false. Floats
-- and ints are converted to numbers and everything else will be returned as is.
--
-- @param v The value to convert.
-- @param vType The type of the given value.
-- @p v The value to convert.
-- @p vType The type of the given value.
-- @return A lua value typed correctly with regard to the vType parameter.
local function fromUciValue(v, vType)
if v == nil then return nil end
if vType == 'bool' then
return (v == '1') and true or false
elseif vType == 'float' or vType == 'int' then
return tonumber(v)
elseif vType == 'string' then
v = v:gsub('\\n', '\n')
v = v:gsub('\\n', '\n')
return v
else
return v
end
end
--- Reports whether a value is valid given the constraints specified in a base table.
-- @param value The value to test.
-- @tparam table baseTable The base table to use constraint data from (min,max,regex).
-- @p value The value to test.
-- @tab baseTable The base table to use constraint data from (min,max,regex).
-- @treturn bool Returns true if the value is valid, false if it is not.
local function isValid(value, baseTable)
local varType, min, max, regex, isValid = baseTable.type, baseTable.min, baseTable.max, baseTable.regex, baseTable.isValid
if isValid then
if isValid then
local ok,msg = isValid(value)
if msg == nil then msg = "invalid value" end
return ok or nil,msg
@ -107,7 +101,7 @@ local function isValid(value, baseTable)
if varType == 'bool' then
return type(value) == 'boolean' or nil,"invalid bool value"
elseif varType == 'int' or varType == 'float' then
local numValue = tonumber(value)
if numValue == nil then
@ -119,7 +113,7 @@ local function isValid(value, baseTable)
elseif max and numValue > max then
return nil, "too high"
end
elseif varType == 'string' then
local ok = true
if min and value:len() < min then
@ -128,14 +122,14 @@ local function isValid(value, baseTable)
return nil,"too long"
elseif regex and value:match(regex) == nil then
return nil,"invalid value"
end
end
end
return true
end
--- Looks up the table in conf_defaults.lua corresponding to a key.
-- @tparam string key The key for which to return the base table.
--- Looks up the table in @{conf_defaults}.lua corresponding to a key.
-- @string key The key for which to return the base table.
-- @treturn table The base table for key, or nil if it does not exist.
local function getBaseKeyTable(key)
local base = baseconfig[key]
@ -144,20 +138,20 @@ end
--- Returns the value of the requested key if it exists.
-- @param key The key to return the associated value for.
-- @p key The key to return the associated value for.
-- @return The associated value, beware (!) that this may be boolean false for keys of 'bool' type.
function M.get(key)
key = replaceDots(key)
local base = getBaseKeyTable(key)
if not base then return nil,ERR_NO_SUCH_KEY end
local v = base.default
local uciV = fromUciValue(uci:get(UCI_CONFIG_NAME, UCI_CONFIG_SECTION, key), base.type)
local actualV = v
if uciV ~= nil then actualV = uciV end
return actualV
end
@ -175,7 +169,7 @@ function M.getAll()
end
--- Reports whether or not a key exists.
-- @tparam string key The key to find.
-- @string key The key to find.
-- @treturn bool True if the key exists, false if not.
function M.exists(key)
key = replaceDots(key)
@ -187,7 +181,7 @@ end
-- if for instance, the default is 'abc', and UCI contains a configured value of
-- 'abc' as well, that key is _not_ a default value.
--
-- @tparam string key The key to report about.
-- @string key The key to report about.
-- @treturn bool True if the key is currently at its default value, false if not.
function M.isDefault(key)
key = replaceDots(key)
@ -196,8 +190,8 @@ function M.isDefault(key)
end
--- Sets a key to a new value or reverts it to the default value.
-- @tparam string key The key to set.
-- @param value The value or set, or nil to revert key to its default value.
-- @string key The key to set.
-- @p[opt=nil] value The value or set, or nil to revert key to its default value.
-- @treturn bool|nil True if everything went well, nil in case of error.
-- @treturn ?string Error message in case first return value is nil (invalid key).
function M.set(key, value)
@ -206,10 +200,10 @@ function M.set(key, value)
local r = utils.create(UCI_CONFIG_FILE)
uci:set(UCI_CONFIG_NAME, UCI_CONFIG_SECTION, UCI_CONFIG_TYPE)
local base = getBaseKeyTable(key)
if not base then return nil,ERR_NO_SUCH_KEY end
if M.isDefault(key) and value == nil then return true end -- key is default already
--log:info(" not default")
local current = uci:get(UCI_CONFIG_NAME, UCI_CONFIG_SECTION, key)
@ -222,7 +216,7 @@ function M.set(key, value)
end
elseif base.type == 'int' or base.type == 'float' then
value = tonumber(value)
if(value == nil) then
if(value == nil) then
return nil,"Value isn't a valid int or float"
end
end
@ -232,7 +226,7 @@ function M.set(key, value)
if not valid then
return nil,m
end
if fromUciValue(current, base.type) == value then return true end
if value ~= nil then
@ -240,13 +234,13 @@ function M.set(key, value)
else
uci:delete(UCI_CONFIG_NAME, UCI_CONFIG_SECTION, key)
end
uci:commit(UCI_CONFIG_NAME)
return true
end
--- Returns a UCI configuration key from the system section.
-- @tparam string key The key for which to return the value, must be non-empty.
-- @string key The key for which to return the value, must be non-empty.
-- @return Requested value or false if it does not exist or nil on invalid key.
function M.getSystemKey(key)
if type(key) ~= 'string' or key:len() == 0 then return nil end
@ -254,21 +248,21 @@ function M.getSystemKey(key)
return v or false
end
--- Sets the given key to the given value.
--- Sets the value of a UCI key in the system section.
-- Note that unlike the public settings, system keys are untyped and value must
-- be of type string; UCI generally uses '1' and '0' for boolean values.
-- @tparam string key The key to set, must be non-empty.
-- @tparam string value The value to set key to.
-- @return True on success or false if key or value arguments are invalid.
-- @string key The key to set, must be non-empty.
-- @string value The value to set key to.
-- @return True on success or nil if key or value arguments are invalid.
function M.setSystemKey(key, value)
if type(key) ~= 'string' or key:len() == 0 then return nil end
if type(value) ~= 'string' then return nil end
local r = utils.create(UCI_CONFIG_FILE) -- make sure the file exists for uci to write to
uci:set(UCI_CONFIG_NAME, UCI_CONFIG_SYSTEM_SECTION, UCI_CONFIG_TYPE)
uci:set(UCI_CONFIG_NAME, UCI_CONFIG_SYSTEM_SECTION, key, value)
uci:commit(UCI_CONFIG_NAME)
return true
end

21
src/util/utils.lua

@ -1,7 +1,6 @@
--[[--
TODO: finish documentation
The unavoidable collection of utility functions.
]]
---
-- The unavoidable collection of utility functions.
-- TODO: use macros/type definitions to document rest modules (to auto-match things like 'M.<func>_NAME%')?
local M = {}
@ -25,7 +24,7 @@ end
function M.toboolean(s)
if not s then return false end
local b = type(s) == 'string' and s:lower() or s
local textTrue = (b == '1' or b == 't' or b == 'true')
local boolTrue = (type(b) == 'boolean' and b == true)
@ -63,7 +62,7 @@ function M.exists(file)
if not file or type(file) ~= 'string' or file:len() == 0 then
return nil, "file must be a non-empty string"
end
local r = io.open(file, 'r') -- ignore returned message
if r then r:close() end
return r ~= nil
@ -72,16 +71,16 @@ end
--creates and returns true if not exists, returns false it does, nil+msg on error
function M.create(file)
local r,m = M.exists(file)
if r == nil then
return r,m
elseif r == true then
return true
end
r,m = io.open(file, 'a') -- append mode is probably safer in case the file does exist after all
if not r then return r,m end
r:close()
return true
end
@ -96,10 +95,10 @@ end
function M.readFile(filePath)
local f, msg, nr = io.open(filePath, 'r')
if not f then return nil,msg,nr end
local res = f:read('*all')
f:close()
return res
end

Loading…
Cancel
Save