Backend: Refactor middleware naming and improve code comments #5235

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer 2025-09-30 23:25:53 +02:00
parent 873454ddd2
commit 838adee3eb
54 changed files with 171 additions and 34 deletions

View file

@ -15,12 +15,12 @@ import (
// ClusterMetrics returns lightweight metrics about the cluster.
//
// @Summary temporary cluster metrics (counts only)
// @Id ClusterMetrics
// @Id ClusterMetrics
// @Tags Cluster
// @Produce json
// @Success 200 {object} cluster.MetricsResponse
// @Failure 401,403,429 {object} i18n.Response
// @Router /api/v1/cluster/metrics [get]
// @Router /api/v1/cluster/metrics [get]
func ClusterMetrics(router *gin.RouterGroup) {
router.GET("/cluster/metrics", func(c *gin.Context) {
s := Auth(c, acl.ResourceCluster, acl.ActionView)

View file

@ -73,6 +73,6 @@ func OIDCLogin(router *gin.RouterGroup) {
r.Success()
// Handle OIDC login request.
provider.AuthCodeUrlHandler(c)
provider.AuthURLHandler(c)
})
}

View file

@ -773,6 +773,12 @@
"UsageInfo": {
"type": "boolean"
},
"VisionFilter": {
"type": "string"
},
"VisionSchedule": {
"type": "string"
},
"WakeupInterval": {
"$ref": "#/definitions/time.Duration"
},
@ -4460,14 +4466,16 @@
"url",
"images",
"vision",
"ollama"
"ollama",
"openai"
],
"type": "string",
"x-enum-varnames": [
"ApiFormatUrl",
"ApiFormatImages",
"ApiFormatVision",
"ApiFormatOllama"
"ApiFormatOllama",
"ApiFormatOpenAI"
]
},
"vision.ApiRequest": {
@ -4704,12 +4712,26 @@
},
"vision.Model": {
"properties": {
"Run": {
"allOf": [
{
"$ref": "#/definitions/vision.RunType"
}
],
"description": "\"auto\", \"never\", \"manual\", \"always\", \"newly-indexed\", \"on-schedule\""
},
"default": {
"type": "boolean"
},
"disabled": {
"type": "boolean"
},
"engine": {
"$ref": "#/definitions/vision.ModelEngine"
},
"format": {
"type": "string"
},
"name": {
"type": "string"
},
@ -4722,6 +4744,12 @@
"resolution": {
"type": "integer"
},
"schema": {
"type": "string"
},
"schemaFile": {
"type": "string"
},
"service": {
"$ref": "#/definitions/vision.Service"
},
@ -4740,19 +4768,77 @@
},
"type": "object"
},
"vision.ModelEngine": {
"enum": [
"vision",
"tensorflow",
"local"
],
"type": "string",
"x-enum-varnames": [
"EngineVision",
"EngineTensorFlow",
"EngineLocal"
]
},
"vision.ModelType": {
"enum": [
"labels",
"nsfw",
"face",
"caption"
"caption",
"generate"
],
"type": "string",
"x-enum-varnames": [
"ModelTypeLabels",
"ModelTypeNsfw",
"ModelTypeFace",
"ModelTypeCaption"
"ModelTypeCaption",
"ModelTypeGenerate"
]
},
"vision.RunType": {
"enum": [
"",
"never",
"manual",
"always",
"newly-indexed",
"on-demand",
"on-schedule",
"on-index"
],
"type": "string",
"x-enum-comments": {
"RunAlways": "Run manually, on-schedule, on-demand, and on-index.",
"RunAuto": "Automatically decide when to run based on model type and configuration.",
"RunManual": "Only run manually e.g. with the \"vision run\" command.",
"RunNever": "Never run the model.",
"RunNewlyIndexed": "Run manually amd for newly-indexed pictures.",
"RunOnDemand": "Run manually, for newly-indexed pictures, and on configured schedule.",
"RunOnIndex": "Run manually and on-index.",
"RunOnSchedule": "Run manually and on-schedule."
},
"x-enum-descriptions": [
"Automatically decide when to run based on model type and configuration.",
"Never run the model.",
"Only run manually e.g. with the \"vision run\" command.",
"Run manually, on-schedule, on-demand, and on-index.",
"Run manually amd for newly-indexed pictures.",
"Run manually, for newly-indexed pictures, and on configured schedule.",
"Run manually and on-schedule.",
"Run manually and on-index."
],
"x-enum-varnames": [
"RunAuto",
"RunNever",
"RunManual",
"RunAlways",
"RunNewlyIndexed",
"RunOnDemand",
"RunOnSchedule",
"RunOnIndex"
]
},
"vision.Service": {

View file

@ -27,7 +27,8 @@ type Client struct {
insecure bool
}
// NewClient creates and returns a new OpenID Connect (OIDC) Relying Party Client based on the specified parameters.
// NewClient creates a new OpenID Connect (OIDC) Relying Party (RP) client using the provided discovery URL,
// client credentials, scopes, and site URL.
func NewClient(issuerUri *url.URL, oidcClient, oidcSecret, oidcScopes, siteUrl string, insecure bool) (result *Client, err error) {
if issuerUri == nil {
err = errors.New("issuer uri required")
@ -126,8 +127,8 @@ func NewClient(issuerUri *url.URL, oidcClient, oidcSecret, oidcScopes, siteUrl s
}, nil
}
// AuthCodeUrlHandler redirects a browser to the login page of the configured OIDC identity provider.
func (c *Client) AuthCodeUrlHandler(ctx *gin.Context) {
// AuthURLHandler redirects a browser to the login page of the configured OIDC identity provider.
func (c *Client) AuthURLHandler(ctx *gin.Context) {
handle := rp.AuthURLHandler(rnd.State, c)
handle(ctx.Writer, ctx.Request)
}

View file

@ -74,7 +74,7 @@ func TestNewClient(t *testing.T) {
assert.Error(t, err)
assert.Nil(t, client)
})
t.Run("EmptyRedirectUrl", func(t *testing.T) {
t.Run("EmptyRedirectURL", func(t *testing.T) {
uri, parseErr := url.Parse("http://dummy-oidc:9998")
assert.NoError(t, parseErr)

View file

@ -7,13 +7,8 @@ import (
"github.com/photoprism/photoprism/internal/event"
)
// HttpClient represents a client that makes HTTP requests.
//
// NOTE: Timeout specifies a time limit for requests made by
// this Client. The timeout includes connection time, any
// redirects, and reading the response body. The timer remains
// running after Get, Head, Post, or Do return and will
// interrupt reading of the Response.Body.
// HttpClient returns an HTTP client tailored for OIDC requests. When debug is true, it wraps the
// default transport with a LoggingRoundTripper and keeps a 30s timeout.
func HttpClient(debug bool) *http.Client {
if debug {
return &http.Client{
@ -25,7 +20,7 @@ func HttpClient(debug bool) *http.Client {
return &http.Client{Timeout: 30 * time.Second}
}
// LoggingRoundTripper specifies the http.RoundTripper interface.
// LoggingRoundTripper wraps an http.RoundTripper and emits audit logs for OIDC requests.
type LoggingRoundTripper struct {
proxy http.RoundTripper
}

View file

@ -8,7 +8,7 @@ import (
"github.com/photoprism/photoprism/internal/config"
)
// RedirectURL returns the redirect URL for authentication via OIDC based on the specified site URL.
// RedirectURL builds the OIDC redirect callback from the provided site URL.
func RedirectURL(siteUrl string) (string, error) {
if siteUrl == "" {
return "", errors.New("site url required")

View file

@ -1,5 +1,6 @@
package photoprism
// Worker action identifiers shared across import/index pipelines.
const (
ActionIndex = "index"
ActionAutoIndex = "autoindex"

View file

@ -1,3 +1,4 @@
package backup
// SqlBackupFileNamePattern matches YYYY-MM-DD.sql database backup filenames.
const SqlBackupFileNamePattern = "[2-9][0-9][0-9][0-9]-[0-1][0-9]-[0-3][0-9].sql"

View file

@ -1,5 +1,6 @@
package photoprism
// CleanUpOptions controls cleanup worker behaviour.
type CleanUpOptions struct {
Dry bool
}

View file

@ -6,6 +6,7 @@ import (
var conf *config.Config
// SetConfig initialises package-level access to the shared Config.
func SetConfig(c *config.Config) {
if c == nil {
panic("config is missing")
@ -14,6 +15,7 @@ func SetConfig(c *config.Config) {
conf = c
}
// Config returns the shared Config, panicking if it has not been set.
func Config() *config.Config {
if conf == nil {
panic("config is missing")

View file

@ -6,12 +6,14 @@ import (
"github.com/photoprism/photoprism/pkg/clean"
)
// ConvertJob represents a single media conversion task.
type ConvertJob struct {
force bool
file *MediaFile
convert *Convert
}
// ConvertWorker processes ConvertJob messages serially on a worker goroutine.
func ConvertWorker(jobs <-chan ConvertJob) {
convertErr := func(err error, job ConvertJob) {
fileName := job.file.RelName(job.convert.conf.OriginalsPath())

View file

@ -6,6 +6,7 @@ import (
"github.com/photoprism/photoprism/internal/ffmpeg/encode"
)
// Cached binary discovery results.
var (
YtDlpBin = ""
FFmpegBin = ""

View file

@ -51,6 +51,7 @@ type Options struct {
noInfoDownload bool
}
// DownloadOptions controls how yt-dlp downloads media on behalf of PhotoPrism.
type DownloadOptions struct {
Filter string // Download format matched by filter (usually a format id or quality designator).
AudioFormats string // --audio-formats Download audio using formats (best, aac, alac, flac, m4a, mp3, opus, vorbis, wav).
@ -63,6 +64,7 @@ type DownloadOptions struct {
Output string
}
// DownloadWithOptions runs yt-dlp with the provided download options.
func (result Metadata) DownloadWithOptions(
ctx context.Context,
options DownloadOptions,

View file

@ -1,5 +1,6 @@
package dl
// Thumbnail models a thumbnail entry returned by yt-dlp metadata.
type Thumbnail struct {
ID string `json:"id"`
URL string `json:"url"`

View file

@ -14,6 +14,7 @@ const (
TypeChannel
)
// TypeFromString translates string identifiers to download Type values.
var TypeFromString = map[string]Type{
"any": TypeAny,
"single": TypeSingle,

View file

@ -18,7 +18,7 @@ func (w *Faces) Optimize() (result FacesOptimizeResult, err error) {
return w.OptimizeFor("")
}
// Optimize optimizes the face lookup table for the specified subj_uid or "" for all subjects
// OptimizeFor optimizes the face lookup table for the given subject UID (or all when empty).
func (w *Faces) OptimizeFor(subj_uid string) (result FacesOptimizeResult, err error) {
if w.Disabled() {
return result, fmt.Errorf("face recognition is disabled")

View file

@ -2,6 +2,7 @@ package photoprism
import "github.com/photoprism/photoprism/internal/ai/face"
// FacesOptions controls face clustering jobs.
type FacesOptions struct {
Force bool
Threshold int

View file

@ -12,6 +12,7 @@ func initCleanUp() {
services.CleanUp = photoprism.NewCleanUp(Config())
}
// CleanUp returns the singleton cleanup worker service instance.
func CleanUp() *photoprism.CleanUp {
onceCleanUp.Do(initCleanUp)

View file

@ -12,6 +12,7 @@ func initConvert() {
services.Convert = photoprism.NewConvert(Config())
}
// Convert returns the singleton media conversion service instance.
func Convert() *photoprism.Convert {
onceConvert.Do(initConvert)

View file

@ -13,6 +13,7 @@ func initCoverCache() {
services.CoverCache = gc.New(time.Hour, 10*time.Minute)
}
// CoverCache returns the shared album cover cache instance.
func CoverCache() *gc.Cache {
onceCoverCache.Do(initCoverCache)

View file

@ -12,6 +12,7 @@ func initFaces() {
services.Faces = photoprism.NewFaces(Config())
}
// Faces returns the singleton face-recognition service instance.
func Faces() *photoprism.Faces {
onceFaces.Do(initFaces)

View file

@ -12,6 +12,7 @@ func initFiles() {
services.Files = photoprism.NewFiles()
}
// Files returns the shared indexed-files cache instance.
func Files() *photoprism.Files {
onceFiles.Do(initFiles)

View file

@ -13,6 +13,7 @@ func initFolderCache() {
services.FolderCache = gc.New(time.Minute*15, 5*time.Minute)
}
// FolderCache returns the shared folder listing cache instance.
func FolderCache() *gc.Cache {
onceFolderCache.Do(initFolderCache)

View file

@ -12,6 +12,7 @@ func initImport() {
services.Import = photoprism.NewImport(Config(), Index(), Convert())
}
// Import returns the singleton import service instance.
func Import() *photoprism.Import {
onceImport.Do(initImport)

View file

@ -12,6 +12,7 @@ func initMoments() {
services.Moments = photoprism.NewMoments(Config())
}
// Moments returns the singleton moments service instance.
func Moments() *photoprism.Moments {
onceMoments.Do(initMoments)

View file

@ -19,6 +19,7 @@ func initOidc() {
)
}
// OIDC returns the singleton OIDC client instance.
func OIDC() *oidc.Client {
onceOidc.Do(initOidc)

View file

@ -12,6 +12,7 @@ func initPhotos() {
services.Photos = photoprism.NewPhotos()
}
// Photos returns the shared map of indexed photos.
func Photos() *photoprism.Photos {
oncePhotos.Do(initPhotos)

View file

@ -12,6 +12,7 @@ func initPlaces() {
services.Places = photoprism.NewPlaces(Config())
}
// Places returns the singleton place lookup service instance.
func Places() *photoprism.Places {
oncePlaces.Do(initPlaces)

View file

@ -12,6 +12,7 @@ func initPurge() {
services.Purge = photoprism.NewPurge(Config(), Files())
}
// Purge returns the singleton purge worker instance.
func Purge() *photoprism.Purge {
oncePurge.Do(initPurge)

View file

@ -12,6 +12,7 @@ func initQuery() {
services.Query = query.New(Config().Db())
}
// Query returns the singleton query helper instance.
func Query() *query.Query {
onceQuery.Do(initQuery)

View file

@ -60,6 +60,7 @@ var services struct {
JWTVerifier *clusterjwt.Verifier
}
// SetConfig stores the shared Config for service constructors.
func SetConfig(c *config.Config) {
if c == nil {
log.Panic("panic: argument is nil in get.SetConfig(c *config.Config)")
@ -73,6 +74,7 @@ func SetConfig(c *config.Config) {
photoprism.SetConfig(c)
}
// Config returns the shared Config used by the service registry.
func Config() *config.Config {
if conf == nil {
log.Panic("panic: conf is nil in get.Config()")

View file

@ -12,6 +12,7 @@ func initSession() {
services.Session = session.New(Config())
}
// Session returns the singleton session manager instance.
func Session() *session.Session {
onceSession.Do(initSession)

View file

@ -13,6 +13,7 @@ func initThumbCache() {
services.ThumbCache = gc.New(time.Hour*24, 10*time.Minute)
}
// ThumbCache returns the thumbnail cache used by the UI.
func ThumbCache() *gc.Cache {
onceThumbCache.Do(initThumbCache)

View file

@ -12,6 +12,7 @@ func initThumbs() {
services.Thumbs = photoprism.NewThumbs(Config())
}
// Thumbs returns the singleton thumbs service instance.
func Thumbs() *photoprism.Thumbs {
onceThumbs.Do(initThumbs)

View file

@ -10,6 +10,7 @@ import (
"github.com/photoprism/photoprism/pkg/fs"
)
// ImportJob describes a media import task pulled from the worker queue.
type ImportJob struct {
FileName string
Related RelatedFiles
@ -18,6 +19,7 @@ type ImportJob struct {
Imp *Import
}
// ImportWorker consumes ImportJob messages and performs the on-disk moves/copies plus indexing.
func ImportWorker(jobs <-chan ImportJob) {
for job := range jobs {
var destMainFileName string

View file

@ -1,5 +1,6 @@
package photoprism
// Indexing status strings emitted from index runs.
const (
IndexUpdated IndexStatus = "updated"
IndexAdded IndexStatus = "added"
@ -10,6 +11,7 @@ const (
IndexFailed IndexStatus = "failed"
)
// IndexStatus is the unified status type returned for each indexed file.
type IndexStatus string
// IndexResult represents a media file indexing result.

View file

@ -44,6 +44,7 @@ func (l Labels) Less(i, j int) bool {
}
}
// AppendLabel adds a label if it has a name and returns the extended slice.
func (l Labels) AppendLabel(label Label) Labels {
if label.Name == "" {
return l
@ -52,6 +53,7 @@ func (l Labels) AppendLabel(label Label) Labels {
return append(l, label)
}
// Keywords flattens label names and categories into keyword tokens.
func (l Labels) Keywords() (result []string) {
for _, label := range l {
result = append(result, txt.Keywords(label.Name)...)
@ -64,6 +66,7 @@ func (l Labels) Keywords() (result []string) {
return result
}
// Title picks the best label name as title, using fallback when confidence is low.
func (l Labels) Title(fallback string) string {
fallbackRunes := len([]rune(fallback))

View file

@ -4,9 +4,9 @@ import (
"errors"
"fmt"
"image"
_ "image/gif"
_ "image/jpeg"
_ "image/png"
_ "image/gif" // register GIF decoder
_ "image/jpeg" // register JPEG decoder
_ "image/png" // register PNG decoder
"io"
"math"
"os"
@ -18,9 +18,9 @@ import (
"sync"
"time"
_ "golang.org/x/image/bmp"
_ "golang.org/x/image/tiff"
_ "golang.org/x/image/webp"
_ "golang.org/x/image/bmp" // register BMP decoder
_ "golang.org/x/image/tiff" // register TIFF decoder
_ "golang.org/x/image/webp" // register WebP decoder
"github.com/djherbis/times"
"github.com/dustin/go-humanize"
@ -220,6 +220,7 @@ func (m *MediaFile) TakenAt() (utc time.Time, local time.Time, source string) {
return m.takenAt, m.takenAtLocal, m.takenAtSrc
}
// HasTimeAndPlace reports whether both TakenAt and GPS coordinates are available.
func (m *MediaFile) HasTimeAndPlace() bool {
data := m.MetaData()

View file

@ -30,6 +30,7 @@ import (
var log = event.Log
// S is a helper alias for string slices used when composing logs and metadata.
type S []string
// logWarn logs an error as warning and keeps quiet otherwise.

View file

@ -2,6 +2,7 @@ package photoprism
import "github.com/photoprism/photoprism/pkg/fs"
// PurgeOptions controls behaviour of the purge worker.
type PurgeOptions struct {
Path string
Ignore fs.Done

View file

@ -1,11 +1,13 @@
package photoprism
// ThumbsJob encapsulates thumbnail generation parameters for a media file.
type ThumbsJob struct {
mediaFile *MediaFile
path string
force bool
}
// ThumbsWorker consumes thumbnail jobs and generates the requested previews.
func ThumbsWorker(jobs <-chan ThumbsJob) {
for job := range jobs {
mf := job.mediaFile

View file

@ -9,8 +9,8 @@ import (
"github.com/photoprism/photoprism/pkg/service/http/header"
)
// Api is a middleware that sets additional response headers when serving REST API requests.
var Api = func(conf *config.Config) gin.HandlerFunc {
// APIMiddleware returns a Gin middleware that applies API-specific headers and CORS handling.
var APIMiddleware = func(conf *config.Config) gin.HandlerFunc {
return func(c *gin.Context) {
// Add a vary response header for authentication, if any.
if c.GetHeader(header.XAuthToken) != "" {

View file

@ -8,5 +8,5 @@ import (
)
func init() {
registerApiDocs = api.GetDocs
registerAPIDocs = api.GetDocs
}

View file

@ -6,6 +6,7 @@ import (
"golang.org/x/time/rate"
)
// Default authentication rate-limit parameters.
const (
DefaultAuthInterval = time.Second * 10 // average authentication errors per second
DefaultAuthLimit = 60 // authentication failure burst rate limit (for access tokens)

View file

@ -1,5 +1,6 @@
package limiter
// DefaultIP is used when the real client IP cannot be determined.
const (
DefaultIP = "0.0.0.0"
)

View file

@ -6,6 +6,7 @@ import (
"golang.org/x/time/rate"
)
// Default login rate-limit parameters.
const (
DefaultLoginInterval = time.Minute // average failed logins per second
DefaultLoginLimit = 10 // login failure burst rate limit (for passwords and 2FA)

View file

@ -1,5 +1,6 @@
package server
// Canonical HTTP and WebDAV method names reused across handlers.
const (
MethodHead = "HEAD"
MethodGet = "GET"

View file

@ -7,8 +7,9 @@ import (
"github.com/photoprism/photoprism/internal/config"
)
// APIv1 is the router group that serves the version 1 REST API.
var APIv1 *gin.RouterGroup
var registerApiDocs func(router *gin.RouterGroup)
var registerAPIDocs func(router *gin.RouterGroup)
// registerRoutes registers the routes for handling HTTP requests with the built-in web server.
func registerRoutes(router *gin.Engine, conf *config.Config) {
@ -35,8 +36,8 @@ func registerRoutes(router *gin.Engine, conf *config.Config) {
// Docs: https://pkg.go.dev/github.com/photoprism/photoprism/internal/api
// API Documentation.
if registerApiDocs != nil {
registerApiDocs(APIv1)
if registerAPIDocs != nil {
registerAPIDocs(APIv1)
}
// User Sessions.

View file

@ -6,6 +6,7 @@ import (
"github.com/photoprism/photoprism/internal/config"
)
// WebDAV mount points served by the application.
const (
WebDAVOriginals = "/originals"
WebDAVImport = "/import"

View file

@ -101,7 +101,7 @@ func Start(ctx context.Context, conf *config.Config) {
router.Use(Security(conf))
// Create REST API router group.
APIv1 = router.Group(conf.BaseUri(config.ApiUri), Api(conf))
APIv1 = router.Group(conf.BaseUri(config.ApiUri), APIMiddleware(conf))
// Initialize package extensions.
Ext().Init(router, conf)

View file

@ -28,6 +28,8 @@ import (
var webdavAuthExpiration = 5 * time.Minute
var webdavAuthCache = gc.New(webdavAuthExpiration, webdavAuthExpiration)
var webdavAuthMutex = sync.Mutex{}
// BasicAuthRealm is the challenge string returned for WebDAV Basic auth prompts.
var BasicAuthRealm = "Basic realm=\"WebDAV Authorization Required\""
// WebDAVAuth checks authentication and authentication

View file

@ -7,6 +7,7 @@ import (
"github.com/photoprism/photoprism/internal/config"
)
// OAuth metadata published via the well-known discovery endpoints.
var (
OAuthResponseTypes = []string{"token"}
OAuthGrantTypes = []string{"client_credentials"}

View file

@ -8,6 +8,9 @@ import (
)
func TestMain(m *testing.M) {
// Remove temporary SQLite files before running the tests.
fs.PurgeTestDbFiles(".", false)
// Run unit tests.
code := m.Run()