refactor: internal/web importable; cmd/openfsd-web; Docker embed-safe

Move web package; no init panic; Dockerfile_web builds cmd/openfsd-web as fsdweb.
This commit is contained in:
Reese Norris
2026-07-12 20:54:54 -04:00
parent a2045c1c0d
commit 65ec62a738
41 changed files with 74 additions and 45 deletions

7
.gitignore vendored
View File

@@ -4,5 +4,8 @@
*.db
**tmp**
build-and-push.sh
cover.out
coverage.out
# Local binaries
/openfsd-web
/fsdweb
/fsd

View File

@@ -13,8 +13,7 @@ COPY . .
# Cache builds
ENV GOCACHE=/root/.cache/go-build
RUN --mount=type=cache,target="/root/.cache/go-build" \
cd web && \
CGO_ENABLED=0 go build -o /go/bin/fsdweb
CGO_ENABLED=0 go build -o /go/bin/fsdweb ./cmd/openfsd-web
FROM alpine:latest

View File

@@ -50,7 +50,7 @@ The preferred way to run openfsd is using **Docker** and **Docker Compose**. See
## API
The web server exposes APIs under `/api/v1` for authentication, user management, and configuration. Although a basic web interface is provided, users are encouraged to call this API from their own external applications. See the [API](https://github.com/renorris/openfsd/tree/main/web) documentation.
The web server exposes APIs under `/api/v1` for authentication, user management, and configuration. Although a basic web interface is provided, users are encouraged to call this API from their own external applications. See the [API](https://github.com/renorris/openfsd/tree/main/internal/web) documentation.
## Docs

View File

@@ -2,17 +2,17 @@ package main
import (
"context"
"log/slog"
"os"
"os/signal"
"github.com/renorris/openfsd/internal/web"
)
func main() {
ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt)
server, err := NewDefaultServer(ctx)
if err != nil {
panic(err)
if err := web.Main(ctx); err != nil {
slog.Error(err.Error())
os.Exit(1)
}
server.Run(ctx)
}

View File

@@ -30,6 +30,7 @@ func TestImportGraph(t *testing.T) {
postofficePkg,
"github.com/renorris/openfsd/fsd",
"github.com/renorris/openfsd/web",
"github.com/renorris/openfsd/internal/web",
"github.com/renorris/openfsd/internal/server",
})
@@ -37,6 +38,7 @@ func TestImportGraph(t *testing.T) {
assertNoDeps(t, postofficePkg, []string{
"github.com/renorris/openfsd/fsd",
"github.com/renorris/openfsd/web",
"github.com/renorris/openfsd/internal/web",
"github.com/renorris/openfsd/internal/server",
})
}

View File

@@ -1,4 +1,4 @@
package main
package web
import (
"net/http"

View File

@@ -1,4 +1,4 @@
package main
package web
import (
"encoding/json"

View File

@@ -1,4 +1,4 @@
package main
package web
import (
"database/sql"

View File

@@ -1,4 +1,4 @@
package main
package web
import (
"errors"

View File

@@ -1,4 +1,4 @@
package main
package web
import (
"bytes"
@@ -25,23 +25,22 @@ import (
//go:embed data_templates/status.txt
var statusTxtRawTemplate string
var statusTxtTemplate *template.Template
//go:embed data_templates/servers.txt
var serversTxtRawTemplate string
var serversTxtTemplate *template.Template
func init() {
var err error
statusTxtTemplate = template.New("statustxt")
if statusTxtTemplate, err = statusTxtTemplate.Parse(statusTxtRawTemplate); err != nil {
panic("Unable to parse status.txt template: " + err.Error())
// parseDataTemplates parses the embedded status/servers text templates.
// Called from NewServer so construction fails with an error instead of init panic.
func parseDataTemplates() (statusTxt, serversTxt *template.Template, err error) {
statusTxt, err = template.New("statustxt").Parse(statusTxtRawTemplate)
if err != nil {
return nil, nil, err
}
serversTxtTemplate = template.New("serverstxt")
if serversTxtTemplate, err = serversTxtTemplate.Parse(serversTxtRawTemplate); err != nil {
panic("Unable to parse servers.txt template: " + err.Error())
serversTxt, err = template.New("serverstxt").Parse(serversTxtRawTemplate)
if err != nil {
return nil, nil, err
}
return statusTxt, serversTxt, nil
}
func (s *Server) handleGetStatusTxt(c *gin.Context) {
@@ -51,7 +50,7 @@ func (s *Server) handleGetStatusTxt(c *gin.Context) {
}
// Generate a new status.txt
statusTxt, err := generateStatusTxt(baseURL)
statusTxt, err := s.generateStatusTxt(baseURL)
if err != nil {
c.Writer.WriteHeader(http.StatusInternalServerError)
c.Writer.WriteString("Error generating status.txt")
@@ -63,7 +62,7 @@ func (s *Server) handleGetStatusTxt(c *gin.Context) {
c.Writer.WriteString(statusTxt)
}
func generateStatusTxt(baseURL string) (txt string, err error) {
func (s *Server) generateStatusTxt(baseURL string) (txt string, err error) {
type TemplateData struct {
ApiServerBaseURL string
}
@@ -72,7 +71,7 @@ func generateStatusTxt(baseURL string) (txt string, err error) {
buf := bytes.Buffer{}
buf.Grow(1024)
if err = statusTxtTemplate.Execute(&buf, &tmplData); err != nil {
if err = s.statusTxtTemplate.Execute(&buf, &tmplData); err != nil {
return
}
@@ -217,7 +216,7 @@ func (s *Server) generateServersTxt() (txt string, err error) {
buf := bytes.Buffer{}
buf.Grow(1024)
if err = serversTxtTemplate.Execute(&buf, &tmplData); err != nil {
if err = s.serversTxtTemplate.Execute(&buf, &tmplData); err != nil {
return
}

View File

@@ -1,4 +1,4 @@
package main
package web
import (
"context"

View File

@@ -1,4 +1,4 @@
package main
package web
import (
"github.com/gin-gonic/gin"

View File

@@ -1,4 +1,4 @@
package main
package web
import (
"bytes"

15
internal/web/main.go Normal file
View File

@@ -0,0 +1,15 @@
package web
import (
"context"
)
// Main constructs the default web server and runs it until ctx is cancelled.
// It is the library entrypoint used by cmd/openfsd-web.
func Main(ctx context.Context) error {
server, err := NewDefaultServer(ctx)
if err != nil {
return err
}
return server.Run(ctx)
}

View File

@@ -1,4 +1,4 @@
package main
package web
import (
"embed"

View File

@@ -1,17 +1,21 @@
package main
package web
import (
"context"
"database/sql"
"fmt"
"github.com/renorris/openfsd/internal/db"
"log/slog"
"net"
"text/template"
"github.com/renorris/openfsd/internal/db"
)
type Server struct {
cfg *ServerConfig
dbRepo *db.Repositories
cfg *ServerConfig
dbRepo *db.Repositories
statusTxtTemplate *template.Template
serversTxtTemplate *template.Template
}
func NewDefaultServer(ctx context.Context) (server *Server, err error) {
@@ -44,9 +48,16 @@ func NewDefaultServer(ctx context.Context) (server *Server, err error) {
}
func NewServer(cfg *ServerConfig, dbRepo *db.Repositories) (server *Server, err error) {
statusTxt, serversTxt, err := parseDataTemplates()
if err != nil {
return nil, fmt.Errorf("parse data templates: %w", err)
}
server = &Server{
cfg: cfg,
dbRepo: dbRepo,
cfg: cfg,
dbRepo: dbRepo,
statusTxtTemplate: statusTxt,
serversTxtTemplate: serversTxt,
}
return

View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -1,4 +1,4 @@
package main
package web
import (
"bytes"

View File

@@ -1,4 +1,4 @@
package main
package web
import (
"database/sql"

View File

@@ -1,4 +1,4 @@
package main
package web
// safeStr returns an empty string if the pointer is nil, or the underlying string value if not nil.
func safeStr(str *string) string {

View File

@@ -6,4 +6,4 @@ start /b cmd /c "set DATABASE_AUTO_MIGRATE=true&& set DATABASE_SOURCE_NAME=openf
powershell -Command "$ProgressPreference = 'SilentlyContinue'; while (-not (Test-NetConnection -ComputerName localhost -Port 13618 -InformationLevel Quiet)) { Start-Sleep -Seconds 1 }" >nul 2>&1
cmd /c "cd web&& set FSD_HTTP_SERVICE_ADDRESS=http://localhost:13618&& set DATABASE_SOURCE_NAME=../openfsd.db?_pragma=busy_timeout(5000)^&_pragma=journal_mode(WAL)&& go run ."
cmd /c "set FSD_HTTP_SERVICE_ADDRESS=http://localhost:13618&& set DATABASE_SOURCE_NAME=openfsd.db?_pragma=busy_timeout(5000)^&_pragma=journal_mode(WAL)&& go run ./cmd/openfsd-web"