Some checks failed
Pipeline: Test, Lint, Build / Get version info (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test JS code (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint i18n files (push) Has been cancelled
Pipeline: Test, Lint, Build / Check Docker configuration (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (darwin/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (darwin/arm64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/386) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v5) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v6) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v7) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (windows/386) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (windows/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to GHCR (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to Docker Hub (push) Has been cancelled
Pipeline: Test, Lint, Build / Cleanup digest artifacts (push) Has been cancelled
Pipeline: Test, Lint, Build / Build Windows installers (push) Has been cancelled
Pipeline: Test, Lint, Build / Package/Release (push) Has been cancelled
Pipeline: Test, Lint, Build / Upload Linux PKG (push) Has been cancelled
Close stale issues and PRs / stale (push) Has been cancelled
POEditor import / update-translations (push) Has been cancelled
146 lines
3.8 KiB
Go
146 lines
3.8 KiB
Go
package plugins
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"github.com/navidrome/navidrome/plugins/schema"
|
|
)
|
|
|
|
// PluginDiscoveryEntry represents the result of plugin discovery
|
|
type PluginDiscoveryEntry struct {
|
|
ID string // Plugin ID (directory name)
|
|
Path string // Resolved plugin directory path
|
|
WasmPath string // Path to the WASM file
|
|
Manifest *schema.PluginManifest // Loaded manifest (nil if failed)
|
|
IsSymlink bool // Whether the plugin is a development symlink
|
|
Error error // Error encountered during discovery
|
|
}
|
|
|
|
// DiscoverPlugins scans the plugins directory and returns information about all discoverable plugins
|
|
// This shared function eliminates duplication between ScanPlugins and plugin list commands
|
|
func DiscoverPlugins(pluginsDir string) []PluginDiscoveryEntry {
|
|
var discoveries []PluginDiscoveryEntry
|
|
|
|
entries, err := os.ReadDir(pluginsDir)
|
|
if err != nil {
|
|
// Return a single entry with the error
|
|
return []PluginDiscoveryEntry{{
|
|
Error: fmt.Errorf("failed to read plugins directory %s: %w", pluginsDir, err),
|
|
}}
|
|
}
|
|
|
|
for _, entry := range entries {
|
|
name := entry.Name()
|
|
pluginPath := filepath.Join(pluginsDir, name)
|
|
|
|
// Skip hidden files
|
|
if name[0] == '.' {
|
|
continue
|
|
}
|
|
|
|
// Check if it's a directory or symlink
|
|
info, err := os.Lstat(pluginPath)
|
|
if err != nil {
|
|
discoveries = append(discoveries, PluginDiscoveryEntry{
|
|
ID: name,
|
|
Error: fmt.Errorf("failed to stat entry %s: %w", pluginPath, err),
|
|
})
|
|
continue
|
|
}
|
|
|
|
isSymlink := info.Mode()&os.ModeSymlink != 0
|
|
isDir := info.IsDir()
|
|
|
|
// Skip if not a directory or symlink
|
|
if !isDir && !isSymlink {
|
|
continue
|
|
}
|
|
|
|
// Resolve symlinks
|
|
pluginDir := pluginPath
|
|
if isSymlink {
|
|
targetDir, err := os.Readlink(pluginPath)
|
|
if err != nil {
|
|
discoveries = append(discoveries, PluginDiscoveryEntry{
|
|
ID: name,
|
|
IsSymlink: true,
|
|
Error: fmt.Errorf("failed to resolve symlink %s: %w", pluginPath, err),
|
|
})
|
|
continue
|
|
}
|
|
|
|
// If target is a relative path, make it absolute
|
|
if !filepath.IsAbs(targetDir) {
|
|
targetDir = filepath.Join(filepath.Dir(pluginPath), targetDir)
|
|
}
|
|
|
|
// Verify that the target is a directory
|
|
targetInfo, err := os.Stat(targetDir)
|
|
if err != nil {
|
|
discoveries = append(discoveries, PluginDiscoveryEntry{
|
|
ID: name,
|
|
IsSymlink: true,
|
|
Error: fmt.Errorf("failed to stat symlink target %s: %w", targetDir, err),
|
|
})
|
|
continue
|
|
}
|
|
|
|
if !targetInfo.IsDir() {
|
|
discoveries = append(discoveries, PluginDiscoveryEntry{
|
|
ID: name,
|
|
IsSymlink: true,
|
|
Error: fmt.Errorf("symlink target is not a directory: %s", targetDir),
|
|
})
|
|
continue
|
|
}
|
|
|
|
pluginDir = targetDir
|
|
}
|
|
|
|
// Check for WASM file
|
|
wasmPath := filepath.Join(pluginDir, "plugin.wasm")
|
|
if _, err := os.Stat(wasmPath); err != nil {
|
|
discoveries = append(discoveries, PluginDiscoveryEntry{
|
|
ID: name,
|
|
Path: pluginDir,
|
|
Error: fmt.Errorf("no plugin.wasm found: %w", err),
|
|
})
|
|
continue
|
|
}
|
|
|
|
// Load manifest
|
|
manifest, err := LoadManifest(pluginDir)
|
|
if err != nil {
|
|
discoveries = append(discoveries, PluginDiscoveryEntry{
|
|
ID: name,
|
|
Path: pluginDir,
|
|
Error: fmt.Errorf("failed to load manifest: %w", err),
|
|
})
|
|
continue
|
|
}
|
|
|
|
// Check for capabilities
|
|
if len(manifest.Capabilities) == 0 {
|
|
discoveries = append(discoveries, PluginDiscoveryEntry{
|
|
ID: name,
|
|
Path: pluginDir,
|
|
Error: fmt.Errorf("no capabilities found in manifest"),
|
|
})
|
|
continue
|
|
}
|
|
|
|
// Success!
|
|
discoveries = append(discoveries, PluginDiscoveryEntry{
|
|
ID: name,
|
|
Path: pluginDir,
|
|
WasmPath: wasmPath,
|
|
Manifest: manifest,
|
|
IsSymlink: isSymlink,
|
|
})
|
|
}
|
|
|
|
return discoveries
|
|
}
|