141 lines
3.5 KiB
Go
141 lines
3.5 KiB
Go
// 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()
|
|
}
|