mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-04-07 01:15:44 +02:00
Add isMissing to Plugin model, add manifest version and name validation, create/update plugins table
This commit is contained in:
parent
5a96d8aeb3
commit
cfe3deff3b
@ -152,6 +152,11 @@ class Database {
|
|||||||
return this.models.device
|
return this.models.device
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @type {typeof import('./models/Plugin')} */
|
||||||
|
get pluginModel() {
|
||||||
|
return this.models.plugin
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if db file exists
|
* Check if db file exists
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
|
@ -2,8 +2,8 @@ const Path = require('path')
|
|||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
const Database = require('../Database')
|
const Database = require('../Database')
|
||||||
const PluginAbstract = require('../PluginAbstract')
|
const PluginAbstract = require('../PluginAbstract')
|
||||||
const fs = require('fs').promises
|
|
||||||
const fsExtra = require('../libs/fsExtra')
|
const fsExtra = require('../libs/fsExtra')
|
||||||
|
const { isUUID, parseSemverStrict } = require('../utils')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef PluginContext
|
* @typedef PluginContext
|
||||||
@ -35,11 +35,14 @@ class PluginManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Validate and load a plugin from a directory
|
||||||
|
* TODO: Validatation
|
||||||
*
|
*
|
||||||
|
* @param {string} dirname
|
||||||
* @param {string} pluginPath
|
* @param {string} pluginPath
|
||||||
* @returns {Promise<{manifest: Object, contents: PluginAbstract}>}
|
* @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()))
|
const pluginFiles = await fsExtra.readdir(pluginPath, { withFileTypes: true }).then((files) => files.filter((file) => !file.isDirectory()))
|
||||||
|
|
||||||
if (!pluginFiles.length) {
|
if (!pluginFiles.length) {
|
||||||
@ -66,6 +69,19 @@ class PluginManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Validate manifest json
|
// 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
|
let pluginInstance = null
|
||||||
try {
|
try {
|
||||||
@ -86,21 +102,74 @@ class PluginManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadPlugins() {
|
/**
|
||||||
|
* Get all plugins from the /metadata/plugins directory
|
||||||
|
*/
|
||||||
|
async getPluginsFromFileSystem() {
|
||||||
await fsExtra.ensureDir(this.pluginMetadataPath)
|
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 pluginDirs = await fsExtra.readdir(this.pluginMetadataPath, { withFileTypes: true, recursive: true }).then((files) => files.filter((file) => file.isDirectory()))
|
||||||
|
|
||||||
|
const pluginsFound = []
|
||||||
for (const pluginDir of pluginDirs) {
|
for (const pluginDir of pluginDirs) {
|
||||||
Logger.info(`[PluginManager] Loading plugin ${pluginDir.name}`)
|
Logger.debug(`[PluginManager] Checking if directory "${pluginDir.name}" is a plugin`)
|
||||||
const plugin = await this.loadPlugin(Path.join(this.pluginMetadataPath, pluginDir.name))
|
const plugin = await this.loadPlugin(pluginDir.name, Path.join(this.pluginMetadataPath, pluginDir.name))
|
||||||
if (plugin) {
|
if (plugin) {
|
||||||
Logger.info(`[PluginManager] Loaded plugin ${plugin.manifest.name}`)
|
Logger.debug(`[PluginManager] Found plugin "${plugin.manifest.name}"`)
|
||||||
this.plugins.push(plugin)
|
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() {
|
async init() {
|
||||||
await this.loadPlugins()
|
await this.loadPlugins()
|
||||||
|
|
||||||
|
@ -31,6 +31,7 @@ async function up({ context: { queryInterface, logger } }) {
|
|||||||
},
|
},
|
||||||
name: DataTypes.STRING,
|
name: DataTypes.STRING,
|
||||||
version: DataTypes.STRING,
|
version: DataTypes.STRING,
|
||||||
|
isMissing: DataTypes.BOOLEAN,
|
||||||
config: DataTypes.JSON,
|
config: DataTypes.JSON,
|
||||||
extraData: DataTypes.JSON,
|
extraData: DataTypes.JSON,
|
||||||
createdAt: DataTypes.DATE,
|
createdAt: DataTypes.DATE,
|
||||||
|
@ -10,6 +10,8 @@ class Plugin extends Model {
|
|||||||
this.name
|
this.name
|
||||||
/** @type {string} */
|
/** @type {string} */
|
||||||
this.version
|
this.version
|
||||||
|
/** @type {boolean} */
|
||||||
|
this.isMissing
|
||||||
/** @type {Object} */
|
/** @type {Object} */
|
||||||
this.config
|
this.config
|
||||||
/** @type {Object} */
|
/** @type {Object} */
|
||||||
@ -34,6 +36,10 @@ class Plugin extends Model {
|
|||||||
},
|
},
|
||||||
name: DataTypes.STRING,
|
name: DataTypes.STRING,
|
||||||
version: DataTypes.STRING,
|
version: DataTypes.STRING,
|
||||||
|
isMissing: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
defaultValue: false
|
||||||
|
},
|
||||||
config: DataTypes.JSON,
|
config: DataTypes.JSON,
|
||||||
extraData: DataTypes.JSON
|
extraData: DataTypes.JSON
|
||||||
},
|
},
|
||||||
|
@ -243,3 +243,21 @@ module.exports.isValidASIN = (str) => {
|
|||||||
if (!str || typeof str !== 'string') return false
|
if (!str || typeof str !== 'string') return false
|
||||||
return /^[A-Z0-9]{10}$/.test(str)
|
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 }
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user