This commit is contained in:
2026-04-22 23:35:59 +01:00
parent df6c33bc3a
commit bee7869af4
116 changed files with 13552 additions and 0 deletions

140
server/main.go Normal file
View File

@@ -0,0 +1,140 @@
// cctv server entrypoint
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"log/slog"
"net/http/httputil"
"net/url"
"os"
"strings"
"github.com/go-gst/go-gst/gst"
"github.com/joho/godotenv"
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/plugins/migratecmd"
"git.koval.net/cyclane/cctv/server/config"
"git.koval.net/cyclane/cctv/server/ingest"
_ "git.koval.net/cyclane/cctv/server/migrations"
)
var (
ingestService *ingest.Ingest
)
func loadEnvFile(filenames ...string) {
for _, filename := range filenames {
if err := godotenv.Load(filename); err == nil {
log.Printf("Loaded environment variables from %s", filename)
}
}
}
func exportCollectionsJSON(e *core.BootstrapEvent) error {
if err := e.Next(); err != nil {
return err
}
collections, err := e.App.FindAllCollections()
if err != nil {
return fmt.Errorf("failed to export collections: %w", err)
}
f, err := os.OpenFile("collections.json", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return fmt.Errorf("failed to open collections.json: %w", err)
}
defer func() {
if err := f.Close(); err != nil {
log.Printf("Failed to close collections.json: %v", err)
}
}()
encoder := json.NewEncoder(f)
encoder.SetIndent("", "\t")
if err := encoder.Encode(collections); err != nil {
return fmt.Errorf("failed to write collections to JSON: %w", err)
}
return nil
}
func main() {
loadEnvFile(".env.local", ".env", ".env.default")
config, err := config.Load()
if err != nil {
log.Fatalf("Failed to load configuration: %v", err)
}
app := pocketbase.NewWithConfig(pocketbase.Config{
DefaultDev: config.IsDev(),
DefaultDataDir: config.DbDataDir,
DefaultEncryptionEnv: config.DbEncryptionKey,
})
migratecmd.MustRegister(app, app.RootCmd, migratecmd.Config{
// enable auto creation of migration files when making collection changes in the Dashboard
Automigrate: config.IsDev(),
})
if config.IsDev() {
app.OnBootstrap().BindFunc(exportCollectionsJSON)
}
var proxy *httputil.ReverseProxy
if config.ExternalWebApp != "" {
proxyURL, err := url.Parse(config.ExternalWebApp)
if err != nil {
log.Fatalf("Failed to parse external web app URL: %v", err)
}
proxy = httputil.NewSingleHostReverseProxy(proxyURL)
proxy.ErrorLog = slog.NewLogLogger(app.Logger().With("svc", "proxy").Handler(), slog.LevelDebug)
}
ingestCtx, cancelIngestCtx := context.WithCancel(context.Background())
app.OnServe().BindFunc(func(se *core.ServeEvent) error {
gst.Init(nil)
ingestService = ingest.BeginIngest(ingestCtx, app)
if !config.IsDev() {
se.Router.BindFunc(func(e *core.RequestEvent) error {
if strings.HasPrefix(e.Request.URL.Path, "/_/") {
return e.NotFoundError("Page not found", nil)
} else {
return e.Next()
}
})
}
if proxy == nil {
// TODO: serve bundled (static) files
} else {
se.Router.GET("/{path...}", func(e *core.RequestEvent) error {
proxy.ServeHTTP(e.Response, e.Request)
return nil
})
}
registerAPI(se)
return se.Next()
})
app.OnTerminate().BindFunc(func(e *core.TerminateEvent) error {
app.Logger().Info("Shutting down ingest service")
cancelIngestCtx()
return nil
})
app.OnRecordCreateExecute().BindFunc(handleCameraUpdate)
app.OnRecordUpdateExecute().BindFunc(handleCameraUpdate)
if err := app.Start(); err != nil {
app.Logger().Info("Application terminated with error")
cancelIngestCtx()
log.Fatal(err)
}
ingestService.Wait()
}