Add main application and desktop API server implementation
- Introduced main.go for the server with CORS middleware and static asset handling. - Added desktop/app.go for local API server with logging and file handling capabilities. - Implemented desktop/main.go to initialize the application with Wails framework and asset management.
This commit is contained in:
159
desktop/app.go
Normal file
159
desktop/app.go
Normal file
@@ -0,0 +1,159 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
)
|
||||
|
||||
// App struct
|
||||
type App struct {
|
||||
ctx context.Context
|
||||
port int
|
||||
debug bool
|
||||
}
|
||||
|
||||
// NewApp creates a new App struct
|
||||
func NewApp(debug bool) *App {
|
||||
return &App{
|
||||
debug: debug,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) logDebug(format string, args ...any) {
|
||||
if a != nil && a.debug {
|
||||
fmt.Printf("[debug] "+format+"\n", args...)
|
||||
}
|
||||
}
|
||||
|
||||
// logHandler wraps HTTP handlers to log requests when debug is enabled.
|
||||
func (a *App) logHandler(next http.Handler) http.Handler {
|
||||
if !a.debug {
|
||||
return next
|
||||
}
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
next.ServeHTTP(w, r)
|
||||
fmt.Printf("[debug] http %s %s %dms\n", r.Method, r.URL.Path, time.Since(start).Milliseconds())
|
||||
})
|
||||
}
|
||||
|
||||
// startup is called when the app starts. The context is saved
|
||||
// so we can call the runtime methods
|
||||
func (a *App) startup(ctx context.Context) {
|
||||
a.ctx = ctx
|
||||
a.logDebug("startup begin")
|
||||
|
||||
// Start local API server on a random port
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
fmt.Printf("Error starting local server: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
a.port = listener.Addr().(*net.TCPAddr).Port
|
||||
a.logDebug("local API listener bound on %s", listener.Addr().String())
|
||||
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// CORS middleware for local desktop API
|
||||
cors := func(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||
if r.Method == "OPTIONS" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
mux.HandleFunc("/api/ping", cors(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprintf(w, `{"status":"ok"}`)
|
||||
}))
|
||||
|
||||
server := &http.Server{
|
||||
Addr: listener.Addr().String(),
|
||||
Handler: a.logHandler(mux),
|
||||
ReadTimeout: 15 * time.Second,
|
||||
WriteTimeout: 15 * time.Second,
|
||||
IdleTimeout: 60 * time.Second,
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err := server.Serve(listener); err != nil && err != http.ErrServerClosed {
|
||||
fmt.Printf("Error serving desktop API: %v\n", err)
|
||||
}
|
||||
}()
|
||||
fmt.Printf("Desktop API server started on port %d\n", a.port)
|
||||
a.logDebug("startup complete")
|
||||
}
|
||||
|
||||
// GetAPIPort returns the port the local server is running on
|
||||
func (a *App) GetAPIPort() int {
|
||||
a.logDebug("GetAPIPort -> %d", a.port)
|
||||
return a.port
|
||||
}
|
||||
|
||||
// LogFrontend allows the frontend to log to the terminal
|
||||
func (a *App) LogFrontend(message string) {
|
||||
fmt.Printf("[frontend] %s\n", message)
|
||||
}
|
||||
|
||||
// SaveFile shows a save dialog and writes the content to the selected file
|
||||
func (a *App) SaveFile(filename string, content string) error {
|
||||
a.logDebug("SaveFile filename=%s", filename)
|
||||
filePath, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
|
||||
DefaultFilename: filename,
|
||||
Title: "Save Graph",
|
||||
Filters: []runtime.FileFilter{
|
||||
{
|
||||
DisplayName: "JSON Files (*.json)",
|
||||
Pattern: "*.json",
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if filePath == "" {
|
||||
return nil // Cancelled
|
||||
}
|
||||
|
||||
return os.WriteFile(filePath, []byte(content), 0644)
|
||||
}
|
||||
|
||||
// LoadFile shows an open dialog and returns the content of the selected file
|
||||
func (a *App) LoadFile() (string, error) {
|
||||
a.logDebug("LoadFile")
|
||||
filePath, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{
|
||||
Title: "Open Graph",
|
||||
Filters: []runtime.FileFilter{
|
||||
{
|
||||
DisplayName: "JSON Files (*.json)",
|
||||
Pattern: "*.json",
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if filePath == "" {
|
||||
return "", nil // Cancelled
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(content), nil
|
||||
}
|
||||
|
||||
53
desktop/main.go
Normal file
53
desktop/main.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"os"
|
||||
|
||||
"github.com/wailsapp/wails/v2"
|
||||
"github.com/wailsapp/wails/v2/pkg/options"
|
||||
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
|
||||
)
|
||||
|
||||
//go:embed all:frontend_dist
|
||||
var assets embed.FS
|
||||
|
||||
func debugEnabled() bool {
|
||||
for _, arg := range os.Args[1:] {
|
||||
if arg == "--debug" || arg == "-d" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func main() {
|
||||
debug := debugEnabled()
|
||||
if debug {
|
||||
println("Debug logging enabled")
|
||||
}
|
||||
|
||||
// Create an instance of the app structure
|
||||
app := NewApp(debug)
|
||||
|
||||
// Create application with options
|
||||
err := wails.Run(&options.App{
|
||||
Title: "Linking Tool",
|
||||
Width: 1280,
|
||||
Height: 800,
|
||||
AssetServer: &assetserver.Options{
|
||||
Assets: assets,
|
||||
},
|
||||
BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
|
||||
OnStartup: app.startup,
|
||||
Bind: []interface{}{
|
||||
app,
|
||||
},
|
||||
EnableDefaultContextMenu: true,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
println("Error:", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
141
main.go
Normal file
141
main.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"flag"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
//go:embed build/*
|
||||
var buildAssets embed.FS
|
||||
|
||||
func corsMiddleware(allowedOrigins []string) func(http.HandlerFunc) http.HandlerFunc {
|
||||
return func(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
origin := r.Header.Get("Origin")
|
||||
if origin == "" {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
allowed := false
|
||||
if len(allowedOrigins) == 0 {
|
||||
allowed = true
|
||||
} else {
|
||||
for _, o := range allowedOrigins {
|
||||
if o == "*" || o == origin {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if allowed {
|
||||
w.Header().Set("Access-Control-Allow-Origin", origin)
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||
}
|
||||
|
||||
if r.Method == "OPTIONS" {
|
||||
if allowed {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if !allowed && len(allowedOrigins) > 0 {
|
||||
log.Printf("Blocked CORS request from origin: %s", origin)
|
||||
http.Error(w, "CORS Origin Not Allowed", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
frontendPath := flag.String("frontend", "", "Path to custom frontend build directory (overrides embedded assets)")
|
||||
host := flag.String("host", "0.0.0.0", "Host to bind the server to")
|
||||
port := flag.String("port", "", "Port to listen on (overrides PORT env var)")
|
||||
allowedOriginsStr := flag.String("allowed-origins", os.Getenv("ALLOWED_ORIGINS"), "Comma-separated list of allowed CORS origins")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
var allowedOrigins []string
|
||||
if *allowedOriginsStr != "" {
|
||||
origins := strings.Split(*allowedOriginsStr, ",")
|
||||
for _, o := range origins {
|
||||
allowedOrigins = append(allowedOrigins, strings.TrimSpace(o))
|
||||
}
|
||||
}
|
||||
|
||||
if *port == "" {
|
||||
*port = os.Getenv("PORT")
|
||||
if *port == "" {
|
||||
*port = "8080"
|
||||
}
|
||||
}
|
||||
|
||||
// Middleware chains
|
||||
cors := corsMiddleware(allowedOrigins)
|
||||
|
||||
http.HandleFunc("/api/ping", cors(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(`{"status":"ok"}`))
|
||||
}))
|
||||
|
||||
// Static Assets
|
||||
var staticFS fs.FS
|
||||
if *frontendPath != "" {
|
||||
log.Printf("Using custom frontend from: %s\n", *frontendPath)
|
||||
staticFS = os.DirFS(*frontendPath)
|
||||
} else {
|
||||
sub, err := fs.Sub(buildAssets, "build")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
staticFS = sub
|
||||
}
|
||||
|
||||
fileServer := http.FileServer(http.FS(staticFS))
|
||||
|
||||
// SPA Handler
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
path := strings.TrimPrefix(r.URL.Path, "/")
|
||||
if path == "" {
|
||||
path = "index.html"
|
||||
}
|
||||
|
||||
_, err := staticFS.Open(path)
|
||||
if err != nil {
|
||||
// If file doesn't exist, serve index.html for SPA routing
|
||||
r.URL.Path = "/"
|
||||
}
|
||||
fileServer.ServeHTTP(w, r)
|
||||
})
|
||||
|
||||
addr := net.JoinHostPort(*host, *port)
|
||||
log.Printf("Linking Tool server starting on %s...\n", addr)
|
||||
|
||||
server := &http.Server{
|
||||
Addr: addr,
|
||||
Handler: nil,
|
||||
ReadTimeout: 15 * time.Second,
|
||||
WriteTimeout: 15 * time.Second,
|
||||
IdleTimeout: 60 * time.Second,
|
||||
}
|
||||
|
||||
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user