feat(control): store api (#3)

This commit is contained in:
2025-11-18 14:43:21 +01:00
committed by GitHub
parent 2ab9c82ccb
commit 3b8647e031
7 changed files with 152 additions and 34 deletions
+22 -1
View File
@@ -6,6 +6,7 @@ import (
"net/http" "net/http"
"os" "os"
"os/exec" "os/exec"
"path/filepath"
"plg-mudics/control/frontend" "plg-mudics/control/frontend"
"plg-mudics/shared" "plg-mudics/shared"
"time" "time"
@@ -14,7 +15,25 @@ import (
"github.com/labstack/echo/v4/middleware" "github.com/labstack/echo/v4/middleware"
) )
var storageFile string
func main() { func main() {
var err error
path, err := getStoragePath()
if err != nil {
slog.Error("Failed to initialize storage path", "error", err)
os.Exit(1)
}
storageFile = filepath.Join(path, "storage.json")
// Ensure storage.json exists
if _, err := os.Stat(storageFile); os.IsNotExist(err) {
if err := os.WriteFile(storageFile, []byte("{}"), 0644); err != nil {
slog.Error("Failed to initialize storage.json", "error", err)
os.Exit(1)
}
}
e := echo.New() e := echo.New()
// Serve the embedded SvelteKit frontend // Serve the embedded SvelteKit frontend
@@ -26,6 +45,8 @@ func main() {
// Servers all API endpoints, e.g. our custom logic // Servers all API endpoints, e.g. our custom logic
apiGroup := e.Group("/api") apiGroup := e.Group("/api")
apiGroup.GET("/ping", pingRoute) apiGroup.GET("/ping", pingRoute)
apiGroup.GET("/storage", getStorageRoute)
apiGroup.POST("/storage", setStorageRoute)
port := "8080" port := "8080"
@@ -38,7 +59,7 @@ func main() {
os.Exit(1) os.Exit(1)
} }
}() }()
err := shared.OpenBrowserWindow("http://localhost:"+port, false, false) err = shared.OpenBrowserWindow("http://localhost:"+port, false, false)
if err != nil { if err != nil {
slog.Error("Failed to open browser window", "error", err) slog.Error("Failed to open browser window", "error", err)
os.Exit(1) os.Exit(1)
+16
View File
@@ -0,0 +1,16 @@
meta {
name: Get Store
type: http
seq: 2
}
get {
url: http://localhost:8080/api/storage
body: none
auth: inherit
}
settings {
encodeUrl: true
timeout: 0
}
+22
View File
@@ -0,0 +1,22 @@
meta {
name: Set Store
type: http
seq: 3
}
post {
url: http://localhost:8080/api/storage
body: json
auth: inherit
}
body:json {
{
"key": "value"
}
}
settings {
encodeUrl: true
timeout: 0
}
+61
View File
@@ -0,0 +1,61 @@
package main
import (
"encoding/json"
"fmt"
"log/slog"
"net/http"
"os"
"path/filepath"
"plg-mudics/shared"
"github.com/labstack/echo/v4"
)
func getStorageRoute(ctx echo.Context) error {
data, err := os.ReadFile(storageFile)
if err != nil {
slog.Error("Could not read storage file", "error", err)
return ctx.JSON(http.StatusInternalServerError, shared.ErrorResponse{Error: "Could not read storage file"})
}
var content interface{}
if err := json.Unmarshal(data, &content); err != nil {
slog.Error("Could not parse storage file", "error", err)
return ctx.JSON(http.StatusInternalServerError, shared.ErrorResponse{Error: "Could not parse storage file"})
}
return ctx.JSON(http.StatusOK, content)
}
func setStorageRoute(ctx echo.Context) error {
var payload interface{}
if err := ctx.Bind(&payload); err != nil {
return ctx.JSON(http.StatusBadRequest, shared.ErrorResponse{Error: "Invalid JSON payload"})
}
data, err := json.MarshalIndent(payload, "", " ")
if err != nil {
return ctx.JSON(http.StatusInternalServerError, shared.ErrorResponse{Error: "Failed to marshal storage file"})
}
if err := os.WriteFile(storageFile, data, 0644); err != nil {
return ctx.JSON(http.StatusInternalServerError, shared.ErrorResponse{Error: "Failed to write storage file"})
}
return ctx.JSON(http.StatusOK, map[string]interface{}{})
}
func getStoragePath() (string, error) {
var storagePath string
home, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("unable to determine user home directory: %w", err)
}
storagePath = filepath.Join(home, ".local", "share", "plg-mudics", "control")
if err := os.MkdirAll(storagePath, os.ModePerm); err != nil {
return "", fmt.Errorf("failed to create local config directory: %w", err)
}
return storagePath, nil
}
+1 -1
View File
@@ -95,7 +95,7 @@ func GetStoragePath() (string, error) {
if err != nil { if err != nil {
return "", fmt.Errorf("unable to determine user home directory: %w", err) return "", fmt.Errorf("unable to determine user home directory: %w", err)
} }
storagePath = filepath.Join(home, ".local", "share", "plg-mudics") storagePath = filepath.Join(home, ".local", "share", "plg-mudics", "display")
if err := os.MkdirAll(storagePath, os.ModePerm); err != nil { if err := os.MkdirAll(storagePath, os.ModePerm); err != nil {
return "", fmt.Errorf("failed to create local config directory: %w", err) return "", fmt.Errorf("failed to create local config directory: %w", err)
} }
+26 -30
View File
@@ -21,10 +21,6 @@ import (
"plg-mudics/display/pkg" "plg-mudics/display/pkg"
) )
type ErrorResponse struct {
Error string `json:"error"`
}
var version string var version string
var sseConnection chan string var sseConnection chan string
var supportedExtensions = map[string]bool{ var supportedExtensions = map[string]bool{
@@ -136,7 +132,7 @@ func extractFilePathMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
fullPath, exists, err := pkg.ResolveStorageFilePath(pathParam) fullPath, exists, err := pkg.ResolveStorageFilePath(pathParam)
if err != nil { if err != nil {
slog.Warn("Failed to validate file path", "path", pathParam, "error", err) slog.Warn("Failed to validate file path", "path", pathParam, "error", err)
return ctx.JSON(http.StatusBadRequest, ErrorResponse{Error: "Invalid file path"}) return ctx.JSON(http.StatusBadRequest, shared.ErrorResponse{Error: "Invalid file path"})
} }
ctx.Set("fullPath", fullPath) ctx.Set("fullPath", fullPath)
ctx.Set("fileExists", exists) ctx.Set("fileExists", exists)
@@ -150,14 +146,14 @@ func shellCommandRoute(ctx echo.Context) error {
} }
if err := ctx.Bind(&commandInput); err != nil { if err := ctx.Bind(&commandInput); err != nil {
slog.Error("Failed to parse shell command", "error", err) slog.Error("Failed to parse shell command", "error", err)
return ctx.JSON(http.StatusBadRequest, ErrorResponse{Error: "Invalid JSON request"}) return ctx.JSON(http.StatusBadRequest, shared.ErrorResponse{Error: "Invalid JSON request"})
} }
cmd := exec.Command("bash", "-c", commandInput.Command) cmd := exec.Command("bash", "-c", commandInput.Command)
storagePath, err := pkg.GetStoragePath() storagePath, err := pkg.GetStoragePath()
if err != nil { if err != nil {
slog.Error("Failed to get storage path", "error", err) slog.Error("Failed to get storage path", "error", err)
return ctx.JSON(http.StatusInternalServerError, ErrorResponse{Error: "Internal server error"}) return ctx.JSON(http.StatusInternalServerError, shared.ErrorResponse{Error: "Internal server error"})
} }
cmd.Dir = storagePath cmd.Dir = storagePath
@@ -176,19 +172,19 @@ func keyboardInputRoute(ctx echo.Context) error {
} }
if err := ctx.Bind(&request); err != nil { if err := ctx.Bind(&request); err != nil {
slog.Error("Failed to parse keyboard input", "error", err) slog.Error("Failed to parse keyboard input", "error", err)
return ctx.JSON(http.StatusBadRequest, ErrorResponse{Error: "Invalid JSON request"}) return ctx.JSON(http.StatusBadRequest, shared.ErrorResponse{Error: "Invalid JSON request"})
} }
code, ok := pkg.KeyboardEvents[request.Key] code, ok := pkg.KeyboardEvents[request.Key]
if !ok { if !ok {
slog.Error("Unsupported key", "key", request.Key) slog.Error("Unsupported key", "key", request.Key)
return ctx.JSON(http.StatusBadRequest, ErrorResponse{Error: fmt.Sprintf("Unsupported key: %s", request.Key)}) return ctx.JSON(http.StatusBadRequest, shared.ErrorResponse{Error: fmt.Sprintf("Unsupported key: %s", request.Key)})
} }
err := pkg.KeyboardInput(code) err := pkg.KeyboardInput(code)
if err != nil { if err != nil {
slog.Error("Failed to send keyboard input", "key", request.Key, "error", err) slog.Error("Failed to send keyboard input", "key", request.Key, "error", err)
return ctx.JSON(http.StatusInternalServerError, ErrorResponse{Error: "Failed to send keyboard input"}) return ctx.JSON(http.StatusInternalServerError, shared.ErrorResponse{Error: "Failed to send keyboard input"})
} }
slog.Info("Keyboard input sent", "key", request.Key) slog.Info("Keyboard input sent", "key", request.Key)
@@ -200,25 +196,25 @@ func uploadFileRoute(ctx echo.Context) error {
// Ensure parent directories exist // Ensure parent directories exist
if err := os.MkdirAll(filepath.Dir(fullPath), os.ModePerm); err != nil { if err := os.MkdirAll(filepath.Dir(fullPath), os.ModePerm); err != nil {
return ctx.JSON(http.StatusInternalServerError, ErrorResponse{Error: "Failed to prepare storage directory"}) return ctx.JSON(http.StatusInternalServerError, shared.ErrorResponse{Error: "Failed to prepare storage directory"})
} }
if ctx.Get("fileExists").(bool) { if ctx.Get("fileExists").(bool) {
return ctx.JSON(http.StatusConflict, ErrorResponse{Error: "File already exists"}) return ctx.JSON(http.StatusConflict, shared.ErrorResponse{Error: "File already exists"})
} }
ext := strings.ToLower(filepath.Ext(fullPath)) ext := strings.ToLower(filepath.Ext(fullPath))
if !supportedExtensions[ext] { if !supportedExtensions[ext] {
return ctx.JSON(http.StatusBadRequest, ErrorResponse{Error: fmt.Sprintf("Unsupported file extension: %s", ext)}) return ctx.JSON(http.StatusBadRequest, shared.ErrorResponse{Error: fmt.Sprintf("Unsupported file extension: %s", ext)})
} }
data, err := io.ReadAll(ctx.Request().Body) data, err := io.ReadAll(ctx.Request().Body)
if err != nil { if err != nil {
return ctx.JSON(http.StatusInternalServerError, ErrorResponse{Error: "Failed to read file body"}) return ctx.JSON(http.StatusInternalServerError, shared.ErrorResponse{Error: "Failed to read file body"})
} }
if err := os.WriteFile(fullPath, data, os.ModePerm); err != nil { if err := os.WriteFile(fullPath, data, os.ModePerm); err != nil {
return ctx.JSON(http.StatusInternalServerError, ErrorResponse{Error: "Failed to save file"}) return ctx.JSON(http.StatusInternalServerError, shared.ErrorResponse{Error: "Failed to save file"})
} }
slog.Info("File uploaded successfully", "path", fullPath) slog.Info("File uploaded successfully", "path", fullPath)
@@ -229,13 +225,13 @@ func downloadFileRoute(ctx echo.Context) error {
fullPath := ctx.Get("fullPath").(string) fullPath := ctx.Get("fullPath").(string)
if !ctx.Get("fileExists").(bool) { if !ctx.Get("fileExists").(bool) {
return ctx.JSON(http.StatusNotFound, ErrorResponse{Error: "File not found"}) return ctx.JSON(http.StatusNotFound, shared.ErrorResponse{Error: "File not found"})
} }
err := ctx.File(fullPath) err := ctx.File(fullPath)
if err != nil { if err != nil {
slog.Error("Failed to serve file", "file", fullPath, "error", err) slog.Error("Failed to serve file", "file", fullPath, "error", err)
return ctx.JSON(http.StatusInternalServerError, ErrorResponse{Error: "Internal server error"}) return ctx.JSON(http.StatusInternalServerError, shared.ErrorResponse{Error: "Internal server error"})
} }
slog.Info("File downloaded successfully", "path", fullPath) slog.Info("File downloaded successfully", "path", fullPath)
@@ -247,17 +243,17 @@ func openFileRoute(ctx echo.Context) error {
fullPath := ctx.Get("fullPath").(string) fullPath := ctx.Get("fullPath").(string)
if !ctx.Get("fileExists").(bool) { if !ctx.Get("fileExists").(bool) {
return ctx.JSON(http.StatusNotFound, ErrorResponse{Error: "File not found"}) return ctx.JSON(http.StatusNotFound, shared.ErrorResponse{Error: "File not found"})
} }
if sseConnection == nil { if sseConnection == nil {
return ctx.JSON(http.StatusInternalServerError, ErrorResponse{Error: "Cant connect to display browser client"}) return ctx.JSON(http.StatusInternalServerError, shared.ErrorResponse{Error: "Cant connect to display browser client"})
} }
err := resetView() err := resetView()
if err != nil { if err != nil {
slog.Error("Failed to reset view", "error", err) slog.Error("Failed to reset view", "error", err)
return ctx.JSON(http.StatusInternalServerError, ErrorResponse{Error: "Failed to reset view"}) return ctx.JSON(http.StatusInternalServerError, shared.ErrorResponse{Error: "Failed to reset view"})
} }
ext := strings.ToLower(filepath.Ext(fullPath)) ext := strings.ToLower(filepath.Ext(fullPath))
@@ -275,10 +271,10 @@ func openFileRoute(ctx echo.Context) error {
err := pkg.OpenPresentation(fullPath) err := pkg.OpenPresentation(fullPath)
if err != nil { if err != nil {
slog.Error("Failed to open presentation", "file", pathParam, "error", err) slog.Error("Failed to open presentation", "file", pathParam, "error", err)
return ctx.JSON(http.StatusInternalServerError, ErrorResponse{Error: "Failed to open presentation"}) return ctx.JSON(http.StatusInternalServerError, shared.ErrorResponse{Error: "Failed to open presentation"})
} }
default: default:
return ctx.JSON(http.StatusBadRequest, ErrorResponse{Error: "Unsupported file type"}) return ctx.JSON(http.StatusBadRequest, shared.ErrorResponse{Error: "Unsupported file type"})
} }
slog.Info("Successfully run file", "file", pathParam) slog.Info("Successfully run file", "file", pathParam)
@@ -291,13 +287,13 @@ func showHTMLRoute(ctx echo.Context) error {
} }
if err := ctx.Bind(&request); err != nil { if err := ctx.Bind(&request); err != nil {
slog.Error("Failed to parse request", "error", err) slog.Error("Failed to parse request", "error", err)
return ctx.JSON(http.StatusBadRequest, ErrorResponse{Error: "Invalid JSON request"}) return ctx.JSON(http.StatusBadRequest, shared.ErrorResponse{Error: "Invalid JSON request"})
} }
err := resetView() err := resetView()
if err != nil { if err != nil {
slog.Error("Failed to reset view", "error", err) slog.Error("Failed to reset view", "error", err)
return ctx.JSON(http.StatusInternalServerError, ErrorResponse{Error: "Failed to reset view"}) return ctx.JSON(http.StatusInternalServerError, shared.ErrorResponse{Error: "Failed to reset view"})
} }
sseConnection <- request.HTML sseConnection <- request.HTML
@@ -318,13 +314,13 @@ func takeScreenshotRoute(ctx echo.Context) error {
screenshotPath, err := pkg.TakeScreenshot() screenshotPath, err := pkg.TakeScreenshot()
if err != nil { if err != nil {
slog.Error("Failed to take screenshot", "error", err) slog.Error("Failed to take screenshot", "error", err)
return ctx.JSON(http.StatusInternalServerError, ErrorResponse{Error: "Internal server error"}) return ctx.JSON(http.StatusInternalServerError, shared.ErrorResponse{Error: "Internal server error"})
} }
err = ctx.File(screenshotPath) err = ctx.File(screenshotPath)
if err != nil { if err != nil {
slog.Error("Failed to serve file", "file", screenshotPath, "error", err) slog.Error("Failed to serve file", "file", screenshotPath, "error", err)
return ctx.JSON(http.StatusInternalServerError, ErrorResponse{Error: "Internal server error"}) return ctx.JSON(http.StatusInternalServerError, shared.ErrorResponse{Error: "Internal server error"})
} }
return nil return nil
@@ -334,19 +330,19 @@ func previewRoute(ctx echo.Context) error {
fullPath := ctx.Get("fullPath").(string) fullPath := ctx.Get("fullPath").(string)
exists := ctx.Get("fileExists").(bool) exists := ctx.Get("fileExists").(bool)
if !exists { if !exists {
return ctx.JSON(http.StatusNotFound, ErrorResponse{Error: "File not found"}) return ctx.JSON(http.StatusNotFound, shared.ErrorResponse{Error: "File not found"})
} }
outputFilePath, err := pkg.GenerateFilePreview(fullPath) outputFilePath, err := pkg.GenerateFilePreview(fullPath)
if err != nil { if err != nil {
slog.Error("Failed to generate preview", "file", fullPath, "error", err) slog.Error("Failed to generate preview", "file", fullPath, "error", err)
if errors.Is(err, pkg.ErrFileTypePreviewNotSupported) { if errors.Is(err, pkg.ErrFileTypePreviewNotSupported) {
return ctx.JSON(http.StatusBadRequest, ErrorResponse{Error: "File type not supported for preview"}) return ctx.JSON(http.StatusBadRequest, shared.ErrorResponse{Error: "File type not supported for preview"})
} }
if errors.Is(err, pkg.ErrFilePreviewToolsMissing) { if errors.Is(err, pkg.ErrFilePreviewToolsMissing) {
return ctx.JSON(http.StatusInternalServerError, ErrorResponse{Error: "Required tools for file preview are missing"}) return ctx.JSON(http.StatusInternalServerError, shared.ErrorResponse{Error: "Required tools for file preview are missing"})
} }
return ctx.JSON(http.StatusInternalServerError, ErrorResponse{Error: "Failed to generate preview"}) return ctx.JSON(http.StatusInternalServerError, shared.ErrorResponse{Error: "Failed to generate preview"})
} }
return ctx.File(outputFilePath) return ctx.File(outputFilePath)
+3 -1
View File
@@ -12,7 +12,9 @@ type CommandResponse struct {
ExitCode int `json:"exitCode"` ExitCode int `json:"exitCode"`
} }
type ErrorResponse struct {
Error string `json:"error"`
}
func RunShellCommand(cmd *exec.Cmd) CommandResponse { func RunShellCommand(cmd *exec.Cmd) CommandResponse {
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer