2
0
mirror of https://github.com/koreader/koreader synced 2024-10-31 21:20:20 +00:00
koreader/frontend/luasettings.lua
poire-z 04d9a557aa Use fsync() for more robust setting files saving
Bump base for util.fsyncOpenedFile() and util.fsyncDirectory().

Use these to force flush to storage device when
saving luasetting, docsetting, and history.lua.
Also dont rotate them to .old until they are at least
60 seconds old.
Also make auto_save_paging_count count right.

Also bump crengine: open (as text) small or empty files
2019-12-10 23:57:32 +01:00

227 lines
5.8 KiB
Lua

--[[--
This module handles generic settings as well as KOReader's global settings system.
]]
local dump = require("dump")
local ffiutil = require("ffi/util")
local lfs = require("libs/libkoreader-lfs")
local logger = require("logger")
local LuaSettings = {}
function LuaSettings:new(o)
o = o or {}
setmetatable(o, self)
self.__index = self
return o
end
--- Opens a settings file.
function LuaSettings:open(file_path)
local new = {file=file_path}
local ok, stored
-- File being absent and returning an empty table is a use case,
-- so logger.warn() only if there was an existing file
local existing = lfs.attributes(new.file, "mode") == "file"
ok, stored = pcall(dofile, new.file)
if ok and stored then
new.data = stored
else
if existing then logger.warn("Failed reading", new.file, "(probably corrupted).") end
-- Fallback to .old if it exists
ok, stored = pcall(dofile, new.file..".old")
if ok and stored then
if existing then logger.warn("read from backup file", new.file..".old") end
new.data = stored
else
if existing then logger.warn("no usable backup file for", new.file, "to read from") end
new.data = {}
end
end
return setmetatable(new, {__index = LuaSettings})
end
--- @todo DocSettings can return a LuaSettings to use following awesome features.
function LuaSettings:wrap(data)
local new = {data = type(data) == "table" and data or {}}
return setmetatable(new, {__index = LuaSettings})
end
--[[--Reads child settings.
@usage
Settings:saveSetting("key", {
a = "b",
c = "true",
d = false,
})
local child = Settings:child("key")
child:readSetting("a")
-- result "b"
]]
function LuaSettings:child(key)
return LuaSettings:wrap(self:readSetting(key))
end
--- Reads a setting.
function LuaSettings:readSetting(key)
return self.data[key]
end
--- Saves a setting.
function LuaSettings:saveSetting(key, value)
self.data[key] = value
return self
end
--- Deletes a setting.
function LuaSettings:delSetting(key)
self.data[key] = nil
return self
end
--- Checks if setting exists.
function LuaSettings:has(key)
return self:readSetting(key) ~= nil
end
--- Checks if setting does not exist.
function LuaSettings:hasNot(key)
return self:readSetting(key) == nil
end
--- Checks if setting is `true`.
function LuaSettings:isTrue(key)
return string.lower(tostring(self:readSetting(key))) == "true"
end
--- Checks if setting is `false`.
function LuaSettings:isFalse(key)
return string.lower(tostring(self:readSetting(key))) == "false"
end
--- Checks if setting is `nil` or `true`.
function LuaSettings:nilOrTrue(key)
return self:hasNot(key) or self:isTrue(key)
end
--- Checks if setting is `nil` or `false`.
function LuaSettings:nilOrFalse(key)
return self:hasNot(key) or self:isFalse(key)
end
--- Flips `nil` or `true` to `false`.
function LuaSettings:flipNilOrTrue(key)
if self:nilOrTrue(key) then
self:saveSetting(key, false)
else
self:delSetting(key)
end
return self
end
--- Flips `nil` or `false` to `true`.
function LuaSettings:flipNilOrFalse(key)
if self:nilOrFalse(key) then
self:saveSetting(key, true)
else
self:delSetting(key)
end
return self
end
--- Flips setting to `true`.
function LuaSettings:flipTrue(key)
if self:isTrue(key) then
self:delSetting(key)
else
self:saveSetting(key, true)
end
return self
end
--- Flips setting to `false`.
function LuaSettings:flipFalse(key)
if self:isFalse(key) then
self:delSetting(key)
else
self:saveSetting(key, false)
end
return self
end
--- Adds item to table.
function LuaSettings:addTableItem(key, value)
local settings_table = self:has(key) and self:readSetting(key) or {}
table.insert(settings_table, value)
self:saveSetting(key, settings_table)
return self
end
--- Removes index from table.
function LuaSettings:removeTableItem(key, index)
local settings_table = self:has(key) and self:readSetting(key) or {}
table.remove(settings_table, index)
self:saveSetting(key, settings_table)
return self
end
--- Replaces existing settings with table.
function LuaSettings:reset(table)
self.data = table
return self
end
--- Writes settings to disk.
function LuaSettings:flush()
if not self.file then return end
local directory_updated = false
if lfs.attributes(self.file, "mode") == "file" then
-- As an additional safety measure (to the ffiutil.fsync* calls
-- used below), we only backup the file to .old when it has
-- not been modified in the last 60 seconds. This should ensure
-- in the case the fsync calls are not supported that the OS
-- may have itself sync'ed that file content in the meantime.
local mtime = lfs.attributes(self.file, "modification")
if mtime < os.time() - 60 then
os.rename(self.file, self.file .. ".old")
directory_updated = true -- fsync directory content too below
end
end
local f_out = io.open(self.file, "w")
if f_out ~= nil then
os.setlocale('C', 'numeric')
f_out:write("-- we can read Lua syntax here!\nreturn ")
f_out:write(dump(self.data))
f_out:write("\n")
ffiutil.fsyncOpenedFile(f_out) -- force flush to the storage device
f_out:close()
end
if directory_updated then
-- Ensure the file renaming is flushed to storage device
ffiutil.fsyncDirectory(self.file)
end
return self
end
--- Closes settings file.
function LuaSettings:close()
self:flush()
end
--- Purges settings file.
function LuaSettings:purge()
if self.file then
os.remove(self.file)
end
return self
end
return LuaSettings