package handlers
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/c4pt0r/agfs/agfs-server/pkg/filesystem"
"github.com/c4pt0r/agfs/agfs-server/pkg/mountablefs"
"github.com/c4pt0r/agfs/agfs-server/pkg/plugin"
log "github.com/sirupsen/logrus"
)
type PluginHandler struct {
mfs *mountablefs.MountableFS
}
func NewPluginHandler(mfs *mountablefs.MountableFS) *PluginHandler {
return &PluginHandler{mfs: mfs}
}
type MountInfo struct {
Path string `json:"path"`
PluginName string `json:"pluginName"`
Config map[string]interface{} `json:"config,omitempty"`
}
type ListMountsResponse struct {
Mounts []MountInfo `json:"mounts"`
}
func (ph *PluginHandler) ListMounts(w http.ResponseWriter, r *http.Request) {
mounts := ph.mfs.GetMounts()
var mountInfos []MountInfo
for _, mount := range mounts {
mountInfos = append(mountInfos, MountInfo{
Path: mount.Path,
PluginName: mount.Plugin.Name(),
Config: mount.Config,
})
}
writeJSON(w, http.StatusOK, ListMountsResponse{Mounts: mountInfos})
}
type UnmountRequest struct {
Path string `json:"path"`
}
func (ph *PluginHandler) Unmount(w http.ResponseWriter, r *http.Request) {
var req UnmountRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return
}
if req.Path == "" {
writeError(w, http.StatusBadRequest, "path is required")
return
}
if err := ph.mfs.Unmount(req.Path); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, SuccessResponse{Message: "plugin unmounted"})
}
type MountRequest struct {
FSType string `json:"fstype"`
Path string `json:"path"`
Config map[string]interface{} `json:"config"`
}
func (ph *PluginHandler) Mount(w http.ResponseWriter, r *http.Request) {
var req MountRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return
}
if req.FSType == "" {
writeError(w, http.StatusBadRequest, "fstype is required")
return
}
if req.Path == "" {
writeError(w, http.StatusBadRequest, "path is required")
return
}
if err := ph.mfs.MountPlugin(req.FSType, req.Path, req.Config); err != nil {
if errors.Is(err, filesystem.ErrAlreadyExists) {
writeError(w, http.StatusConflict, err.Error())
return
}
errMsg := err.Error()
if strings.Contains(errMsg, "unknown filesystem type") || strings.Contains(errMsg, "unknown plugin") ||
strings.Contains(errMsg, "failed to validate") || strings.Contains(errMsg, "is required") ||
strings.Contains(errMsg, "invalid") || strings.Contains(errMsg, "unknown configuration parameter") {
writeError(w, http.StatusBadRequest, err.Error())
} else {
writeError(w, http.StatusInternalServerError, err.Error())
}
return
}
writeJSON(w, http.StatusOK, SuccessResponse{Message: "plugin mounted"})
}
type LoadPluginRequest struct {
LibraryPath string `json:"library_path"`
}
type LoadPluginResponse struct {
Message string `json:"message"`
PluginName string `json:"plugin_name"`
OriginalName string `json:"original_name,omitempty"`
Renamed bool `json:"renamed"`
}
func isHTTPURL(path string) bool {
return strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://")
}
func isAGFSPath(path string) bool {
return strings.HasPrefix(path, "agfs://")
}
func downloadPluginFromURL(url string) (string, error) {
log.Infof("Downloading plugin from URL: %s", url)
resp, err := http.Get(url)
if err != nil {
return "", fmt.Errorf("failed to download from URL: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("failed to download from URL: HTTP %d", resp.StatusCode)
}
ext := filepath.Ext(url)
if ext == "" {
ext = ".so"
}
hash := sha256.Sum256([]byte(url))
hashStr := hex.EncodeToString(hash[:])[:16]
tmpDir := os.TempDir()
tmpFile := filepath.Join(tmpDir, fmt.Sprintf("agfs-plugin-%s%s", hashStr, ext))
outFile, err := os.Create(tmpFile)
if err != nil {
return "", fmt.Errorf("failed to create temporary file: %w", err)
}
defer outFile.Close()
written, err := io.Copy(outFile, resp.Body)
if err != nil {
os.Remove(tmpFile)
return "", fmt.Errorf("failed to write downloaded content: %w", err)
}
log.Infof("Downloaded plugin to temporary file: %s (%d bytes)", tmpFile, written)
return tmpFile, nil
}
func (ph *PluginHandler) readPluginFromAGFS(agfsPath string) (string, error) {
path := strings.TrimPrefix(agfsPath, "agfs://")
if path == "" || path == "/" {
return "", fmt.Errorf("invalid agfs path: %s", agfsPath)
}
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
log.Infof("Reading plugin from AGFS path: %s", path)
data, err := ph.mfs.Read(path, 0, -1)
if err != nil && err != io.EOF {
return "", fmt.Errorf("failed to read from AGFS path %s: %w", path, err)
}
ext := filepath.Ext(path)
if ext == "" {
ext = ".so"
}
hash := sha256.Sum256([]byte(agfsPath))
hashStr := hex.EncodeToString(hash[:])[:16]
tmpDir := os.TempDir()
tmpFile := filepath.Join(tmpDir, fmt.Sprintf("agfs-plugin-%s%s", hashStr, ext))
if err := os.WriteFile(tmpFile, data, 0644); err != nil {
return "", fmt.Errorf("failed to write temporary file: %w", err)
}
log.Infof("Read plugin from AGFS to temporary file: %s (%d bytes)", tmpFile, len(data))
return tmpFile, nil
}
func (ph *PluginHandler) LoadPlugin(w http.ResponseWriter, r *http.Request) {
var req LoadPluginRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return
}
if req.LibraryPath == "" {
writeError(w, http.StatusBadRequest, "library_path is required")
return
}
libraryPath := req.LibraryPath
var tmpFile string
if isHTTPURL(libraryPath) {
downloadedFile, err := downloadPluginFromURL(libraryPath)
if err != nil {
writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to download plugin: %v", err))
return
}
tmpFile = downloadedFile
libraryPath = downloadedFile
log.Infof("Using downloaded plugin from temporary file: %s", libraryPath)
} else if isAGFSPath(libraryPath) {
agfsFile, err := ph.readPluginFromAGFS(libraryPath)
if err != nil {
writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to read plugin from AGFS: %v", err))
return
}
tmpFile = agfsFile
libraryPath = agfsFile
log.Infof("Using plugin from AGFS temporary file: %s", libraryPath)
}
plugin, err := ph.mfs.LoadExternalPlugin(libraryPath)
if err != nil {
if tmpFile != "" {
os.Remove(tmpFile)
}
writeError(w, http.StatusInternalServerError, err.Error())
return
}
response := LoadPluginResponse{
Message: "plugin loaded successfully",
PluginName: plugin.Name(),
Renamed: false,
}
if renamedPlugin, ok := plugin.(*mountablefs.RenamedPlugin); ok {
response.OriginalName = renamedPlugin.OriginalName()
response.Renamed = true
}
writeJSON(w, http.StatusOK, response)
}
type UnloadPluginRequest struct {
LibraryPath string `json:"library_path"`
}
func (ph *PluginHandler) UnloadPlugin(w http.ResponseWriter, r *http.Request) {
var req UnloadPluginRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return
}
if req.LibraryPath == "" {
writeError(w, http.StatusBadRequest, "library_path is required")
return
}
if err := ph.mfs.UnloadExternalPlugin(req.LibraryPath); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, SuccessResponse{Message: "plugin unloaded successfully"})
}
type PluginMountInfo struct {
Path string `json:"path"`
Config map[string]interface{} `json:"config,omitempty"`
}
type PluginInfo struct {
Name string `json:"name"`
LibraryPath string `json:"library_path,omitempty"`
IsExternal bool `json:"is_external"`
MountedPaths []PluginMountInfo `json:"mounted_paths"`
ConfigParams []plugin.ConfigParameter `json:"config_params,omitempty"`
}
type ListPluginsResponse struct {
Plugins []PluginInfo `json:"plugins"`
}
func (ph *PluginHandler) ListPlugins(w http.ResponseWriter, r *http.Request) {
mounts := ph.mfs.GetMounts()
pluginMountsMap := make(map[string][]PluginMountInfo)
pluginInstanceMap := make(map[string]plugin.ServicePlugin)
pluginNamesSet := make(map[string]bool)
for _, mount := range mounts {
pluginName := mount.Plugin.Name()
pluginNamesSet[pluginName] = true
pluginMountsMap[pluginName] = append(pluginMountsMap[pluginName], PluginMountInfo{
Path: mount.Path,
Config: mount.Config,
})
if _, exists := pluginInstanceMap[pluginName]; !exists {
pluginInstanceMap[pluginName] = mount.Plugin
}
}
pluginNameToPath := ph.mfs.GetPluginNameToPathMap()
for pluginName := range pluginNameToPath {
pluginNamesSet[pluginName] = true
}
builtinPlugins := ph.mfs.GetBuiltinPluginNames()
for _, pluginName := range builtinPlugins {
pluginNamesSet[pluginName] = true
}
var plugins []PluginInfo
for pluginName := range pluginNamesSet {
info := PluginInfo{
Name: pluginName,
MountedPaths: pluginMountsMap[pluginName],
IsExternal: false,
}
if libPath, exists := pluginNameToPath[pluginName]; exists {
info.IsExternal = true
info.LibraryPath = libPath
}
if pluginInstance, exists := pluginInstanceMap[pluginName]; exists {
info.ConfigParams = pluginInstance.GetConfigParams()
} else {
tempPlugin := ph.mfs.CreatePlugin(pluginName)
if tempPlugin != nil {
info.ConfigParams = tempPlugin.GetConfigParams()
}
}
plugins = append(plugins, info)
}
writeJSON(w, http.StatusOK, ListPluginsResponse{Plugins: plugins})
}
func (ph *PluginHandler) SetupRoutes(mux *http.ServeMux) {
mux.HandleFunc("/api/v1/mounts", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
ph.ListMounts(w, r)
})
mux.HandleFunc("/api/v1/mount", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
ph.Mount(w, r)
})
mux.HandleFunc("/api/v1/unmount", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
ph.Unmount(w, r)
})
mux.HandleFunc("/api/v1/plugins", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
ph.ListPlugins(w, r)
})
mux.HandleFunc("/api/v1/plugins/load", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
ph.LoadPlugin(w, r)
})
mux.HandleFunc("/api/v1/plugins/unload", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
ph.UnloadPlugin(w, r)
})
}