mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2025-12-13 00:42:49 -05:00
switch hto monorepo structure
This commit is contained in:
430
backend/internal/plugins/manager.go
Normal file
430
backend/internal/plugins/manager.go
Normal file
@@ -0,0 +1,430 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
type Manager struct {
|
||||
fs afero.Fs
|
||||
pluginsDir string
|
||||
gitClient GitClient
|
||||
}
|
||||
|
||||
func NewManager() (*Manager, error) {
|
||||
return NewManagerWithFs(afero.NewOsFs())
|
||||
}
|
||||
|
||||
func NewManagerWithFs(fs afero.Fs) (*Manager, error) {
|
||||
pluginsDir := getPluginsDir()
|
||||
return &Manager{
|
||||
fs: fs,
|
||||
pluginsDir: pluginsDir,
|
||||
gitClient: &realGitClient{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getPluginsDir() string {
|
||||
configHome := os.Getenv("XDG_CONFIG_HOME")
|
||||
if configHome == "" {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return filepath.Join(os.TempDir(), "DankMaterialShell", "plugins")
|
||||
}
|
||||
configHome = filepath.Join(homeDir, ".config")
|
||||
}
|
||||
return filepath.Join(configHome, "DankMaterialShell", "plugins")
|
||||
}
|
||||
|
||||
func (m *Manager) IsInstalled(plugin Plugin) (bool, error) {
|
||||
pluginPath := filepath.Join(m.pluginsDir, plugin.ID)
|
||||
exists, err := afero.DirExists(m.fs, pluginPath)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if exists {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
systemPluginPath := filepath.Join("/etc/xdg/quickshell/dms-plugins", plugin.ID)
|
||||
systemExists, err := afero.DirExists(m.fs, systemPluginPath)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return systemExists, nil
|
||||
}
|
||||
|
||||
func (m *Manager) Install(plugin Plugin) error {
|
||||
pluginPath := filepath.Join(m.pluginsDir, plugin.ID)
|
||||
|
||||
exists, err := afero.DirExists(m.fs, pluginPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check if plugin exists: %w", err)
|
||||
}
|
||||
|
||||
if exists {
|
||||
return fmt.Errorf("plugin already installed: %s", plugin.Name)
|
||||
}
|
||||
|
||||
if err := m.fs.MkdirAll(m.pluginsDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create plugins directory: %w", err)
|
||||
}
|
||||
|
||||
reposDir := filepath.Join(m.pluginsDir, ".repos")
|
||||
if err := m.fs.MkdirAll(reposDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create repos directory: %w", err)
|
||||
}
|
||||
|
||||
if plugin.Path != "" {
|
||||
repoName := m.getRepoName(plugin.Repo)
|
||||
repoPath := filepath.Join(reposDir, repoName)
|
||||
|
||||
repoExists, err := afero.DirExists(m.fs, repoPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check if repo exists: %w", err)
|
||||
}
|
||||
|
||||
if !repoExists {
|
||||
if err := m.gitClient.PlainClone(repoPath, plugin.Repo); err != nil {
|
||||
m.fs.RemoveAll(repoPath)
|
||||
return fmt.Errorf("failed to clone repository: %w", err)
|
||||
}
|
||||
} else {
|
||||
// Pull latest changes if repo already exists
|
||||
if err := m.gitClient.Pull(repoPath); err != nil {
|
||||
// If pull fails (e.g., corrupted shallow clone), delete and re-clone
|
||||
if err := m.fs.RemoveAll(repoPath); err != nil {
|
||||
return fmt.Errorf("failed to remove corrupted repository: %w", err)
|
||||
}
|
||||
|
||||
if err := m.gitClient.PlainClone(repoPath, plugin.Repo); err != nil {
|
||||
return fmt.Errorf("failed to re-clone repository: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sourcePath := filepath.Join(repoPath, plugin.Path)
|
||||
sourceExists, err := afero.DirExists(m.fs, sourcePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check plugin path: %w", err)
|
||||
}
|
||||
if !sourceExists {
|
||||
return fmt.Errorf("plugin path does not exist in repository: %s", plugin.Path)
|
||||
}
|
||||
|
||||
if err := m.createSymlink(sourcePath, pluginPath); err != nil {
|
||||
return fmt.Errorf("failed to create symlink: %w", err)
|
||||
}
|
||||
|
||||
metaPath := pluginPath + ".meta"
|
||||
metaContent := fmt.Sprintf("repo=%s\npath=%s\nrepodir=%s", plugin.Repo, plugin.Path, repoName)
|
||||
if err := afero.WriteFile(m.fs, metaPath, []byte(metaContent), 0644); err != nil {
|
||||
return fmt.Errorf("failed to write metadata: %w", err)
|
||||
}
|
||||
} else {
|
||||
if err := m.gitClient.PlainClone(pluginPath, plugin.Repo); err != nil {
|
||||
m.fs.RemoveAll(pluginPath)
|
||||
return fmt.Errorf("failed to clone plugin: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) getRepoName(repoURL string) string {
|
||||
hash := sha256.Sum256([]byte(repoURL))
|
||||
return hex.EncodeToString(hash[:])[:16]
|
||||
}
|
||||
|
||||
func (m *Manager) createSymlink(source, dest string) error {
|
||||
if symlinkFs, ok := m.fs.(afero.Symlinker); ok {
|
||||
return symlinkFs.SymlinkIfPossible(source, dest)
|
||||
}
|
||||
return os.Symlink(source, dest)
|
||||
}
|
||||
|
||||
func (m *Manager) Update(plugin Plugin) error {
|
||||
pluginPath := filepath.Join(m.pluginsDir, plugin.ID)
|
||||
|
||||
exists, err := afero.DirExists(m.fs, pluginPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check if plugin exists: %w", err)
|
||||
}
|
||||
|
||||
if !exists {
|
||||
systemPluginPath := filepath.Join("/etc/xdg/quickshell/dms-plugins", plugin.ID)
|
||||
systemExists, err := afero.DirExists(m.fs, systemPluginPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check if plugin exists: %w", err)
|
||||
}
|
||||
if systemExists {
|
||||
return fmt.Errorf("cannot update system plugin: %s", plugin.Name)
|
||||
}
|
||||
return fmt.Errorf("plugin not installed: %s", plugin.Name)
|
||||
}
|
||||
|
||||
metaPath := pluginPath + ".meta"
|
||||
metaExists, err := afero.Exists(m.fs, metaPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check metadata: %w", err)
|
||||
}
|
||||
|
||||
if metaExists {
|
||||
reposDir := filepath.Join(m.pluginsDir, ".repos")
|
||||
repoName := m.getRepoName(plugin.Repo)
|
||||
repoPath := filepath.Join(reposDir, repoName)
|
||||
|
||||
// Try to pull, if it fails (e.g., shallow clone corruption), delete and re-clone
|
||||
if err := m.gitClient.Pull(repoPath); err != nil {
|
||||
// Repository is likely corrupted or has issues, delete and re-clone
|
||||
if err := m.fs.RemoveAll(repoPath); err != nil {
|
||||
return fmt.Errorf("failed to remove corrupted repository: %w", err)
|
||||
}
|
||||
|
||||
if err := m.gitClient.PlainClone(repoPath, plugin.Repo); err != nil {
|
||||
return fmt.Errorf("failed to re-clone repository: %w", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Try to pull, if it fails, delete and re-clone
|
||||
if err := m.gitClient.Pull(pluginPath); err != nil {
|
||||
if err := m.fs.RemoveAll(pluginPath); err != nil {
|
||||
return fmt.Errorf("failed to remove corrupted plugin: %w", err)
|
||||
}
|
||||
|
||||
if err := m.gitClient.PlainClone(pluginPath, plugin.Repo); err != nil {
|
||||
return fmt.Errorf("failed to re-clone plugin: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Uninstall(plugin Plugin) error {
|
||||
pluginPath := filepath.Join(m.pluginsDir, plugin.ID)
|
||||
|
||||
exists, err := afero.DirExists(m.fs, pluginPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check if plugin exists: %w", err)
|
||||
}
|
||||
|
||||
if !exists {
|
||||
systemPluginPath := filepath.Join("/etc/xdg/quickshell/dms-plugins", plugin.ID)
|
||||
systemExists, err := afero.DirExists(m.fs, systemPluginPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check if plugin exists: %w", err)
|
||||
}
|
||||
if systemExists {
|
||||
return fmt.Errorf("cannot uninstall system plugin: %s", plugin.Name)
|
||||
}
|
||||
return fmt.Errorf("plugin not installed: %s", plugin.Name)
|
||||
}
|
||||
|
||||
metaPath := pluginPath + ".meta"
|
||||
metaExists, err := afero.Exists(m.fs, metaPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check metadata: %w", err)
|
||||
}
|
||||
|
||||
if metaExists {
|
||||
reposDir := filepath.Join(m.pluginsDir, ".repos")
|
||||
repoName := m.getRepoName(plugin.Repo)
|
||||
repoPath := filepath.Join(reposDir, repoName)
|
||||
|
||||
shouldCleanup, err := m.shouldCleanupRepo(repoPath, plugin.Repo, plugin.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check repo cleanup: %w", err)
|
||||
}
|
||||
|
||||
if err := m.fs.Remove(pluginPath); err != nil {
|
||||
return fmt.Errorf("failed to remove symlink: %w", err)
|
||||
}
|
||||
|
||||
if err := m.fs.Remove(metaPath); err != nil {
|
||||
return fmt.Errorf("failed to remove metadata: %w", err)
|
||||
}
|
||||
|
||||
if shouldCleanup {
|
||||
if err := m.fs.RemoveAll(repoPath); err != nil {
|
||||
return fmt.Errorf("failed to cleanup repository: %w", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if err := m.fs.RemoveAll(pluginPath); err != nil {
|
||||
return fmt.Errorf("failed to remove plugin: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) shouldCleanupRepo(repoPath, repoURL, excludePlugin string) (bool, error) {
|
||||
installed, err := m.ListInstalled()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
registry, err := NewRegistry()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
allPlugins, err := registry.List()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
for _, id := range installed {
|
||||
if id == excludePlugin {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, p := range allPlugins {
|
||||
if p.ID == id && p.Repo == repoURL && p.Path != "" {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (m *Manager) ListInstalled() ([]string, error) {
|
||||
installedMap := make(map[string]bool)
|
||||
|
||||
exists, err := afero.DirExists(m.fs, m.pluginsDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if exists {
|
||||
entries, err := afero.ReadDir(m.fs, m.pluginsDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read plugins directory: %w", err)
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
name := entry.Name()
|
||||
if name == ".repos" || strings.HasSuffix(name, ".meta") {
|
||||
continue
|
||||
}
|
||||
|
||||
fullPath := filepath.Join(m.pluginsDir, name)
|
||||
isPlugin := false
|
||||
|
||||
if entry.IsDir() {
|
||||
isPlugin = true
|
||||
} else if entry.Mode()&os.ModeSymlink != 0 {
|
||||
isPlugin = true
|
||||
} else {
|
||||
info, err := m.fs.Stat(fullPath)
|
||||
if err == nil && info.IsDir() {
|
||||
isPlugin = true
|
||||
}
|
||||
}
|
||||
|
||||
if isPlugin {
|
||||
// Read plugin.json to get the actual plugin ID
|
||||
pluginID := m.getPluginID(fullPath)
|
||||
if pluginID != "" {
|
||||
installedMap[pluginID] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
systemPluginsDir := "/etc/xdg/quickshell/dms-plugins"
|
||||
systemExists, err := afero.DirExists(m.fs, systemPluginsDir)
|
||||
if err == nil && systemExists {
|
||||
entries, err := afero.ReadDir(m.fs, systemPluginsDir)
|
||||
if err == nil {
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
fullPath := filepath.Join(systemPluginsDir, entry.Name())
|
||||
// Read plugin.json to get the actual plugin ID
|
||||
pluginID := m.getPluginID(fullPath)
|
||||
if pluginID != "" {
|
||||
installedMap[pluginID] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var installed []string
|
||||
for name := range installedMap {
|
||||
installed = append(installed, name)
|
||||
}
|
||||
|
||||
return installed, nil
|
||||
}
|
||||
|
||||
// getPluginID reads the plugin.json file and returns the plugin ID
|
||||
func (m *Manager) getPluginID(pluginPath string) string {
|
||||
manifestPath := filepath.Join(pluginPath, "plugin.json")
|
||||
data, err := afero.ReadFile(m.fs, manifestPath)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
var manifest struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &manifest); err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return manifest.ID
|
||||
}
|
||||
|
||||
func (m *Manager) GetPluginsDir() string {
|
||||
return m.pluginsDir
|
||||
}
|
||||
|
||||
func (m *Manager) HasUpdates(pluginID string, plugin Plugin) (bool, error) {
|
||||
pluginPath := filepath.Join(m.pluginsDir, pluginID)
|
||||
|
||||
exists, err := afero.DirExists(m.fs, pluginPath)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to check if plugin exists: %w", err)
|
||||
}
|
||||
|
||||
if !exists {
|
||||
systemPluginPath := filepath.Join("/etc/xdg/quickshell/dms-plugins", pluginID)
|
||||
systemExists, err := afero.DirExists(m.fs, systemPluginPath)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to check system plugin: %w", err)
|
||||
}
|
||||
if systemExists {
|
||||
return false, nil
|
||||
}
|
||||
return false, fmt.Errorf("plugin not installed: %s", pluginID)
|
||||
}
|
||||
|
||||
// Check if there's a .meta file (plugin installed from a monorepo)
|
||||
metaPath := pluginPath + ".meta"
|
||||
metaExists, err := afero.Exists(m.fs, metaPath)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to check metadata: %w", err)
|
||||
}
|
||||
|
||||
if metaExists {
|
||||
// Plugin is from a monorepo, check the repo directory
|
||||
reposDir := filepath.Join(m.pluginsDir, ".repos")
|
||||
repoName := m.getRepoName(plugin.Repo)
|
||||
repoPath := filepath.Join(reposDir, repoName)
|
||||
|
||||
return m.gitClient.HasUpdates(repoPath)
|
||||
}
|
||||
|
||||
// Plugin is a standalone repo
|
||||
return m.gitClient.HasUpdates(pluginPath)
|
||||
}
|
||||
Reference in New Issue
Block a user