diff --git a/control/main.go b/control/main.go index 3f1225d..8bfa51b 100644 --- a/control/main.go +++ b/control/main.go @@ -6,6 +6,7 @@ import ( "net/http" "os" "os/exec" + "path/filepath" "plg-mudics/control/frontend" "plg-mudics/shared" "time" @@ -14,7 +15,25 @@ import ( "github.com/labstack/echo/v4/middleware" ) +var storageFile string + 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() // Serve the embedded SvelteKit frontend @@ -26,6 +45,8 @@ func main() { // Servers all API endpoints, e.g. our custom logic apiGroup := e.Group("/api") apiGroup.GET("/ping", pingRoute) + apiGroup.GET("/storage", getStorageRoute) + apiGroup.POST("/storage", setStorageRoute) port := "8080" @@ -38,7 +59,7 @@ func main() { os.Exit(1) } }() - err := shared.OpenBrowserWindow("http://localhost:"+port, false, false) + err = shared.OpenBrowserWindow("http://localhost:"+port, false, false) if err != nil { slog.Error("Failed to open browser window", "error", err) os.Exit(1) diff --git a/control/requests/Get Store.bru b/control/requests/Get Store.bru new file mode 100644 index 0000000..df7d57b --- /dev/null +++ b/control/requests/Get Store.bru @@ -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 +} diff --git a/control/requests/Set Store.bru b/control/requests/Set Store.bru new file mode 100644 index 0000000..7c2be8b --- /dev/null +++ b/control/requests/Set Store.bru @@ -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 +} diff --git a/control/storage.go b/control/storage.go new file mode 100644 index 0000000..3fc1da3 --- /dev/null +++ b/control/storage.go @@ -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 +} diff --git a/display/pkg/main.go b/display/pkg/main.go index f7ec9a3..5a3f697 100644 --- a/display/pkg/main.go +++ b/display/pkg/main.go @@ -95,7 +95,7 @@ func GetStoragePath() (string, error) { if err != nil { 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 { return "", fmt.Errorf("failed to create local config directory: %w", err) } diff --git a/display/web/main.go b/display/web/main.go index 02f88db..335d17d 100644 --- a/display/web/main.go +++ b/display/web/main.go @@ -21,10 +21,6 @@ import ( "plg-mudics/display/pkg" ) -type ErrorResponse struct { - Error string `json:"error"` -} - var version string var sseConnection chan string var supportedExtensions = map[string]bool{ @@ -136,7 +132,7 @@ func extractFilePathMiddleware(next echo.HandlerFunc) echo.HandlerFunc { fullPath, exists, err := pkg.ResolveStorageFilePath(pathParam) if err != nil { 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("fileExists", exists) @@ -150,14 +146,14 @@ func shellCommandRoute(ctx echo.Context) error { } if err := ctx.Bind(&commandInput); err != nil { 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) storagePath, err := pkg.GetStoragePath() if err != nil { 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 @@ -176,19 +172,19 @@ func keyboardInputRoute(ctx echo.Context) error { } if err := ctx.Bind(&request); err != nil { 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] if !ok { 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) if err != nil { 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) @@ -200,25 +196,25 @@ func uploadFileRoute(ctx echo.Context) error { // Ensure parent directories exist 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) { - 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)) 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) 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 { - 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) @@ -229,13 +225,13 @@ func downloadFileRoute(ctx echo.Context) error { fullPath := ctx.Get("fullPath").(string) 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) if err != nil { 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) @@ -247,17 +243,17 @@ func openFileRoute(ctx echo.Context) error { fullPath := ctx.Get("fullPath").(string) 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 { - 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() if err != nil { 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)) @@ -275,10 +271,10 @@ func openFileRoute(ctx echo.Context) error { err := pkg.OpenPresentation(fullPath) if err != nil { 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: - 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) @@ -291,13 +287,13 @@ func showHTMLRoute(ctx echo.Context) error { } if err := ctx.Bind(&request); err != nil { 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() if err != nil { 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 @@ -318,13 +314,13 @@ func takeScreenshotRoute(ctx echo.Context) error { screenshotPath, err := pkg.TakeScreenshot() if err != nil { 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) if err != nil { 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 @@ -334,19 +330,19 @@ func previewRoute(ctx echo.Context) error { fullPath := ctx.Get("fullPath").(string) exists := ctx.Get("fileExists").(bool) 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) if err != nil { slog.Error("Failed to generate preview", "file", fullPath, "error", err) 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) { - 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) diff --git a/shared/main.go b/shared/main.go index ffdf305..d9ad7a2 100644 --- a/shared/main.go +++ b/shared/main.go @@ -12,7 +12,9 @@ type CommandResponse struct { ExitCode int `json:"exitCode"` } - +type ErrorResponse struct { + Error string `json:"error"` +} func RunShellCommand(cmd *exec.Cmd) CommandResponse { var stdout, stderr bytes.Buffer @@ -35,4 +37,4 @@ func RunShellCommand(cmd *exec.Cmd) CommandResponse { } return commandOutput -} \ No newline at end of file +}