mirror of https://github.com/jetkvm/kvm.git
261 lines
6.6 KiB
Go
261 lines
6.6 KiB
Go
package plugin
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"kvm/internal/storage"
|
|
"os"
|
|
"path"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
const pluginsFolder = "/userdata/jetkvm/plugins"
|
|
const pluginsUploadFolder = pluginsFolder + "/uploads"
|
|
|
|
func init() {
|
|
_ = os.MkdirAll(pluginsUploadFolder, 0755)
|
|
|
|
if err := pluginDatabase.Load(); err != nil {
|
|
fmt.Printf("failed to load plugin database: %v\n", err)
|
|
}
|
|
}
|
|
|
|
// Starts all plugins that need to be started
|
|
func ReconcilePlugins() {
|
|
for _, install := range pluginDatabase.Plugins {
|
|
err := install.ReconcileSubprocess()
|
|
if err != nil {
|
|
fmt.Printf("failed to reconcile subprocess for plugin: %v\n", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func GracefullyShutdownPlugins() {
|
|
for _, install := range pluginDatabase.Plugins {
|
|
install.Shutdown()
|
|
}
|
|
}
|
|
|
|
func RpcPluginStartUpload(filename string, size int64) (*storage.StorageFileUpload, error) {
|
|
sanitizedFilename, err := storage.SanitizeFilename(filename)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
filePath := path.Join(pluginsUploadFolder, sanitizedFilename)
|
|
uploadPath := filePath + ".incomplete"
|
|
|
|
if _, err := os.Stat(filePath); err == nil {
|
|
return nil, fmt.Errorf("file already exists: %s", sanitizedFilename)
|
|
}
|
|
|
|
var alreadyUploadedBytes int64 = 0
|
|
if stat, err := os.Stat(uploadPath); err == nil {
|
|
alreadyUploadedBytes = stat.Size()
|
|
}
|
|
|
|
uploadId := "plugin_" + uuid.New().String()
|
|
file, err := os.OpenFile(uploadPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to open file for upload: %v", err)
|
|
}
|
|
|
|
storage.AddPendingUpload(uploadId, storage.PendingUpload{
|
|
File: file,
|
|
Size: size,
|
|
AlreadyUploadedBytes: alreadyUploadedBytes,
|
|
})
|
|
|
|
return &storage.StorageFileUpload{
|
|
AlreadyUploadedBytes: alreadyUploadedBytes,
|
|
DataChannel: uploadId,
|
|
}, nil
|
|
}
|
|
|
|
func RpcPluginExtract(filename string) (*PluginManifest, error) {
|
|
sanitizedFilename, err := storage.SanitizeFilename(filename)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
filePath := path.Join(pluginsUploadFolder, sanitizedFilename)
|
|
extractFolder, err := extractPlugin(filePath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := os.Remove(filePath); err != nil {
|
|
return nil, fmt.Errorf("failed to delete uploaded file: %v", err)
|
|
}
|
|
|
|
manifest, err := readManifest(extractFolder)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Get existing PluginInstall
|
|
install, ok := pluginDatabase.Plugins[manifest.Name]
|
|
if !ok {
|
|
install = &PluginInstall{
|
|
Enabled: false,
|
|
Version: manifest.Version,
|
|
ExtractedVersions: make(map[string]string),
|
|
}
|
|
}
|
|
|
|
_, ok = install.ExtractedVersions[manifest.Version]
|
|
if ok {
|
|
return nil, fmt.Errorf("this version has already been uploaded: %s", manifest.Version)
|
|
}
|
|
|
|
install.ExtractedVersions[manifest.Version] = extractFolder
|
|
pluginDatabase.Plugins[manifest.Name] = install
|
|
|
|
if err := pluginDatabase.Save(); err != nil {
|
|
return nil, fmt.Errorf("failed to save plugin database: %v", err)
|
|
}
|
|
|
|
return manifest, nil
|
|
}
|
|
|
|
func RpcPluginInstall(name string, version string) error {
|
|
// TODO: find the plugin version in the plugins.json file
|
|
pluginInstall, ok := pluginDatabase.Plugins[name]
|
|
if !ok {
|
|
return fmt.Errorf("plugin not found: %s", name)
|
|
}
|
|
|
|
if pluginInstall.Version == version && pluginInstall.Enabled {
|
|
fmt.Printf("Plugin %s is already installed with version %s\n", name, version)
|
|
return nil
|
|
}
|
|
|
|
_, ok = pluginInstall.ExtractedVersions[version]
|
|
if !ok {
|
|
return fmt.Errorf("plugin version not found: %s", version)
|
|
}
|
|
|
|
// TODO: If there is a running plugin with the same name, stop it and start the new version
|
|
|
|
pluginInstall.Version = version
|
|
pluginInstall.Enabled = true
|
|
pluginDatabase.Plugins[name] = pluginInstall
|
|
|
|
if err := pluginDatabase.Save(); err != nil {
|
|
return fmt.Errorf("failed to save plugin database: %v", err)
|
|
}
|
|
|
|
err := pluginInstall.ReconcileSubprocess()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to start plugin %s: %v", name, err)
|
|
}
|
|
|
|
// TODO: Determine if the old version should be removed
|
|
|
|
return nil
|
|
}
|
|
|
|
func RpcPluginList() ([]PluginStatus, error) {
|
|
plugins := make([]PluginStatus, 0, len(pluginDatabase.Plugins))
|
|
for pluginName, plugin := range pluginDatabase.Plugins {
|
|
status, err := plugin.GetStatus()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get plugin status for %s: %v", pluginName, err)
|
|
}
|
|
plugins = append(plugins, *status)
|
|
}
|
|
return plugins, nil
|
|
}
|
|
|
|
func RpcPluginUpdateConfig(name string, enabled bool) (*PluginStatus, error) {
|
|
pluginInstall, ok := pluginDatabase.Plugins[name]
|
|
if !ok {
|
|
return nil, fmt.Errorf("plugin not found: %s", name)
|
|
}
|
|
|
|
pluginInstall.Enabled = enabled
|
|
pluginDatabase.Plugins[name] = pluginInstall
|
|
|
|
if err := pluginDatabase.Save(); err != nil {
|
|
return nil, fmt.Errorf("failed to save plugin database: %v", err)
|
|
}
|
|
|
|
err := pluginInstall.ReconcileSubprocess()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to stop plugin %s: %v", name, err)
|
|
}
|
|
|
|
status, err := pluginInstall.GetStatus()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get plugin status for %s: %v", name, err)
|
|
}
|
|
return status, nil
|
|
}
|
|
|
|
func RpcPluginUninstall(name string) error {
|
|
pluginInstall, ok := pluginDatabase.Plugins[name]
|
|
if !ok {
|
|
return fmt.Errorf("plugin not found: %s", name)
|
|
}
|
|
|
|
pluginInstall.Enabled = false
|
|
|
|
err := pluginInstall.ReconcileSubprocess()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to stop plugin %s: %v", name, err)
|
|
}
|
|
|
|
delete(pluginDatabase.Plugins, name)
|
|
if err := pluginDatabase.Save(); err != nil {
|
|
return fmt.Errorf("failed to save plugin database: %v", err)
|
|
}
|
|
|
|
err = pluginDatabase.CleanupExtractDirectories()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to cleanup extract directories: %v", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func readManifest(extractFolder string) (*PluginManifest, error) {
|
|
manifestPath := path.Join(extractFolder, "manifest.json")
|
|
manifestFile, err := os.Open(manifestPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to open manifest file: %v", err)
|
|
}
|
|
defer manifestFile.Close()
|
|
|
|
manifest := PluginManifest{}
|
|
if err := json.NewDecoder(manifestFile).Decode(&manifest); err != nil {
|
|
return nil, fmt.Errorf("failed to read manifest file: %v", err)
|
|
}
|
|
|
|
if err := validateManifest(&manifest); err != nil {
|
|
return nil, fmt.Errorf("invalid manifest file: %v", err)
|
|
}
|
|
|
|
return &manifest, nil
|
|
}
|
|
|
|
func validateManifest(manifest *PluginManifest) error {
|
|
if manifest.ManifestVersion != "1" {
|
|
return fmt.Errorf("unsupported manifest version: %s", manifest.ManifestVersion)
|
|
}
|
|
|
|
if manifest.Name == "" {
|
|
return fmt.Errorf("missing plugin name")
|
|
}
|
|
|
|
if manifest.Version == "" {
|
|
return fmt.Errorf("missing plugin version")
|
|
}
|
|
|
|
if manifest.Homepage == "" {
|
|
return fmt.Errorf("missing plugin homepage")
|
|
}
|
|
|
|
return nil
|
|
}
|