mirror of
https://github.com/koreader/koreader.git
synced 2025-12-13 20:36:53 +01:00
add provider module (#12641)
* implements a Provider singleton, to be used by thirdparty plugins * exporter: support for thirdparty providers * splits plugin loading into two steps: discovery and load 1. get a list of all candidate plugins to load for the different paths 2. sort providers before on the rest of them and try to load them
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
local lfs = require("libs/libkoreader-lfs")
|
||||
local logger = require("logger")
|
||||
local util = require("util")
|
||||
local _ = require("gettext")
|
||||
|
||||
local DEFAULT_PLUGIN_PATH = "plugins"
|
||||
@@ -22,6 +23,22 @@ local DEPRECATION_MESSAGES = {
|
||||
feature = _("The following features are unmaintained and will be removed soon:"),
|
||||
}
|
||||
|
||||
local function isProvider(name)
|
||||
return name:sub(1, 8) == "provider"
|
||||
end
|
||||
|
||||
local function sortProvidersFirst(v1, v2)
|
||||
if isProvider(v1.name) and isProvider(v2.name) then
|
||||
return v1.path < v2.path
|
||||
elseif isProvider(v1.name) then
|
||||
return true
|
||||
elseif isProvider(v2.name) then
|
||||
return false
|
||||
else
|
||||
return v1.path < v2.path
|
||||
end
|
||||
end
|
||||
|
||||
local function deprecationFmt(field)
|
||||
local s
|
||||
if type(field) == "table" then
|
||||
@@ -76,12 +93,17 @@ local PluginLoader = {
|
||||
all_plugins = nil,
|
||||
}
|
||||
|
||||
function PluginLoader:loadPlugins()
|
||||
if self.enabled_plugins then return self.enabled_plugins, self.disabled_plugins end
|
||||
function PluginLoader:_discover()
|
||||
local plugins_disabled = G_reader_settings:readSetting("plugins_disabled")
|
||||
if type(plugins_disabled) ~= "table" then
|
||||
plugins_disabled = {}
|
||||
end
|
||||
-- disable obsolete plugins
|
||||
for element in pairs(OBSOLETE_PLUGINS) do
|
||||
plugins_disabled[element] = true
|
||||
end
|
||||
|
||||
self.enabled_plugins = {}
|
||||
self.disabled_plugins = {}
|
||||
self.loaded_plugins = {}
|
||||
local discovered = {}
|
||||
local lookup_path_list = { DEFAULT_PLUGIN_PATH }
|
||||
local extra_paths = G_reader_settings:readSetting("extra_plugin_paths")
|
||||
if extra_paths then
|
||||
@@ -106,21 +128,8 @@ function PluginLoader:loadPlugins()
|
||||
table.insert(lookup_path_list, extra_path)
|
||||
end
|
||||
end
|
||||
|
||||
-- keep reference to old value so they can be restored later
|
||||
local package_path = package.path
|
||||
local package_cpath = package.cpath
|
||||
|
||||
local plugins_disabled = G_reader_settings:readSetting("plugins_disabled")
|
||||
if type(plugins_disabled) ~= "table" then
|
||||
plugins_disabled = {}
|
||||
end
|
||||
-- disable obsolete plugins
|
||||
for element in pairs(OBSOLETE_PLUGINS) do
|
||||
plugins_disabled[element] = true
|
||||
end
|
||||
for _, lookup_path in ipairs(lookup_path_list) do
|
||||
logger.info("Loading plugins from directory:", lookup_path)
|
||||
logger.info("Looking for plugins in directory:", lookup_path)
|
||||
for entry in lfs.dir(lookup_path) do
|
||||
local plugin_root = lookup_path.."/"..entry
|
||||
local mode = lfs.attributes(plugin_root, "mode")
|
||||
@@ -128,9 +137,37 @@ function PluginLoader:loadPlugins()
|
||||
if mode == "directory" and entry:find(".+%.koplugin$") then
|
||||
local mainfile = plugin_root.."/main.lua"
|
||||
local metafile = plugin_root.."/_meta.lua"
|
||||
local disabled = false
|
||||
if plugins_disabled and plugins_disabled[entry:sub(1, -10)] then
|
||||
mainfile = metafile
|
||||
disabled = true
|
||||
end
|
||||
local __, name = util.splitFilePathName(plugin_root)
|
||||
|
||||
table.insert(discovered, {
|
||||
["main"] = mainfile,
|
||||
["meta"] = metafile,
|
||||
["path"] = plugin_root,
|
||||
["disabled"] = disabled,
|
||||
["name"] = name,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
return discovered
|
||||
end
|
||||
|
||||
function PluginLoader:_load(t)
|
||||
-- keep reference to old value so they can be restored later
|
||||
local package_path = package.path
|
||||
local package_cpath = package.cpath
|
||||
|
||||
local mainfile, metafile, plugin_root, disabled
|
||||
for _, v in ipairs(t) do
|
||||
mainfile = v.main
|
||||
metafile = v.meta
|
||||
plugin_root = v.path
|
||||
disabled = v.disabled
|
||||
package.path = string.format("%s/?.lua;%s", plugin_root, package_path)
|
||||
package.cpath = string.format("%s/lib/?.so;%s", plugin_root, package_cpath)
|
||||
local ok, plugin_module = pcall(dofile, mainfile)
|
||||
@@ -139,25 +176,37 @@ function PluginLoader:loadPlugins()
|
||||
elseif type(plugin_module.disabled) ~= "boolean" or not plugin_module.disabled then
|
||||
plugin_module.path = plugin_root
|
||||
plugin_module.name = plugin_module.name or plugin_root:match("/(.-)%.koplugin")
|
||||
if (plugins_disabled and plugins_disabled[entry:sub(1, -10)]) then
|
||||
if disabled then
|
||||
table.insert(self.disabled_plugins, plugin_module)
|
||||
else
|
||||
local ok_meta, plugin_metamodule = pcall(dofile, metafile)
|
||||
if ok_meta and plugin_metamodule then
|
||||
for k,v in pairs(plugin_metamodule) do plugin_module[k] = v end
|
||||
for k, module in pairs(plugin_metamodule) do
|
||||
plugin_module[k] = module
|
||||
end
|
||||
end
|
||||
sandboxPluginEventHandlers(plugin_module)
|
||||
table.insert(self.enabled_plugins, plugin_module)
|
||||
logger.dbg("Plugin loaded", plugin_module.name)
|
||||
end
|
||||
end
|
||||
else
|
||||
logger.dbg("Plugin", mainfile, "has been disabled.")
|
||||
end
|
||||
package.path = package_path
|
||||
package.cpath = package_cpath
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
function PluginLoader:loadPlugins()
|
||||
if self.enabled_plugins then return self.enabled_plugins, self.disabled_plugins end
|
||||
|
||||
self.enabled_plugins = {}
|
||||
self.disabled_plugins = {}
|
||||
self.loaded_plugins = {}
|
||||
|
||||
local t = self:_discover()
|
||||
table.sort(t, sortProvidersFirst)
|
||||
self:_load(t)
|
||||
-- set package path for all loaded plugins
|
||||
for _, plugin in ipairs(self.enabled_plugins) do
|
||||
package.path = string.format("%s;%s/?.lua", package.path, plugin.path)
|
||||
|
||||
92
frontend/provider.lua
Normal file
92
frontend/provider.lua
Normal file
@@ -0,0 +1,92 @@
|
||||
--[[
|
||||
|
||||
Provider is a singleton that holds thirdparty implementations for features.
|
||||
|
||||
To be used on plugins, prefixed with "provider-", that implement specific feature APIs.
|
||||
|
||||
]]--
|
||||
|
||||
local util = require("util")
|
||||
|
||||
local function aTable(t)
|
||||
if type(t) ~= "table" then
|
||||
return {}
|
||||
end
|
||||
return t
|
||||
end
|
||||
|
||||
local Provider = {
|
||||
features = {
|
||||
["exporter"] = {},
|
||||
},
|
||||
}
|
||||
|
||||
function Provider:_isValidFeature(s)
|
||||
return self.features[s] ~= nil
|
||||
end
|
||||
|
||||
--[[--
|
||||
Registers an implementation of a feature.
|
||||
|
||||
@param feature string that identifies the feature
|
||||
@param name string that identifies the provider
|
||||
@param impl table with implementation details
|
||||
@treturn bool registered
|
||||
]]
|
||||
function Provider:register(feature, name, impl)
|
||||
if type(name) ~= "string" or type(feature) ~= "string" or type(impl) ~= "table" then
|
||||
return false
|
||||
end
|
||||
if self:_isValidFeature(feature) then
|
||||
self.features[feature][name] = impl
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
--[[--
|
||||
Unregisters an implementation of a feature.
|
||||
|
||||
@param feature string feature identifier
|
||||
@param name string provider identifier
|
||||
@treturn bool unregistered
|
||||
]]
|
||||
function Provider:unregister(feature, name)
|
||||
if type(name) ~= "string" or type(feature) ~= "string" then
|
||||
return false
|
||||
end
|
||||
if self:_isValidFeature(feature) then
|
||||
self.features[feature][name] = nil
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
--[[--
|
||||
Counts providers for a given feature
|
||||
|
||||
@param feature string feature identifier
|
||||
@treturn int number
|
||||
]]
|
||||
function Provider:size(feature)
|
||||
local count = 0
|
||||
if self:_isValidFeature(feature) then
|
||||
count = util.tableSize(aTable(self.features[feature]))
|
||||
end
|
||||
return count
|
||||
end
|
||||
|
||||
--[[--
|
||||
Get providers for a given feature
|
||||
|
||||
@param feature string feature identifier
|
||||
@treturn table provider/implementation k/v pairs.
|
||||
]]
|
||||
function Provider:getProvidersTable(feature)
|
||||
if self:_isValidFeature(feature) then
|
||||
return aTable(self.features[feature])
|
||||
end
|
||||
return aTable()
|
||||
end
|
||||
|
||||
return Provider
|
||||
@@ -31,6 +31,7 @@ local Dispatcher = require("dispatcher")
|
||||
local InfoMessage = require("ui/widget/infomessage")
|
||||
local MyClipping = require("clip")
|
||||
local NetworkMgr = require("ui/network/manager")
|
||||
local Provider = require("provider")
|
||||
local ReaderHighlight = require("apps/reader/modules/readerhighlight")
|
||||
local UIManager = require("ui/uimanager")
|
||||
local WidgetContainer = require("ui/widget/container/widgetcontainer")
|
||||
@@ -99,9 +100,7 @@ local function updateMyClippings(clippings, new_clippings)
|
||||
return clippings
|
||||
end
|
||||
|
||||
local Exporter = WidgetContainer:extend{
|
||||
name = "exporter",
|
||||
targets = {
|
||||
local targets = {
|
||||
html = require("target/html"),
|
||||
joplin = require("target/joplin"),
|
||||
json = require("target/json"),
|
||||
@@ -111,15 +110,33 @@ local Exporter = WidgetContainer:extend{
|
||||
readwise = require("target/readwise"),
|
||||
text = require("target/text"),
|
||||
xmnote = require("target/xmnote"),
|
||||
},
|
||||
}
|
||||
|
||||
local function genExportersTable(path)
|
||||
local t = {}
|
||||
for k, v in pairs(targets) do
|
||||
t[k] = v
|
||||
end
|
||||
if Provider:size("exporter") > 0 then
|
||||
local tbl = Provider:getProvidersTable("exporter")
|
||||
for k, v in pairs(tbl) do
|
||||
t[k] = v
|
||||
end
|
||||
end
|
||||
for _, v in pairs(t) do
|
||||
v.path = path
|
||||
end
|
||||
return t
|
||||
end
|
||||
|
||||
local Exporter = WidgetContainer:extend{
|
||||
name = "exporter",
|
||||
}
|
||||
|
||||
function Exporter:init()
|
||||
migrateSettings()
|
||||
self.parser = MyClipping:new{}
|
||||
for _, v in pairs(self.targets) do
|
||||
v.path = self.path
|
||||
end
|
||||
self.targets = genExportersTable(self.path)
|
||||
self.ui.menu:registerToMainMenu(self)
|
||||
self:onDispatcherRegisterActions()
|
||||
end
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
-- Set search path for `require()`.
|
||||
package.path =
|
||||
"common/?.lua;frontend/?.lua;" ..
|
||||
"common/?.lua;frontend/?.lua;plugins/exporter.koplugin/?.lua;" ..
|
||||
package.path
|
||||
package.cpath =
|
||||
"common/?.so;common/?.dll;/usr/lib/lua/?.so;" ..
|
||||
|
||||
41
spec/unit/provider_spec.lua
Normal file
41
spec/unit/provider_spec.lua
Normal file
@@ -0,0 +1,41 @@
|
||||
describe("Provider module", function()
|
||||
local Provider
|
||||
local t
|
||||
|
||||
setup(function()
|
||||
require("commonrequire")
|
||||
Provider = require("provider")
|
||||
end)
|
||||
|
||||
it("should fail to register an improper provider", function()
|
||||
assert.is_false(Provider:register())
|
||||
end)
|
||||
it("should fail to unregister an improper provider", function()
|
||||
assert.is_false(Provider:unregister())
|
||||
end)
|
||||
it("should register a proper provider with empty implementation", function()
|
||||
assert.is_true(Provider:register("exporter", "test", {}))
|
||||
end)
|
||||
it("should override an implementation for the same name of the same kind", function()
|
||||
assert.is_true(Provider:register("exporter", "test", { test = function() end }))
|
||||
assert.is_true(type(Provider.features["exporter"]["test"].test) == "function")
|
||||
end)
|
||||
it("should unregister a provider", function()
|
||||
assert.is_true(Provider:unregister("exporter", "test"))
|
||||
end)
|
||||
it("should count providers for a specific feature", function()
|
||||
assert.is_true(Provider:register("exporter", "test1", {}))
|
||||
assert.is_true(Provider:register("exporter", "test2", {}))
|
||||
assert.is_true(Provider:register("exporter", "test3", {}))
|
||||
assert.is_true(Provider:size("exporter") == 3)
|
||||
end)
|
||||
|
||||
it("should dump a table of providers for a specific feature", function()
|
||||
assert.are.same(Provider.features["exporter"],
|
||||
Provider:getProvidersTable("exporter"))
|
||||
end)
|
||||
it("should dump an empty table for an invalid feature", function()
|
||||
t = Provider:getProvidersTable("invalid")
|
||||
assert.is_true(type(t) == "table")
|
||||
end)
|
||||
end)
|
||||
Reference in New Issue
Block a user