| .. | ||
| download | ||
| embed | ||
| testdata | ||
| abort.go | ||
| AGENTS.md | ||
| albums.go | ||
| albums_search.go | ||
| albums_search_test.go | ||
| albums_test.go | ||
| api.go | ||
| api_auth.go | ||
| api_auth_jwt.go | ||
| api_auth_jwt_test.go | ||
| api_auth_test.go | ||
| api_client_config.go | ||
| api_client_config_test.go | ||
| api_event.go | ||
| api_event_test.go | ||
| api_log.go | ||
| api_methods.go | ||
| api_request.go | ||
| api_response.go | ||
| api_response_headers.go | ||
| api_response_test.go | ||
| api_test.go | ||
| auth_tokens.go | ||
| batch_albums.go | ||
| batch_labels.go | ||
| batch_photos.go | ||
| batch_photos_edit.go | ||
| batch_photos_edit_test.go | ||
| batch_photos_test.go | ||
| cache.go | ||
| cache_test.go | ||
| cameras.go | ||
| cameras_search.go | ||
| cameras_search_test.go | ||
| cameras_test.go | ||
| cluster_health_test.go | ||
| cluster_metrics.go | ||
| cluster_metrics_test.go | ||
| cluster_nodes.go | ||
| cluster_nodes_redaction_test.go | ||
| cluster_nodes_register.go | ||
| cluster_nodes_register_test.go | ||
| cluster_nodes_test.go | ||
| cluster_nodes_update_siteurl_test.go | ||
| cluster_permissions_test.go | ||
| cluster_summary.go | ||
| cluster_test_helpers.go | ||
| cluster_theme.go | ||
| cluster_theme_test.go | ||
| config_options.go | ||
| config_options_test.go | ||
| config_settings.go | ||
| config_settings_test.go | ||
| connect.go | ||
| connect_test.go | ||
| covers.go | ||
| covers_test.go | ||
| doc_overrides.go | ||
| docs.go | ||
| download.go | ||
| download_album.go | ||
| download_album_test.go | ||
| download_test.go | ||
| echo.go | ||
| echo_test.go | ||
| errors.go | ||
| errors_test.go | ||
| faces.go | ||
| faces_search.go | ||
| faces_search_test.go | ||
| faces_test.go | ||
| file_delete.go | ||
| file_delete_test.go | ||
| file_orientation.go | ||
| files.go | ||
| files_test.go | ||
| folders_cover.go | ||
| folders_cover_test.go | ||
| folders_search.go | ||
| folders_search_test.go | ||
| health.go | ||
| import.go | ||
| import_test.go | ||
| index.go | ||
| index_test.go | ||
| labels.go | ||
| labels_search.go | ||
| labels_search_test.go | ||
| labels_test.go | ||
| lenses.go | ||
| lenses_search.go | ||
| lenses_search_test.go | ||
| lenses_test.go | ||
| links.go | ||
| links_test.go | ||
| markers.go | ||
| markers_test.go | ||
| mcp.go | ||
| mcp_test.go | ||
| metrics.go | ||
| metrics_test.go | ||
| moments_time.go | ||
| moments_time_test.go | ||
| oauth_authorize.go | ||
| oauth_authorize_test.go | ||
| oauth_error.go | ||
| oauth_error_test.go | ||
| oauth_handlers.go | ||
| oauth_logout.go | ||
| oauth_logout_test.go | ||
| oauth_revoke.go | ||
| oauth_revoke_test.go | ||
| oauth_token.go | ||
| oauth_token_ratelimit_test.go | ||
| oauth_token_test.go | ||
| oauth_userinfo.go | ||
| oauth_userinfo_test.go | ||
| oidc_login.go | ||
| oidc_login_test.go | ||
| oidc_redirect.go | ||
| oidc_redirect_test.go | ||
| oidc_session_cookie.go | ||
| oidc_session_cookie_test.go | ||
| options.go | ||
| photo_label.go | ||
| photo_label_test.go | ||
| photo_unstack.go | ||
| photo_unstack_test.go | ||
| photos.go | ||
| photos_search.go | ||
| photos_search_geo.go | ||
| photos_search_geo_test.go | ||
| photos_search_test.go | ||
| photos_test.go | ||
| places_reverse.go | ||
| places_search.go | ||
| places_test.go | ||
| reactions.go | ||
| README.md | ||
| request_limits.go | ||
| server.go | ||
| services.go | ||
| services_search.go | ||
| services_search_test.go | ||
| services_test.go | ||
| services_upload.go | ||
| services_upload_test.go | ||
| session.go | ||
| session_create.go | ||
| session_delete.go | ||
| session_delete_test.go | ||
| session_get.go | ||
| session_ratelimit_test.go | ||
| session_response.go | ||
| session_test.go | ||
| share.go | ||
| share_preview.go | ||
| share_preview_test.go | ||
| share_test.go | ||
| status.go | ||
| status_test.go | ||
| subjects.go | ||
| subjects_search.go | ||
| subjects_search_test.go | ||
| subjects_test.go | ||
| svg.go | ||
| svg_test.go | ||
| swagger.json | ||
| thumbnails.go | ||
| thumbnails_test.go | ||
| users_avatar.go | ||
| users_avatar_test.go | ||
| users_passcode.go | ||
| users_passcode_test.go | ||
| users_password.go | ||
| users_password_test.go | ||
| users_sessions.go | ||
| users_sessions_test.go | ||
| users_update.go | ||
| users_update_test.go | ||
| users_upload.go | ||
| users_upload_multipart_test.go | ||
| users_upload_test.go | ||
| video.go | ||
| video_test.go | ||
| vision_caption.go | ||
| vision_caption_test.go | ||
| vision_face.go | ||
| vision_face_test.go | ||
| vision_labels.go | ||
| vision_labels_test.go | ||
| vision_nsfw.go | ||
| vision_nsfw_test.go | ||
| websocket.go | ||
| websocket_create.go | ||
| websocket_reader.go | ||
| websocket_test.go | ||
| websocket_topics_test.go | ||
| websocket_writer.go | ||
| zip.go | ||
| zip_test.go | ||
API Package Guide
Overview
The API package exposes PhotoPrism’s HTTP endpoints via Gin handlers. Each file under internal/api contains the handlers, request/response DTOs, and Swagger annotations for a specific feature area. Handlers remain thin: they validate input, enforce security or ACL checks, and delegate domain work to services in internal/photoprism, internal/service, or other internal packages. Keep exported types aligned with the REST schema and avoid embedding business logic directly in handlers.
Routing & Wiring
- Register handlers in
internal/server/routes.goby attaching them to the proper router group (for example,APIv1 := router.Group(conf.BaseUri("/api/v1"), Api(conf))). - Group endpoints by resource to match existing patterns: sessions, cluster, photos, labels, files, downloads, metadata, and technical routes.
- Apply middleware stacks (
Api,AuthRequired,limiter.Auth, etc.) at the router level to keep handlers focused on request handling. - Use
conf.BaseUri()when constructing route prefixes so configuration overrides propagate consistently. - When new endpoints require feature toggles, gate them in the router rather than inside the handler so disabled routes remain undiscoverable.
Handler Implementation Patterns
- Accept and return JSON using the shared response helpers. Set
header.ContentTypeJSONand ensure responses include proper cache headers (no-storefor sensitive payloads). - Parse parameters with Gin binding and validate inputs before delegating work. For complex payloads, define dedicated request structs with validation tags.
- Use the shared download helpers (
safe.Download,avatar.SafeDownload) when calling outward HTTP APIs to inherit timeout, size, and SSRF protections. - Query and persist data through the corresponding services or repositories; avoid ad-hoc SQL or GORM usage in handlers when dedicated functions exist elsewhere.
- Surface pagination consistently with
count,offset, andlimitfollowing the defaults (100 max 1000). Validateoffset >= 0and clampcountto the allowed range. - When responses need role-specific fields, build DTOs that redact sensitive data for non-admin roles so the handler stays deterministic.
- JSON field casing: use TitleCase field names (
UUID,Name,SiteUrl,CreatedAt) for request/response bodies that correspond to a database entity (mirroring the entity/model, e.g. the clusterNodeandClusterInstanceDTOs), and camelCase (storageNamespace,redirectUri) for generated or artificial payloads that do not map to a specific entity — client config, session responses, and action/RPC bodies. A filtered or computed projection of an entity stays TitleCase; an action payload that operates on an entity stays camelCase but MAY TitleCase the single identity field that mirrors the entity (e.g. aUUID).
Security & Middleware
- Authenticate requests using the standard middleware (
AuthRequired) and check roles via helpers ininternal/auth/acl(acl.ParseRole,acl.ScopePermits,acl.ScopeAttrPermits). - Bound request bodies before parsing JSON or multipart payloads. Use
LimitRequestBodyBytes(...)with a route-appropriate cap beforeBindJSON(...)/ShouldBindJSON(...), detectIsRequestBodyTooLarge(err), and return413 Request Entity Too LargeviaAbortRequestTooLarge(...). - Keep new JSON binding sites on the shared request-limit path by running
make check-api-request-limits(also included inmake lint) after adding or refactoring API handlers in the root repo or private overlays. - Never log secrets or tokens. Prefer structured logging through
event.Logand redact sensitive values before logging. - Enforce rate limiting with the shared limiters (
limiter.Auth,limiter.Login) and respond withlimiter.AbortJSONto maintain consistent 429 JSON payloads. - Derive client IPs through
api.ClientIPand extract bearer tokens withheader.BearerTokenor the helper setters. Use constant-time comparison for tokens and secrets. - For downloads or proxy endpoints, validate URLs against allowed schemes (
http,https) and reject private or loopback addresses unless explicitly required. - Upload-time NSFW screening (
users_upload.go) — whenPHOTOPRISM_UPLOAD_NSFW=false, the upload handler runsvision.DetectNSFWagainst every accepted file and deletes any file flagged above the NSFW threshold before it reachesoriginals/. The check is skipped entirely whenUPLOAD_NSFW=true(default). Seeinternal/ai/nsfw/README.mdfor the full NSFW call-graph and flag matrix.
Audit Logging
- Emit security events via
event.Audit*(AuditInfo,AuditWarn,AuditErr,AuditDebug) and always build the slice as Who → What → Outcome.- Who:
ClientIP(c)followed by the most specific actor context ("session %s","client %s","user %s"). - What: Resource constant plus action segments (for example,
string(acl.ResourceCluster),"node", "%s"). Place extra context such as counts or error placeholders in separate segments before the outcome. - Outcome: End with a single token such as
status.Succeeded,status.Failed,status.Denied, orstatus.Error(err)when the sanitized error message should be the outcome; nothing comes after it.
- Who:
- Prefer existing helpers (
ClientIP,clean.Log,clean.LogQuote,clean.Error) instead of formatting values manually, and avoid inline=expressions. - Example patterns:
event.AuditInfo([]string{ ClientIP(c), "session %s", string(acl.ResourceCluster), "node", "%s", status.Deleted, }, s.RefID, uuid) event.AuditErr([]string{ clientIp, "session %s", string(acl.ResourceCluster), "download theme", status.Error(err), }, refID)
User-Visible Notifications vs Audit Log
event.AuditInfo / AuditWarn / AuditErr write to the audit log and broadcast on audit.log.<level> — the toast component on the frontend does NOT subscribe to that channel, so an audit entry alone produces no UI feedback. To raise a red or green toast in the browser, publish on the notify.* channel via event.Error(msg) / event.ErrorMsg(id, …) (red) or event.Success(msg) (green).
The two helpers have distinct subscribers; choose based on who the message is for:
- Short endpoints whose response the frontend reads (single-shot CRUD, login, settings updates). The calling component renders the response, so
AuditErrplus an HTTP error is enough — the UI gets the error string from the response body. - Long-running endpoints that the UI drives via the event hub (
POST /api/v1/index,POST /api/v1/import/*path, and similar). The frontend cancels the in-flight HTTP request on the firstindex.*/import.*wire event, so the response body is invisible in normal operation. In-flight failures that need a specific toast MUST be published viaevent.ErrorMsg(...)onnotify.error; an HTTP error alone produces only the frontend's generic fallback toast (or nothing, if the cancel has already fired). - Forensic events that don't need UI surfacing (rate limiting, ACL denials, internal aborts whose user-visible signal comes from a sibling channel).
AuditErralone is the right call.
When in doubt, ask: "after this handler returns, what does the user see?" If the answer is "the frontend will read the response", AuditErr covers it. If the answer is "the page is already subscribed to wire events and the response is discarded", publish on notify.* as well.
// Forensic audit only — frontend will read the response body and render the error.
event.AuditErr([]string{ClientIP(c), "session %s", "delete album", status.Failed}, s.RefID)
AbortBadRequest(c, err)
// Forensic audit + specific red toast — needed when the request was already canceled by the wire.
event.AuditErr([]string{ClientIP(c), "session %s", "index files", status.Failed}, s.RefID)
event.ErrorMsg(i18n.ErrIndexingFailed)
Swagger Documentation
- Annotate handlers with Swagger comments that include full
/api/v1/...paths, request/response schemas, and security definitions. Only annotate routes that are externally accessible. - Regenerate docs after adding or updating handlers:
make fmt-go swag-fmt swag. This formats Go files, normalizes annotations, and updatesinternal/api/swagger.json. Do not edit the generated JSON manually. - When adding new DTOs, keep field names aligned with the JSON schema and update client documentation if serialized names change.
- Use enum annotations sparingly and ensure they reflect actual runtime constraints to avoid misleading generated clients.
Testing Strategy
- Build tests around the API harness (
NewApiTest) to obtain a configured Gin router, config, and dependencies. This isolates filesystem paths and avoids polluting global state. - Wrap requests with helper functions (for example,
PerformRequestJSON,PerformAuthenticatedRequest) to capture status codes, headers, and payloads. Assert headers using constants frompkg/http/header. - When handlers interact with the database, initialize fixtures through config helpers such as
config.NewTestConfig("api")orconfig.NewMinimalTestConfigWithDb("api", t.TempDir())depending on fixture needs. - Stub external dependencies (
httptest.Server) for remote calls and setAllowPrivate=trueexplicitly when the test server binds to loopback addresses. - Structure tests with table-driven subtests (
t.Run("CaseName", ...)) and use PascalCase names. Provide cleanup functions (t.Cleanup) to remove temporary files or databases created during tests. - Do not run
internal/apitests in parallel. These suites share fixture files, temporary assets, and database state, so parallelgo testinvocations can cause false failures and readonly/fixture-conflict errors.
Focused Test Runs
- Fast iteration:
go test ./internal/api -run '<Package|HandlerName>' -count=1 - Cluster endpoints:
go test ./internal/api -run 'Cluster' -count=1 - Downloads and zip streaming:
go test ./internal/api -run 'Download|Archive' -count=1 - Combined CLI and API validation: pair
go test ./internal/commands -run 'Cluster' -count=1with the matching API suite to ensure DTOs remain compatible. - Keep focused
internal/apiruns sequential. Do not launch multiplego test ./internal/api ...commands at the same time.
Preflight Checklist
- Format and regenerate documentation:
make fmt-go swag-fmt swag - Compile backend:
go build ./... - Execute targeted API suites:
go test ./internal/api -run '<Name>' -count=1 - Run integration-heavy checks before release:
go test ./internal/service/cluster/registry -count=1alongside relevant API routes to confirm cluster DTOs stay aligned. - Verify that
photoprism show commands --jsonreflects any new API-driven flags or outputs when CLI exposure changes.