mirror of
https://github.com/MengMengCode/VoCat.git
synced 2026-08-13 03:13:43 +08:00
162 lines
5.2 KiB
Go
162 lines
5.2 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const maxPluginUploadBytes int64 = 64 << 20
|
|
|
|
func (s *Server) routeExtensionAPI(w http.ResponseWriter, r *http.Request, cleanPath string) bool {
|
|
if cleanPath == "extensions" {
|
|
if s.extensions == nil {
|
|
writeError(w, http.StatusServiceUnavailable, "extensions_unavailable", "plugin manager is unavailable")
|
|
return true
|
|
}
|
|
switch r.Method {
|
|
case http.MethodGet:
|
|
writeJSON(w, http.StatusOK, map[string]any{"data": s.extensions.List()})
|
|
default:
|
|
w.Header().Set("Allow", "GET")
|
|
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "method not allowed")
|
|
}
|
|
return true
|
|
}
|
|
if cleanPath == "extensions/install-url" {
|
|
if s.extensions == nil {
|
|
writeError(w, http.StatusServiceUnavailable, "extensions_unavailable", "plugin manager is unavailable")
|
|
return true
|
|
}
|
|
if !requireMethod(w, r, http.MethodPost) {
|
|
return true
|
|
}
|
|
var request struct {
|
|
URL string `json:"url"`
|
|
SHA256 string `json:"sha256"`
|
|
}
|
|
if err := s.decodeJSON(w, r, &request); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid_request", err.Error())
|
|
return true
|
|
}
|
|
ctx, cancel := contextWithTimeout(r, 60*time.Second)
|
|
defer cancel()
|
|
plugin, err := s.extensions.InstallURL(ctx, request.URL, request.SHA256)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "plugin_install_failed", err.Error())
|
|
return true
|
|
}
|
|
s.recordAudit(r.Context(), "admin", "plugin.install_url", "plugin", plugin.ID, "success", request.URL)
|
|
writeJSON(w, http.StatusCreated, map[string]any{"data": plugin})
|
|
return true
|
|
}
|
|
if cleanPath == "extensions/upload" {
|
|
if s.extensions == nil {
|
|
writeError(w, http.StatusServiceUnavailable, "extensions_unavailable", "plugin manager is unavailable")
|
|
return true
|
|
}
|
|
if !requireMethod(w, r, http.MethodPost) {
|
|
return true
|
|
}
|
|
r.Body = http.MaxBytesReader(nil, r.Body, maxPluginUploadBytes+(1<<20))
|
|
if err := r.ParseMultipartForm(maxPluginUploadBytes); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid_plugin_upload", "plugin upload must be multipart/form-data and no larger than 64 MiB")
|
|
return true
|
|
}
|
|
file, _, err := r.FormFile("package")
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid_plugin_upload", "multipart field package is required")
|
|
return true
|
|
}
|
|
defer file.Close()
|
|
plugin, err := s.extensions.Install(io.LimitReader(file, maxPluginUploadBytes+1), r.FormValue("sha256"))
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "plugin_install_failed", err.Error())
|
|
return true
|
|
}
|
|
s.recordAudit(r.Context(), "admin", "plugin.upload", "plugin", plugin.ID, "success", "upload")
|
|
writeJSON(w, http.StatusCreated, map[string]any{"data": plugin})
|
|
return true
|
|
}
|
|
|
|
segments := splitAPIPath(cleanPath)
|
|
if len(segments) < 2 || segments[0] != "extensions" {
|
|
return false
|
|
}
|
|
if s.extensions == nil {
|
|
writeError(w, http.StatusServiceUnavailable, "extensions_unavailable", "plugin manager is unavailable")
|
|
return true
|
|
}
|
|
id := segments[1]
|
|
if len(segments) >= 3 && segments[2] == "backend" {
|
|
s.extensions.ProxyBackend(w, r, id)
|
|
return true
|
|
}
|
|
if len(segments) != 2 {
|
|
writeError(w, http.StatusNotFound, "not_found", "plugin endpoint not found")
|
|
return true
|
|
}
|
|
switch r.Method {
|
|
case http.MethodPut:
|
|
var request struct {
|
|
Enabled bool `json:"enabled"`
|
|
}
|
|
if err := s.decodeJSON(w, r, &request); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid_request", err.Error())
|
|
return true
|
|
}
|
|
plugin, err := s.extensions.SetEnabled(id, request.Enabled)
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
writeError(w, http.StatusNotFound, "plugin_not_found", "plugin not found")
|
|
return true
|
|
}
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "plugin_state_failed", err.Error())
|
|
return true
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"data": plugin})
|
|
case http.MethodDelete:
|
|
if err := s.extensions.Uninstall(id); errors.Is(err, os.ErrNotExist) {
|
|
writeError(w, http.StatusNotFound, "plugin_not_found", "plugin not found")
|
|
} else if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "plugin_uninstall_failed", err.Error())
|
|
} else {
|
|
writeJSON(w, http.StatusOK, map[string]any{"data": map[string]bool{"uninstalled": true}})
|
|
}
|
|
default:
|
|
w.Header().Set("Allow", "PUT, DELETE")
|
|
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "method not allowed")
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (s *Server) handlePluginAsset(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet && r.Method != http.MethodHead {
|
|
w.Header().Set("Allow", "GET, HEAD")
|
|
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "method not allowed")
|
|
return
|
|
}
|
|
if !s.requireAuthenticated(w, r) {
|
|
return
|
|
}
|
|
if s.extensions == nil {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
path := strings.TrimPrefix(r.URL.Path, "/plugin-assets/")
|
|
parts := strings.SplitN(path, "/", 2)
|
|
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
s.extensions.ServeAsset(w, r, parts[0], parts[1])
|
|
}
|
|
|
|
func contextWithTimeout(r *http.Request, timeout time.Duration) (context.Context, context.CancelFunc) {
|
|
return context.WithTimeout(r.Context(), timeout)
|
|
}
|