port "ari is learning!" assets to live go backend. some prep work for further developments for ari melody LIVE
167 lines
4.1 KiB
Go
167 lines
4.1 KiB
Go
package learning
|
|
|
|
import (
|
|
"context"
|
|
"embed"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"codeberg.org/arimelody/ari-is-learning/broadcast"
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
//go:embed public
|
|
var publicFS embed.FS
|
|
//go:embed pages
|
|
var pagesFS embed.FS
|
|
|
|
type (
|
|
ServiceConfig struct {
|
|
TitleFilePath string
|
|
}
|
|
|
|
Service struct {
|
|
cfg ServiceConfig
|
|
|
|
ctx context.Context
|
|
titleFileMutex sync.Mutex
|
|
titleLastModTime time.Time
|
|
titleText string
|
|
titleUpdated chan string
|
|
titleUpdatedBroadcast broadcast.BroadcastChannel[string]
|
|
}
|
|
)
|
|
|
|
func New(ctx context.Context, cfg ServiceConfig) *Service {
|
|
titleUpdated := make(chan string)
|
|
|
|
if _, err := os.Stat(cfg.TitleFilePath); os.IsNotExist(err) {
|
|
log.Printf("Learning title file [%s] does not exist, creating one...", cfg.TitleFilePath)
|
|
os.WriteFile(cfg.TitleFilePath, []byte("Untitled"), 0644)
|
|
} else if err != nil {
|
|
log.Fatalf("Failed to stat title file: %v", err)
|
|
}
|
|
|
|
srv := &Service{
|
|
cfg: cfg,
|
|
ctx: ctx,
|
|
titleFileMutex: sync.Mutex{},
|
|
titleLastModTime: time.Unix(0, 0),
|
|
titleText: "",
|
|
titleUpdated: titleUpdated,
|
|
titleUpdatedBroadcast: broadcast.NewBroadcastChannel(ctx, titleUpdated),
|
|
}
|
|
|
|
return srv
|
|
}
|
|
|
|
func (srv *Service) BindRoutes(group *gin.RouterGroup) {
|
|
group.GET("/public/*path", func(ctx *gin.Context) {
|
|
path := strings.TrimPrefix(ctx.Request.URL.Path, "/learning/")
|
|
http.ServeFileFS(ctx.Writer, ctx.Request, publicFS, path)
|
|
})
|
|
|
|
group.GET("/sse", func(ctx *gin.Context) {
|
|
ctx.Header("connection", "keep-alive")
|
|
|
|
ctx.SSEvent("title-update", srv.titleText)
|
|
|
|
titleUpdated := srv.titleUpdatedBroadcast.Subscribe()
|
|
|
|
ticker := time.NewTicker(10 * time.Millisecond)
|
|
ctx.Stream(func(w io.Writer) bool {
|
|
select {
|
|
case titleText := <-titleUpdated:
|
|
ctx.SSEvent("title-update", titleText)
|
|
case <-ticker.C:
|
|
}
|
|
return true
|
|
})
|
|
|
|
srv.titleUpdatedBroadcast.Cancel(titleUpdated)
|
|
})
|
|
|
|
group.POST("/title", func(ctx *gin.Context) {
|
|
data, err := io.ReadAll(ctx.Request.Body)
|
|
if err != nil {
|
|
log.Printf("Failed to read request body: %v", err)
|
|
ctx.String(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
|
return
|
|
}
|
|
log.Printf("Waiting on title file lock...")
|
|
srv.titleFileMutex.Lock()
|
|
defer srv.titleFileMutex.Unlock()
|
|
log.Printf("Got it!")
|
|
file, err := os.OpenFile(srv.cfg.TitleFilePath, os.O_RDWR | os.O_CREATE | os.O_TRUNC, 0)
|
|
if err != nil {
|
|
log.Printf("Failed to open title file: %v", err)
|
|
ctx.String(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
|
return
|
|
}
|
|
if _, err := file.Write(data); err != nil {
|
|
log.Printf("Failed to write to title file: %v", err)
|
|
ctx.String(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
|
return
|
|
}
|
|
|
|
ctx.String(http.StatusOK, http.StatusText(http.StatusOK))
|
|
})
|
|
|
|
group.GET("/hello", func(ctx *gin.Context) {
|
|
http.ServeFileFS(ctx.Writer, ctx.Request, pagesFS, "pages/index.html")
|
|
})
|
|
group.GET("/brb", func(ctx *gin.Context) {
|
|
http.ServeFileFS(ctx.Writer, ctx.Request, pagesFS, "pages/brb.html")
|
|
})
|
|
group.GET("/bye", func(ctx *gin.Context) {
|
|
http.ServeFileFS(ctx.Writer, ctx.Request, pagesFS, "pages/bye.html")
|
|
})
|
|
}
|
|
|
|
func (srv *Service) Run(ctx context.Context) {
|
|
failed := make(chan error)
|
|
|
|
go func() {
|
|
for {
|
|
srv.titleFileMutex.Lock()
|
|
stat, err := os.Stat(srv.cfg.TitleFilePath)
|
|
if err != nil {
|
|
failed<-fmt.Errorf("Failed to stat title file: %v", err)
|
|
return
|
|
}
|
|
|
|
if srv.titleLastModTime.Before(stat.ModTime()) {
|
|
data, err := os.ReadFile(srv.cfg.TitleFilePath)
|
|
if err != nil {
|
|
failed<-fmt.Errorf("Failed to read title file: %v", err)
|
|
return
|
|
}
|
|
|
|
log.Printf(
|
|
"title updated: \"%s\" -> \"%s\"",
|
|
srv.titleText,
|
|
strings.TrimSpace(string(data)),
|
|
)
|
|
|
|
srv.titleText = strings.TrimSpace(string(data))
|
|
if !srv.titleLastModTime.IsZero() { srv.titleUpdated<-srv.titleText }
|
|
srv.titleLastModTime = stat.ModTime()
|
|
}
|
|
srv.titleFileMutex.Unlock()
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
}
|
|
}()
|
|
|
|
select {
|
|
case err := <-failed:
|
|
log.Fatalf("Learning service error: %v", err)
|
|
case <-ctx.Done():
|
|
}
|
|
}
|