first commit! 🎉
port "ari is learning!" assets to live go backend. some prep work for further developments for ari melody LIVE
This commit is contained in:
commit
6927d54cbd
12 changed files with 675 additions and 0 deletions
167
learning/learning.go
Normal file
167
learning/learning.go
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
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():
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue