From 0a862c378271046244796ae0d768f5a24e6a57ac Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 9 Nov 2025 19:08:44 +0100 Subject: [PATCH 1/7] Remove phantom token middleware for swagger --- docker-compose.dev.yml | 3 ++- docker-compose.yml | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index db6631d4..9dc3876d 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -91,7 +91,8 @@ services: - images:/app/images labels: - "traefik.enable=true" - - "traefik.http.routers.api.rule=PathPrefix(`/api/`) || PathPrefix(`/swagger`)" + - "traefik.http.routers.swagger.rule=PathPrefix(`/swagger`)" + - "traefik.http.routers.api.rule=PathPrefix(`/api/`)" - "traefik.http.routers.api.middlewares=phantom-token" - "traefik.http.middlewares.phantom-token.forwardauth.address=http://auth:4568/auth/jwt" - "traefik.http.middlewares.phantom-token.forwardauth.authRequestHeaders=Authorization,Cookie,X-Api-Key" diff --git a/docker-compose.yml b/docker-compose.yml index c167991c..a9cced58 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -61,7 +61,8 @@ services: - images:/app/images labels: - "traefik.enable=true" - - "traefik.http.routers.api.rule=PathPrefix(`/api/`) || PathPrefix(`/swagger`)" + - "traefik.http.routers.swagger.rule=PathPrefix(`/swagger`)" + - "traefik.http.routers.api.rule=PathPrefix(`/api/`)" - "traefik.http.routers.api.middlewares=phantom-token" - "traefik.http.middlewares.phantom-token.forwardauth.address=http://auth:4568/auth/jwt" - "traefik.http.middlewares.phantom-token.forwardauth.authRequestHeaders=Authorization,Cookie,X-Api-Key" From 40c13e7ddff7b30101ca510cd5b829072c3410c3 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 9 Nov 2025 19:08:58 +0100 Subject: [PATCH 2/7] Cleanup casing in api extension setup --- api/src/db/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/src/db/index.ts b/api/src/db/index.ts index cd010a7a..e8969cae 100644 --- a/api/src/db/index.ts +++ b/api/src/db/index.ts @@ -120,8 +120,8 @@ export const migrate = async () => { await db.execute( sql.raw(` create extension if not exists pg_trgm; - SET pg_trgm.word_similarity_threshold = 0.4; - ALTER DATABASE "${postgresConfig.database}" SET pg_trgm.word_similarity_threshold = 0.4; + set pg_trgm.word_similarity_threshold = 0.4; + alter database "${postgresConfig.database}" set pg_trgm.word_similarity_threshold = 0.4; `), ); } catch (err: any) { From 8f0fb42b472e8cfb193ee6e9abbe655f42278dee Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 9 Nov 2025 19:09:10 +0100 Subject: [PATCH 3/7] Add `/ready` api to auth --- auth/main.go | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/auth/main.go b/auth/main.go index a163d42a..424bc385 100644 --- a/auth/main.go +++ b/auth/main.go @@ -59,7 +59,27 @@ func (v *Validator) Validate(i any) error { } func (h *Handler) CheckHealth(c echo.Context) error { - return c.JSON(200, struct{ Status string }{Status: "healthy"}) + return c.JSON(200, struct { + Status string `json:"status"` + }{Status: "healthy"}) +} + +func (h *Handler) CheckReady(c echo.Context) error { + _, err := h.rawDb.Exec(c.Request().Context(), "select 1") + + status := "healthy" + db := "healthy" + ret := 200 + if err != nil { + status = "unhealthy" + db = err.Error() + ret = 500 + } + + return c.JSON(ret, struct { + Status string `json:"status"` + Database string `json:"database"` + }{Status: status, Database: db}) } func GetenvOr(env string, def string) string { @@ -135,6 +155,7 @@ func OpenDatabase() (*pgxpool.Pool, error) { type Handler struct { db *dbc.Queries + rawDb *pgxpool.Pool config *Configuration } @@ -210,7 +231,8 @@ func main() { } h := Handler{ - db: dbc.New(db), + db: dbc.New(db), + rawDb: db, } conf, err := LoadConfiguration(h.db) if err != nil { @@ -228,6 +250,7 @@ func main() { })) g.GET("/health", h.CheckHealth) + g.GET("/ready", h.CheckReady) r.GET("/users", h.ListUsers) r.GET("/users/:id", h.GetUser) From 563ae85db1115342909ac5369a28b0c63cd79f14 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 9 Nov 2025 19:14:13 +0100 Subject: [PATCH 4/7] Add `/health` and `/ready` for transcoder --- transcoder/main.go | 1 + transcoder/src/api/health.go | 40 ++++++++++++++++++++++++++++++++++++ transcoder/src/extract.go | 2 +- transcoder/src/keyframes.go | 2 +- transcoder/src/metadata.go | 22 ++++++++++---------- transcoder/src/thumbnails.go | 2 +- 6 files changed, 55 insertions(+), 14 deletions(-) create mode 100644 transcoder/src/api/health.go diff --git a/transcoder/main.go b/transcoder/main.go index 4601c10a..81f42194 100644 --- a/transcoder/main.go +++ b/transcoder/main.go @@ -145,6 +145,7 @@ func main() { g.Use(RequireCorePlayPermission) } + api.RegisterHealthHandlers(e.Group("/video"), metadata.Database) api.RegisterStreamHandlers(g, transcoder) api.RegisterMetadataHandlers(g, metadata) api.RegisterPProfHandlers(e) diff --git a/transcoder/src/api/health.go b/transcoder/src/api/health.go new file mode 100644 index 00000000..21053985 --- /dev/null +++ b/transcoder/src/api/health.go @@ -0,0 +1,40 @@ +package api + +import ( + "github.com/jackc/pgx/v5/pgxpool" + "github.com/labstack/echo/v4" +) + +type health struct { + db *pgxpool.Pool +} + +func RegisterHealthHandlers(e *echo.Group, db *pgxpool.Pool) { + h := health{db} + e.GET("/health", h.CheckHealth) + e.GET("/ready", h.CheckReady) +} + +func (h *health) CheckHealth(c echo.Context) error { + return c.JSON(200, struct { + Status string `json:"status"` + }{Status: "healthy"}) +} + +func (h *health) CheckReady(c echo.Context) error { + _, err := h.db.Exec(c.Request().Context(), "select 1") + + status := "healthy" + db := "healthy" + ret := 200 + if err != nil { + status = "unhealthy" + ret = 500 + db = err.Error() + } + + return c.JSON(ret, struct { + Status string `json:"status"` + Database string `json:"database"` + }{Status: status, Database: db}) +} diff --git a/transcoder/src/extract.go b/transcoder/src/extract.go index 90199679..1d5634a3 100644 --- a/transcoder/src/extract.go +++ b/transcoder/src/extract.go @@ -27,7 +27,7 @@ func (s *MetadataService) ExtractSubs(ctx context.Context, info *MediaInfo) (any log.Printf("Couldn't extract subs: %v", err) return set(nil, err) } - _, err = s.database.Exec(ctx, `update gocoder.info set ver_extract = $2 where sha = $1`, info.Sha, ExtractVersion) + _, err = s.Database.Exec(ctx, `update gocoder.info set ver_extract = $2 where sha = $1`, info.Sha, ExtractVersion) return set(nil, err) } diff --git a/transcoder/src/keyframes.go b/transcoder/src/keyframes.go index c747296c..5f76ed08 100644 --- a/transcoder/src/keyframes.go +++ b/transcoder/src/keyframes.go @@ -157,7 +157,7 @@ func (s *MetadataService) GetKeyframes(info *MediaInfo, isVideo bool, idx uint32 } kf.info.ready.Wait() - tx, _ := s.database.Begin(ctx) + tx, _ := s.Database.Begin(ctx) tx.Exec( ctx, fmt.Sprintf(`update %s set keyframes = $3 where sha = $1 and idx = $2`, table), diff --git a/transcoder/src/metadata.go b/transcoder/src/metadata.go index a4512b5b..312c9542 100644 --- a/transcoder/src/metadata.go +++ b/transcoder/src/metadata.go @@ -21,7 +21,7 @@ import ( ) type MetadataService struct { - database *pgxpool.Pool + Database *pgxpool.Pool lock RunLock[string, *MediaInfo] thumbLock RunLock[string, any] extractLock RunLock[string, any] @@ -43,7 +43,7 @@ func NewMetadataService() (*MetadataService, error) { if err != nil { return nil, fmt.Errorf("failed to setup database: %w", err) } - s.database = db + s.Database = db storage, err := s.setupStorage(ctx) if err != nil { @@ -55,8 +55,8 @@ func NewMetadataService() (*MetadataService, error) { } func (s *MetadataService) Close() error { - if s.database != nil { - s.database.Close() + if s.Database != nil { + s.Database.Close() } if s.storage != nil { @@ -174,7 +174,7 @@ func (s *MetadataService) GetMetadata(ctx context.Context, path string, sha stri for _, audio := range ret.Audios { audio.Keyframes = nil } - tx, err := s.database.Begin(ctx) + tx, err := s.Database.Begin(ctx) if err != nil { return nil, err } @@ -191,7 +191,7 @@ func (s *MetadataService) GetMetadata(ctx context.Context, path string, sha stri } func (s *MetadataService) getMetadata(ctx context.Context, path string, sha string) (*MediaInfo, error) { - rows, _ := s.database.Query( + rows, _ := s.Database.Query( ctx, `select i.sha, i.path, i.extension, i.mime_codec, i.size, i.duration, i.container, i.fonts, @@ -214,7 +214,7 @@ func (s *MetadataService) getMetadata(ctx context.Context, path string, sha stri return nil, err } - rows, _ = s.database.Query( + rows, _ = s.Database.Query( ctx, `select * from gocoder.videos as v where v.sha=$1`, sha, @@ -224,7 +224,7 @@ func (s *MetadataService) getMetadata(ctx context.Context, path string, sha stri return nil, err } - rows, _ = s.database.Query( + rows, _ = s.Database.Query( ctx, `select * from gocoder.audios as a where a.sha=$1`, sha, @@ -234,7 +234,7 @@ func (s *MetadataService) getMetadata(ctx context.Context, path string, sha stri return nil, err } - rows, _ = s.database.Query( + rows, _ = s.Database.Query( ctx, `select * from gocoder.subtitles as s where s.sha=$1`, sha, @@ -259,7 +259,7 @@ func (s *MetadataService) getMetadata(ctx context.Context, path string, sha stri fmt.Printf("Couldn't find external subtitles: %v", err) } - rows, _ = s.database.Query( + rows, _ = s.Database.Query( ctx, `select * from gocoder.chapters as c where c.sha=$1`, sha, @@ -282,7 +282,7 @@ func (s *MetadataService) storeFreshMetadata(ctx context.Context, path string, s return set(nil, err) } - tx, err := s.database.Begin(ctx) + tx, err := s.Database.Begin(ctx) if err != nil { return set(ret, err) } diff --git a/transcoder/src/thumbnails.go b/transcoder/src/thumbnails.go index 199ab33a..2489cb48 100644 --- a/transcoder/src/thumbnails.go +++ b/transcoder/src/thumbnails.go @@ -77,7 +77,7 @@ func (s *MetadataService) ExtractThumbs(ctx context.Context, path string, sha st if err != nil { return set(nil, err) } - _, err = s.database.Exec(ctx, `update gocoder.info set ver_thumbs = $2 where sha = $1`, sha, ThumbsVersion) + _, err = s.Database.Exec(ctx, `update gocoder.info set ver_thumbs = $2 where sha = $1`, sha, ThumbsVersion) return set(nil, err) } From 61b38d5b030e0ed15ef04fc627511a74d7fb6b7f Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 9 Nov 2025 19:21:17 +0100 Subject: [PATCH 5/7] Add `/ready` for api --- api/src/base.ts | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/api/src/base.ts b/api/src/base.ts index 5f8d0dc6..cbd91681 100644 --- a/api/src/base.ts +++ b/api/src/base.ts @@ -14,6 +14,7 @@ import { showsH } from "./controllers/shows/shows"; import { staffH } from "./controllers/staff"; import { studiosH } from "./controllers/studios"; import { videosReadH, videosWriteH } from "./controllers/videos"; +import { db } from "./db"; import type { KError } from "./models/error"; export const base = new Elysia({ name: "base" }) @@ -58,6 +59,33 @@ export const base = new Elysia({ name: "base" }) detail: { description: "Check if the api is healthy." }, response: { 200: t.Object({ status: t.Literal("healthy") }) }, }) + .get( + "/ready", + async ({ status }) => { + try { + await db.execute("select 1"); + return { status: "healthy", database: "healthy" } as const; + } catch (e) { + return status(500, { + status: "unhealthy", + database: e, + }); + } + }, + { + detail: { description: "Check if the api is healthy." }, + response: { + 200: t.Object({ + status: t.Literal("healthy"), + database: t.Literal("healthy"), + }), + 500: t.Object({ + status: t.Literal("unhealthy"), + database: t.Any(), + }), + }, + }, + ) .as("global"); export const prefix = "/api"; From a95bbcb6ebf00581ce0d5033e9c2ca9affcdfece Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 9 Nov 2025 19:27:15 +0100 Subject: [PATCH 6/7] Fix `last_seen` update on auth --- auth/dbc/users.sql.go | 2 +- auth/sql/queries/users.sql | 2 +- auth/tests/invalid-password.hurl | 4 ++-- auth/users.go | 3 +++ 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/auth/dbc/users.sql.go b/auth/dbc/users.sql.go index 1426d231..701e171c 100644 --- a/auth/dbc/users.sql.go +++ b/auth/dbc/users.sql.go @@ -259,7 +259,7 @@ const touchUser = `-- name: TouchUser :exec update keibi.users set - last_used = now()::timestamptz + last_seen = now()::timestamptz where pk = $1 ` diff --git a/auth/sql/queries/users.sql b/auth/sql/queries/users.sql index bc888803..292d6d3d 100644 --- a/auth/sql/queries/users.sql +++ b/auth/sql/queries/users.sql @@ -47,7 +47,7 @@ limit 1; update keibi.users set - last_used = now()::timestamptz + last_seen = now()::timestamptz where pk = $1; diff --git a/auth/tests/invalid-password.hurl b/auth/tests/invalid-password.hurl index 7817a09a..c9fde22b 100644 --- a/auth/tests/invalid-password.hurl +++ b/auth/tests/invalid-password.hurl @@ -19,7 +19,7 @@ GET {{host}}/users/me Authorization: Bearer {{jwt}} HTTP 200 [Captures] -register_info: body +register_id: jsonpath "$.id" [Asserts] jsonpath "$.username" == "login-user" @@ -48,7 +48,7 @@ Authorization: Bearer {{jwt}} HTTP 200 [Asserts] jsonpath "$.username" == "login-user" -body == {{register_info}} +jsonpath "$.id" == {{register_id}} # Invalid password login diff --git a/auth/users.go b/auth/users.go index 81a75c5d..29acf3f1 100644 --- a/auth/users.go +++ b/auth/users.go @@ -190,6 +190,9 @@ func (h *Handler) GetMe(c echo.Context) error { if err != nil { return err } + if len(dbuser) == 0 { + return c.JSON(403, "Invalid jwt token, couldn't find user.") + } user := MapDbUser(&dbuser[0].User) for _, oidc := range dbuser { From b4c85f3f28de3e1d899427ac380556341a42e4f1 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 9 Nov 2025 19:45:47 +0100 Subject: [PATCH 7/7] Add `/health` and `/ready` for scanner --- scanner/scanner/database.py | 6 ++++++ scanner/scanner/routers/routes.py | 21 ++++++++++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/scanner/scanner/database.py b/scanner/scanner/database.py index 918ae0c7..e5184339 100644 --- a/scanner/scanner/database.py +++ b/scanner/scanner/database.py @@ -49,6 +49,12 @@ async def get_db(): yield cast(Connection, db) +# because https://github.com/fastapi/fastapi/pull/10353 +async def get_db_fapi(): + async with get_db() as db: + yield db + + async def migrate(migrations_dir="./migrations"): async with get_db() as db: _ = await db.execute( diff --git a/scanner/scanner/routers/routes.py b/scanner/scanner/routers/routes.py index 90808df5..41c30d23 100644 --- a/scanner/scanner/routers/routes.py +++ b/scanner/scanner/routers/routes.py @@ -1,6 +1,9 @@ from typing import Annotated -from fastapi import APIRouter, BackgroundTasks, Security +from asyncpg import Connection +from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Security + +from scanner.database import get_db_fapi from ..fsscan import create_scanner from ..jwt import validate_bearer @@ -26,3 +29,19 @@ async def trigger_scan( await scanner.scan() tasks.add_task(run) + + +@router.get("/health") +def get_health(): + return {"status": "healthy"} + + +@router.get("/ready") +async def get_ready(db: Annotated[Connection, Depends(get_db_fapi)]): + try: + _ = await db.execute("select 1") + return {"status": "healthy", "database": "healthy"} + except Exception as e: + raise HTTPException( + status_code=500, detail={"status": "unhealthy", "database": str(e)} + )