mirror of
https://codeberg.org/PLG-Development/PLG-MuDiCS
synced 2026-07-06 00:47:09 +00:00
refactor(display): move os logic into own package
This commit is contained in:
+27
-175
@@ -3,34 +3,25 @@ package main
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"github.com/micmonay/keybd_event"
|
"github.com/micmonay/keybd_event"
|
||||||
)
|
|
||||||
|
|
||||||
type CommandResponse struct {
|
"plg-mudics-display/pkg"
|
||||||
Stdout string `json:"stdout"`
|
)
|
||||||
Stderr string `json:"stderr"`
|
|
||||||
ExitCode int `json:"exitCode"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ErrorResponse struct {
|
type ErrorResponse struct {
|
||||||
Error string `json:"error"`
|
Error string `json:"error"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var storagePath string
|
|
||||||
var chromiumBin string
|
|
||||||
var sseConnection chan string
|
var sseConnection chan string
|
||||||
var supportedExtensions = map[string]bool{
|
var supportedExtensions = map[string]bool{
|
||||||
".mp4": true,
|
".mp4": true,
|
||||||
@@ -49,30 +40,16 @@ const Version = "0.1.0"
|
|||||||
func main() {
|
func main() {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
if err := checkDependencies(); err != nil {
|
|
||||||
slog.Error("Dependency check failed", "error", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure local config directory exists
|
// Ensure local config directory exists
|
||||||
home, err := os.UserHomeDir()
|
_, err = pkg.GetStoragePath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Unable to determine user home directory", "error", err)
|
slog.Error("Failed to get storage path", "error", err)
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
storagePath = filepath.Join(home, ".local", "share", "plg-connect-display")
|
|
||||||
if err := os.MkdirAll(storagePath, os.ModePerm); err != nil {
|
|
||||||
slog.Error("Failed to create local config directory", "path", storagePath, "error", err)
|
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open browser window
|
// Open browser window
|
||||||
go func() {
|
go pkg.OpenBrowserWindow("http://127.0.0.1:1323")
|
||||||
args := fmt.Sprintf("%s --app='http://127.0.0.1:1323' --start-fullscreen --user-data-dir=$(mktemp -d) --autoplay-policy=no-user-gesture-required", chromiumBin)
|
|
||||||
cmd := exec.Command("bash", "-c", args)
|
|
||||||
_ = cmd.Run()
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Webserver
|
// Webserver
|
||||||
e := echo.New()
|
e := echo.New()
|
||||||
|
|
||||||
@@ -98,32 +75,6 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkDependencies() error {
|
|
||||||
// Detect available Chromium binary name
|
|
||||||
for _, b := range []string{"chromium", "chromium-browser"} {
|
|
||||||
if _, err := exec.LookPath(b); err == nil {
|
|
||||||
chromiumBin = b
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if chromiumBin == "" {
|
|
||||||
return errors.New("chromium or chromium-browser not found in PATH")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check other dependencies
|
|
||||||
deps := []string{
|
|
||||||
"soffice", // LibreOffice
|
|
||||||
"bash",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, dep := range deps {
|
|
||||||
if _, err := exec.LookPath(dep); err != nil {
|
|
||||||
return errors.New(dep + " not found in PATH")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func indexRoute(ctx echo.Context) error {
|
func indexRoute(ctx echo.Context) error {
|
||||||
return indexTemplate().Render(ctx.Request().Context(), ctx.Response().Writer)
|
return indexTemplate().Render(ctx.Request().Context(), ctx.Response().Writer)
|
||||||
}
|
}
|
||||||
@@ -141,11 +92,11 @@ func sseRoute(ctx echo.Context) error {
|
|||||||
sseConnection = make(chan string)
|
sseConnection = make(chan string)
|
||||||
|
|
||||||
// init display
|
// init display
|
||||||
ip, err := getDeviceIp()
|
ip, err := pkg.GetDeviceIp()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed to get device IP address", "error", err)
|
slog.Error("Failed to get device IP address", "error", err)
|
||||||
}
|
}
|
||||||
mac, err := getDeviceMac()
|
mac, err := pkg.GetDeviceMac()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed to get device MAC address", "error", err)
|
slog.Error("Failed to get device MAC address", "error", err)
|
||||||
}
|
}
|
||||||
@@ -179,65 +130,14 @@ func sseRoute(ctx echo.Context) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDeviceIp() (string, error) {
|
|
||||||
addrs, err := net.InterfaceAddrs()
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to get network interfaces: %w", err)
|
|
||||||
}
|
|
||||||
for _, addr := range addrs {
|
|
||||||
ipNet, ok := addr.(*net.IPNet)
|
|
||||||
if ok && !ipNet.IP.IsLoopback() && ipNet.IP.To4() != nil {
|
|
||||||
return ipNet.IP.String(), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", fmt.Errorf("no suitable IP address found")
|
|
||||||
}
|
|
||||||
|
|
||||||
func getDeviceMac() (string, error) {
|
|
||||||
interfaces, err := net.Interfaces()
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to get network interfaces: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, interf := range interfaces {
|
|
||||||
mac := interf.HardwareAddr.String()
|
|
||||||
if mac != "" {
|
|
||||||
return mac, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", fmt.Errorf("no suitable MAC address found")
|
|
||||||
}
|
|
||||||
|
|
||||||
func extractFilePathMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
|
func extractFilePathMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
return func(ctx echo.Context) error {
|
return func(ctx echo.Context) error {
|
||||||
// Retrieve and clean the path parameter
|
|
||||||
pathParam := ctx.Param("path")
|
pathParam := ctx.Param("path")
|
||||||
cleanPath := filepath.Clean(pathParam)
|
fullPath, exists, err := pkg.ResolveStorageFilePath(pathParam)
|
||||||
fullPath := filepath.Join(storagePath, cleanPath)
|
if err != nil {
|
||||||
rel, err := filepath.Rel(storagePath, fullPath)
|
slog.Warn("Failed to validate file path", "path", pathParam, "error", err)
|
||||||
if err != nil || strings.HasPrefix(rel, "..") {
|
|
||||||
return ctx.JSON(http.StatusBadRequest, ErrorResponse{Error: "Invalid file path"})
|
return ctx.JSON(http.StatusBadRequest, ErrorResponse{Error: "Invalid file path"})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine if the target path exists and is a file
|
|
||||||
var exists bool
|
|
||||||
info, statErr := os.Stat(fullPath)
|
|
||||||
if statErr != nil {
|
|
||||||
if os.IsNotExist(statErr) {
|
|
||||||
exists = false
|
|
||||||
} else {
|
|
||||||
slog.Error("Failed to stat path", "path", fullPath, "error", statErr)
|
|
||||||
return ctx.JSON(http.StatusInternalServerError, ErrorResponse{Error: "Internal server error"})
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if info.IsDir() {
|
|
||||||
return ctx.JSON(http.StatusBadRequest, ErrorResponse{Error: "Path is a directory"})
|
|
||||||
}
|
|
||||||
exists = true
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Set("fullPath", fullPath)
|
ctx.Set("fullPath", fullPath)
|
||||||
ctx.Set("fileExists", exists)
|
ctx.Set("fileExists", exists)
|
||||||
return next(ctx)
|
return next(ctx)
|
||||||
@@ -254,9 +154,14 @@ func shellCommandRoute(ctx echo.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cmd := exec.Command("bash", "-c", "-r", commandInput.Command)
|
cmd := exec.Command("bash", "-c", "-r", 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"})
|
||||||
|
}
|
||||||
cmd.Dir = storagePath
|
cmd.Dir = storagePath
|
||||||
|
|
||||||
commandOutput := runShellCommand(cmd)
|
commandOutput := pkg.RunShellCommand(cmd)
|
||||||
if commandOutput.ExitCode != 0 {
|
if commandOutput.ExitCode != 0 {
|
||||||
slog.Error("Shell command execution error", "error", commandOutput.Stderr)
|
slog.Error("Shell command execution error", "error", commandOutput.Stderr)
|
||||||
}
|
}
|
||||||
@@ -274,13 +179,13 @@ func keyboardInputRoute(ctx echo.Context) error {
|
|||||||
return ctx.JSON(http.StatusBadRequest, ErrorResponse{Error: "Invalid JSON request"})
|
return ctx.JSON(http.StatusBadRequest, ErrorResponse{Error: "Invalid JSON request"})
|
||||||
}
|
}
|
||||||
|
|
||||||
code, ok := 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, ErrorResponse{Error: fmt.Sprintf("Unsupported key: %s", request.Key)})
|
||||||
}
|
}
|
||||||
|
|
||||||
err := 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, ErrorResponse{Error: "Failed to send keyboard input"})
|
||||||
@@ -367,7 +272,7 @@ func openFileRoute(ctx echo.Context) error {
|
|||||||
imageTemplate(pathParam).Render(context.Background(), &templateBuffer)
|
imageTemplate(pathParam).Render(context.Background(), &templateBuffer)
|
||||||
sseConnection <- templateBuffer.String()
|
sseConnection <- templateBuffer.String()
|
||||||
case ".pptx", ".odp":
|
case ".pptx", ".odp":
|
||||||
openPresentation(fullPath)
|
pkg.OpenPresentation(fullPath)
|
||||||
default:
|
default:
|
||||||
return ctx.JSON(http.StatusBadRequest, ErrorResponse{Error: "Unsupported file type"})
|
return ctx.JSON(http.StatusBadRequest, ErrorResponse{Error: "Unsupported file type"})
|
||||||
}
|
}
|
||||||
@@ -376,25 +281,6 @@ func openFileRoute(ctx echo.Context) error {
|
|||||||
return ctx.NoContent(http.StatusOK)
|
return ctx.NoContent(http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
func openPresentation(path string) {
|
|
||||||
cmd := exec.Command("bash", "-c", "-r", fmt.Sprintf("soffice --show %s -nologo -norestore", path))
|
|
||||||
_ = cmd.Run()
|
|
||||||
}
|
|
||||||
|
|
||||||
func keyboardInput(key int) error {
|
|
||||||
kb, err := keybd_event.NewKeyBonding()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create key bonding: %w", err)
|
|
||||||
}
|
|
||||||
kb.SetKeys(key)
|
|
||||||
|
|
||||||
if err := kb.Launching(); err != nil {
|
|
||||||
return fmt.Errorf("failed to launch key event: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func showHTMLRoute(ctx echo.Context) error {
|
func showHTMLRoute(ctx echo.Context) error {
|
||||||
var request struct {
|
var request struct {
|
||||||
HTML string `json:"html"`
|
HTML string `json:"html"`
|
||||||
@@ -422,21 +308,10 @@ func pingRoute(ctx echo.Context) error {
|
|||||||
}{Version: Version})
|
}{Version: Version})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset previous file views so they dont collide with the new one
|
|
||||||
func resetView() error {
|
|
||||||
err := keyboardInput(keybd_event.VK_ESC)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to send ESC key: %w", err)
|
|
||||||
}
|
|
||||||
sseConnection <- ""
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func takeScreenshotRoute(ctx echo.Context) error {
|
func takeScreenshotRoute(ctx echo.Context) error {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
screenshotPath, err := 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, ErrorResponse{Error: "Internal server error"})
|
||||||
@@ -451,36 +326,13 @@ func takeScreenshotRoute(ctx echo.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func takeScreenshot() (string, error) {
|
// Reset previous file views so they dont collide with the new one
|
||||||
tempFilePath := filepath.Join(os.TempDir(), fmt.Sprintf("screenshot_%d.png", time.Now().Unix()))
|
func resetView() error {
|
||||||
|
err := pkg.KeyboardInput(keybd_event.VK_ESC)
|
||||||
cmd := exec.Command("gnome-screenshot", "-f", tempFilePath)
|
|
||||||
commandOutput := runShellCommand(cmd)
|
|
||||||
if commandOutput.ExitCode != 0 {
|
|
||||||
return "", errors.New(commandOutput.Stderr)
|
|
||||||
}
|
|
||||||
return tempFilePath, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func runShellCommand(cmd *exec.Cmd) CommandResponse {
|
|
||||||
var stdout, stderr bytes.Buffer
|
|
||||||
cmd.Stdout = &stdout
|
|
||||||
cmd.Stderr = &stderr
|
|
||||||
err := cmd.Run()
|
|
||||||
|
|
||||||
commandOutput := CommandResponse{
|
|
||||||
Stdout: stdout.String(),
|
|
||||||
Stderr: stderr.String(),
|
|
||||||
ExitCode: cmd.ProcessState.ExitCode(),
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var exitErr *exec.ExitError
|
return fmt.Errorf("failed to send ESC key: %w", err)
|
||||||
if errors.As(err, &exitErr) {
|
|
||||||
commandOutput.ExitCode = exitErr.ExitCode()
|
|
||||||
} else {
|
|
||||||
commandOutput.Stderr = err.Error()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
sseConnection <- ""
|
||||||
|
|
||||||
return commandOutput
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package main
|
package pkg
|
||||||
|
|
||||||
import "github.com/micmonay/keybd_event"
|
import "github.com/micmonay/keybd_event"
|
||||||
|
|
||||||
var keyboardEvents = map[string]int{
|
var KeyboardEvents = map[string]int{
|
||||||
"VK_SP1": keybd_event.VK_SP1,
|
"VK_SP1": keybd_event.VK_SP1,
|
||||||
"VK_SP2": keybd_event.VK_SP2,
|
"VK_SP2": keybd_event.VK_SP2,
|
||||||
"VK_SP3": keybd_event.VK_SP3,
|
"VK_SP3": keybd_event.VK_SP3,
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
package pkg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/micmonay/keybd_event"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CommandResponse struct {
|
||||||
|
Stdout string `json:"stdout"`
|
||||||
|
Stderr string `json:"stderr"`
|
||||||
|
ExitCode int `json:"exitCode"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetDeviceIp() (string, error) {
|
||||||
|
addrs, err := net.InterfaceAddrs()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to get network interfaces: %w", err)
|
||||||
|
}
|
||||||
|
for _, addr := range addrs {
|
||||||
|
ipNet, ok := addr.(*net.IPNet)
|
||||||
|
if ok && !ipNet.IP.IsLoopback() && ipNet.IP.To4() != nil {
|
||||||
|
return ipNet.IP.String(), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("no suitable IP address found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetDeviceMac() (string, error) {
|
||||||
|
interfaces, err := net.Interfaces()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to get network interfaces: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, interf := range interfaces {
|
||||||
|
mac := interf.HardwareAddr.String()
|
||||||
|
if mac != "" {
|
||||||
|
return mac, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("no suitable MAC address found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func OpenPresentation(path string) {
|
||||||
|
cmd := exec.Command("bash", "-c", "-r", fmt.Sprintf("soffice --show %s -nologo -norestore", path))
|
||||||
|
_ = cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func KeyboardInput(key int) error {
|
||||||
|
kb, err := keybd_event.NewKeyBonding()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create key bonding: %w", err)
|
||||||
|
}
|
||||||
|
kb.SetKeys(key)
|
||||||
|
|
||||||
|
if err := kb.Launching(); err != nil {
|
||||||
|
return fmt.Errorf("failed to launch key event: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TakeScreenshot() (string, error) {
|
||||||
|
tempFilePath := filepath.Join(os.TempDir(), fmt.Sprintf("screenshot_%d.png", time.Now().Unix()))
|
||||||
|
|
||||||
|
cmd := exec.Command("gnome-screenshot", "-f", tempFilePath)
|
||||||
|
commandOutput := RunShellCommand(cmd)
|
||||||
|
if commandOutput.ExitCode != 0 {
|
||||||
|
return "", errors.New(commandOutput.Stderr)
|
||||||
|
}
|
||||||
|
return tempFilePath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func RunShellCommand(cmd *exec.Cmd) CommandResponse {
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
cmd.Stdout = &stdout
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
err := cmd.Run()
|
||||||
|
|
||||||
|
commandOutput := CommandResponse{
|
||||||
|
Stdout: stdout.String(),
|
||||||
|
Stderr: stderr.String(),
|
||||||
|
ExitCode: cmd.ProcessState.ExitCode(),
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
var exitErr *exec.ExitError
|
||||||
|
if errors.As(err, &exitErr) {
|
||||||
|
commandOutput.ExitCode = exitErr.ExitCode()
|
||||||
|
} else {
|
||||||
|
commandOutput.Stderr = err.Error()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return commandOutput
|
||||||
|
}
|
||||||
|
|
||||||
|
func OpenBrowserWindow(url string) error {
|
||||||
|
// Detect available Chromium binary name
|
||||||
|
var chromiumBin string
|
||||||
|
for _, b := range []string{"chromium", "chromium-browser"} {
|
||||||
|
if _, err := exec.LookPath(b); err == nil {
|
||||||
|
chromiumBin = b
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if chromiumBin == "" {
|
||||||
|
return errors.New("chromium or chromium-browser not found in PATH")
|
||||||
|
}
|
||||||
|
|
||||||
|
args := fmt.Sprintf("%s --app='%s' --start-fullscreen --user-data-dir=$(mktemp -d) --autoplay-policy=no-user-gesture-required", chromiumBin, url)
|
||||||
|
cmd := exec.Command("bash", "-c", args)
|
||||||
|
_ = cmd.Run()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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-connect-display")
|
||||||
|
if err := os.MkdirAll(storagePath, os.ModePerm); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to create local config directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return storagePath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResolveStorageFilePath validates and resolves a storage-relative file path.
|
||||||
|
// Returns the full path, whether the file exists, or an error.
|
||||||
|
func ResolveStorageFilePath(pathParam string) (string, bool, error) {
|
||||||
|
storagePath, err := GetStoragePath()
|
||||||
|
if err != nil {
|
||||||
|
return "", false, fmt.Errorf("failed to get storage path: %w", err)
|
||||||
|
}
|
||||||
|
cleanPath := filepath.Clean(pathParam)
|
||||||
|
fullPath := filepath.Join(storagePath, cleanPath)
|
||||||
|
rel, err := filepath.Rel(storagePath, fullPath)
|
||||||
|
|
||||||
|
if err != nil || strings.HasPrefix(rel, "..") {
|
||||||
|
return "", false, fmt.Errorf("invalid file path")
|
||||||
|
}
|
||||||
|
|
||||||
|
info, statErr := os.Stat(fullPath)
|
||||||
|
|
||||||
|
if statErr != nil {
|
||||||
|
if os.IsNotExist(statErr) {
|
||||||
|
return fullPath, false, nil
|
||||||
|
}
|
||||||
|
return "", false, fmt.Errorf("failed to stat path: %w", statErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.IsDir() {
|
||||||
|
return "", false, fmt.Errorf("path is a directory")
|
||||||
|
}
|
||||||
|
|
||||||
|
return fullPath, true, nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user