Add isMissing to Plugin model, add manifest version and name validation, create/update plugins table

This commit is contained in:
advplyr 2024-12-21 13:26:42 -06:00
parent 5a96d8aeb3
commit cfe3deff3b
5 changed files with 106 additions and 7 deletions

View File

@ -152,6 +152,11 @@ class Database {
return this.models.device
}
/** @type {typeof import('./models/Plugin')} */
get pluginModel() {
return this.models.plugin
}
/**
* Check if db file exists
* @returns {boolean}

View File

@ -2,8 +2,8 @@ const Path = require('path')
const Logger = require('../Logger')
const Database = require('../Database')
const PluginAbstract = require('../PluginAbstract')
const fs = require('fs').promises
const fsExtra = require('../libs/fsExtra')
const { isUUID, parseSemverStrict } = require('../utils')
/**
* @typedef PluginContext
@ -35,11 +35,14 @@ class PluginManager {
}
/**
* Validate and load a plugin from a directory
* TODO: Validatation
*
* @param {string} dirname
* @param {string} pluginPath
* @returns {Promise<{manifest: Object, contents: PluginAbstract}>}
*/
async loadPlugin(pluginPath) {
async loadPlugin(dirname, pluginPath) {
const pluginFiles = await fsExtra.readdir(pluginPath, { withFileTypes: true }).then((files) => files.filter((file) => !file.isDirectory()))
if (!pluginFiles.length) {
@ -66,6 +69,19 @@ class PluginManager {
}
// TODO: Validate manifest json
if (!isUUID(manifestJson.id)) {
Logger.error(`Invalid plugin ID in manifest for plugin ${pluginPath}`)
return null
}
if (!parseSemverStrict(manifestJson.version)) {
Logger.error(`Invalid plugin version in manifest for plugin ${pluginPath}`)
return null
}
// TODO: Enforcing plugin name to be the same as the directory name? Ensures plugins are identifiable in the file system. May have issues with unicode characters.
if (dirname !== manifestJson.name) {
Logger.error(`Plugin directory name "${dirname}" does not match manifest name "${manifestJson.name}"`)
return null
}
let pluginInstance = null
try {
@ -86,21 +102,74 @@ class PluginManager {
}
}
async loadPlugins() {
/**
* Get all plugins from the /metadata/plugins directory
*/
async getPluginsFromFileSystem() {
await fsExtra.ensureDir(this.pluginMetadataPath)
// Get all directories in the plugins directory
const pluginDirs = await fsExtra.readdir(this.pluginMetadataPath, { withFileTypes: true, recursive: true }).then((files) => files.filter((file) => file.isDirectory()))
const pluginsFound = []
for (const pluginDir of pluginDirs) {
Logger.info(`[PluginManager] Loading plugin ${pluginDir.name}`)
const plugin = await this.loadPlugin(Path.join(this.pluginMetadataPath, pluginDir.name))
Logger.debug(`[PluginManager] Checking if directory "${pluginDir.name}" is a plugin`)
const plugin = await this.loadPlugin(pluginDir.name, Path.join(this.pluginMetadataPath, pluginDir.name))
if (plugin) {
Logger.info(`[PluginManager] Loaded plugin ${plugin.manifest.name}`)
this.plugins.push(plugin)
Logger.debug(`[PluginManager] Found plugin "${plugin.manifest.name}"`)
pluginsFound.push(plugin)
}
}
return pluginsFound
}
/**
* Load plugins from the /metadata/plugins directory and update the database
*/
async loadPlugins() {
const pluginsFound = await this.getPluginsFromFileSystem()
const existingPlugins = await Database.pluginModel.findAll()
// Add new plugins or update existing plugins
for (const plugin of pluginsFound) {
const existingPlugin = existingPlugins.find((p) => p.id === plugin.manifest.id)
if (existingPlugin) {
// TODO: Should automatically update?
if (existingPlugin.version !== plugin.manifest.version) {
Logger.info(`[PluginManager] Updating plugin "${plugin.manifest.name}" version from "${existingPlugin.version}" to version "${plugin.manifest.version}"`)
await existingPlugin.update({ version: plugin.manifest.version, isMissing: false })
} else if (existingPlugin.isMissing) {
Logger.info(`[PluginManager] Plugin "${plugin.manifest.name}" was missing but is now found`)
await existingPlugin.update({ isMissing: false })
} else {
Logger.debug(`[PluginManager] Plugin "${plugin.manifest.name}" already exists in the database with version "${plugin.manifest.version}"`)
}
} else {
await Database.pluginModel.create({
id: plugin.manifest.id,
name: plugin.manifest.name,
version: plugin.manifest.version
})
Logger.info(`[PluginManager] Added plugin "${plugin.manifest.name}" to the database`)
}
}
// Mark missing plugins
for (const plugin of existingPlugins) {
const foundPlugin = pluginsFound.find((p) => p.manifest.id === plugin.id)
if (!foundPlugin && !plugin.isMissing) {
Logger.info(`[PluginManager] Plugin "${plugin.name}" not found or invalid - marking as missing`)
await plugin.update({ isMissing: true })
}
}
this.plugins = pluginsFound
}
/**
* Load and initialize all plugins
*/
async init() {
await this.loadPlugins()

View File

@ -31,6 +31,7 @@ async function up({ context: { queryInterface, logger } }) {
},
name: DataTypes.STRING,
version: DataTypes.STRING,
isMissing: DataTypes.BOOLEAN,
config: DataTypes.JSON,
extraData: DataTypes.JSON,
createdAt: DataTypes.DATE,

View File

@ -10,6 +10,8 @@ class Plugin extends Model {
this.name
/** @type {string} */
this.version
/** @type {boolean} */
this.isMissing
/** @type {Object} */
this.config
/** @type {Object} */
@ -34,6 +36,10 @@ class Plugin extends Model {
},
name: DataTypes.STRING,
version: DataTypes.STRING,
isMissing: {
type: DataTypes.BOOLEAN,
defaultValue: false
},
config: DataTypes.JSON,
extraData: DataTypes.JSON
},

View File

@ -243,3 +243,21 @@ module.exports.isValidASIN = (str) => {
if (!str || typeof str !== 'string') return false
return /^[A-Z0-9]{10}$/.test(str)
}
/**
* Parse semver string that must be in format "major.minor.patch" all numbers
*
* @param {string} version
* @returns {{major: number, minor: number, patch: number} | null}
*/
module.exports.parseSemverStrict = (version) => {
if (typeof version !== 'string') {
return null
}
const [major, minor, patch] = version.split('.').map(Number)
if (isNaN(major) || isNaN(minor) || isNaN(patch)) {
return null
}
return { major, minor, patch }
}