API: Add missing Swagger annotations and update swagger.json

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer
2025-10-30 11:00:16 +01:00
parent 3a0eaebb82
commit 5ad391068d
11 changed files with 413 additions and 26 deletions

View File

@@ -20,6 +20,13 @@ import (
var swaggerJSON []byte
// GetDocs registers the Swagger API documentation endpoints.
//
// @Summary serves embedded Swagger documentation (debug builds only)
// @Id GetDocs
// @Tags System
// @Produce json
// @Success 200 {object} gin.H "Swagger JSON"
// @Router /swagger.json [get]
func GetDocs(router *gin.RouterGroup) {
// Get global configuration.
conf := get.Config()

View File

@@ -187,7 +187,13 @@ func StartImport(router *gin.RouterGroup) {
// CancelImport stops the current import operation.
//
// DELETE /api/v1/import
// @Summary cancels the active import job
// @Id CancelImport
// @Tags Library
// @Produce json
// @Success 200 {object} i18n.Response
// @Failure 401,403,404,429 {object} i18n.Response
// @Router /api/v1/import [delete]
func CancelImport(router *gin.RouterGroup) {
router.DELETE("/import", func(c *gin.Context) {
s := Auth(c, acl.ResourceFiles, acl.ActionManage)

View File

@@ -179,7 +179,13 @@ func StartIndexing(router *gin.RouterGroup) {
// CancelIndexing stops indexing media files in the "originals" folder.
//
// DELETE /api/v1/index
// @Summary cancels the active indexing job
// @Id CancelIndexing
// @Tags Library
// @Produce json
// @Success 200 {object} i18n.Response
// @Failure 401,403,404,429 {object} i18n.Response
// @Router /api/v1/index [delete]
func CancelIndexing(router *gin.RouterGroup) {
router.DELETE("/index", func(c *gin.Context) {
s := Auth(c, acl.ResourcePhotos, acl.ActionUpdate)

View File

@@ -15,7 +15,12 @@ import (
// OAuthUserinfo should return information about the authenticated user,
// see https://github.com/photoprism/photoprism/issues/4369.
//
// GET /api/v1/oauth/userinfo
// @Summary OAuth2 userinfo endpoint (not implemented)
// @Id OAuthUserinfo
// @Tags Authentication
// @Produce json
// @Failure 405 {object} i18n.Response
// @Router /api/v1/oauth/userinfo [get]
func OAuthUserinfo(router *gin.RouterGroup) {
router.GET("/oauth/userinfo", func(c *gin.Context) {
// Prevent CDNs from caching this endpoint.

View File

@@ -12,10 +12,17 @@ import (
"github.com/photoprism/photoprism/pkg/clean"
)
// Shares handles link share
// ShareToken renders the generic share landing page.
//
// GET /s/:token/...
func Shares(router *gin.RouterGroup) {
// @Summary renders the generic share landing page
// @Id ShareToken
// @Tags Shares
// @Produce text/html
// @Param token path string true "Share token"
// @Success 200 {string} string "Rendered HTML page"
// @Failure 302 {string} string "Redirect to the base site when the token is invalid"
// @Router /s/{token} [get]
func ShareToken(router *gin.RouterGroup) {
router.GET("/:token", func(c *gin.Context) {
conf := get.Config()
@@ -34,7 +41,20 @@ func Shares(router *gin.RouterGroup) {
uri := conf.LibraryUri("/albums")
c.HTML(http.StatusOK, "share.gohtml", gin.H{"shared": gin.H{"token": token, "uri": uri}, "config": clientConfig})
})
}
// ShareTokenShared renders the album/page view for an individual share.
//
// @Summary renders the album/page view for an individual share
// @Id ShareTokenShared
// @Tags Shares
// @Produce text/html
// @Param token path string true "Share token"
// @Param shared path string true "Shared resource UID"
// @Success 200 {string} string "Rendered HTML page"
// @Failure 302 {string} string "Redirect to the base site when the token is invalid"
// @Router /s/{token}/{shared} [get]
func ShareTokenShared(router *gin.RouterGroup) {
router.GET("/:token/:shared", func(c *gin.Context) {
conf := get.Config()

View File

@@ -25,8 +25,15 @@ import (
// SharePreview returns a preview image for the given share uid if the token is valid.
//
// GET /s/:token/:shared/preview
// TODO: Proof of concept, needs refactoring.
// @Summary returns a share preview image when the token is valid
// @Id SharePreview
// @Tags Shares
// @Produce image/jpeg
// @Param token path string true "Share token"
// @Param shared path string true "Shared resource UID"
// @Success 200 {file} file "Preview image"
// @Failure 302 {string} string "Redirect to the default preview page"
// @Router /s/{token}/{shared}/preview [get]
func SharePreview(router *gin.RouterGroup) {
router.GET("/:token/:shared/preview", func(c *gin.Context) {
conf := get.Config()

View File

@@ -7,31 +7,34 @@ import (
"github.com/stretchr/testify/assert"
)
func TestGetShares(t *testing.T) {
func TestShareToken(t *testing.T) {
t.Run("InvalidToken", func(t *testing.T) {
app, router, _ := NewApiTest()
Shares(router)
r := PerformRequest(app, "GET", "/api/v1/1jxf3jfn2k/ss6sg6bxpogaaba7")
assert.Equal(t, http.StatusTemporaryRedirect, r.Code)
})
//TODO Why does it panic?
/*t.Run("ValidTokenAndShare", func(t *testing.T) {
app, router, _ := NewApiTest()
Shares(router)
r := PerformRequest(app, "GET", "/api/v1/4jxf3jfn2k/as6sg6bxpogaaba7")
assert.Equal(t, http.StatusTemporaryRedirect, r.Code)
})*/
t.Run("InvalidToken", func(t *testing.T) {
app, router, _ := NewApiTest()
Shares(router)
ShareToken(router)
r := PerformRequest(app, "GET", "/api/v1/xxx")
assert.Equal(t, http.StatusTemporaryRedirect, r.Code)
})
//TODO Why does it panic?
/*t.Run("ValidToken", func(t *testing.T) {
app, router, _ := NewApiTest()
Shares(router)
ShareToken(router)
r := PerformRequest(app, "GET", "/api/v1/4jxf3jfn2k")
assert.Equal(t, http.StatusTemporaryRedirect, r.Code)
})*/
}
func TestShareTokenShared(t *testing.T) {
t.Run("InvalidToken", func(t *testing.T) {
app, router, _ := NewApiTest()
ShareTokenShared(router)
r := PerformRequest(app, "GET", "/api/v1/1jxf3jfn2k/ss6sg6bxpogaaba7")
assert.Equal(t, http.StatusTemporaryRedirect, r.Code)
})
//TODO Why does it panic?
/*t.Run("ValidTokenAndShare", func(t *testing.T) {
app, router, _ := NewApiTest()
ShareTokenShared(router)
r := PerformRequest(app, "GET", "/api/v1/4jxf3jfn2k/as6sg6bxpogaaba7")
assert.Equal(t, http.StatusTemporaryRedirect, r.Code)
})*/
}

View File

@@ -48,7 +48,14 @@ var uncachedIconSvg []byte
// GetSvg returns SVG placeholder symbols.
//
// GET /api/v1/svg/*
// @Summary returns SVG placeholder symbols for UI fallbacks
// @Id GetSvg
// @Tags Assets
// @Produce image/svg+xml
// @Param icon path string true "SVG icon name" Enums(user,face,camera,photo,raw,file,video,label,portrait,folder,album,broken,uncached)
// @Success 200 {string} string "SVG icon"
// @Failure 404 {object} gin.H "Icon not found"
// @Router /api/v1/svg/{icon} [get]
func GetSvg(router *gin.RouterGroup) {
router.GET("/svg/user", func(c *gin.Context) {
c.Data(http.StatusOK, "image/svg+xml", userIconSvg)

View File

@@ -7871,6 +7871,50 @@
]
}
},
"/api/v1/import": {
"delete": {
"operationId": "CancelImport",
"produces": [
"application/json"
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/i18n.Response"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/i18n.Response"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/i18n.Response"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/i18n.Response"
}
},
"429": {
"description": "Too Many Requests",
"schema": {
"$ref": "#/definitions/i18n.Response"
}
}
},
"summary": "cancels the active import job",
"tags": [
"Library"
]
}
},
"/api/v1/import/": {
"post": {
"consumes": [
@@ -7924,6 +7968,48 @@
}
},
"/api/v1/index": {
"delete": {
"operationId": "CancelIndexing",
"produces": [
"application/json"
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/i18n.Response"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/i18n.Response"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/i18n.Response"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/i18n.Response"
}
},
"429": {
"description": "Too Many Requests",
"schema": {
"$ref": "#/definitions/i18n.Response"
}
}
},
"summary": "cancels the active indexing job",
"tags": [
"Library"
]
},
"post": {
"consumes": [
"application/json"
@@ -8657,6 +8743,26 @@
]
}
},
"/api/v1/oauth/userinfo": {
"get": {
"operationId": "OAuthUserinfo",
"produces": [
"application/json"
],
"responses": {
"405": {
"description": "Method Not Allowed",
"schema": {
"$ref": "#/definitions/i18n.Response"
}
}
},
"summary": "OAuth2 userinfo endpoint (not implemented)",
"tags": [
"Authentication"
]
}
},
"/api/v1/oidc/login": {
"get": {
"operationId": "OIDCLogin",
@@ -11236,6 +11342,56 @@
]
}
},
"/api/v1/svg/{icon}": {
"get": {
"operationId": "GetSvg",
"parameters": [
{
"description": "SVG icon name",
"enum": [
"user",
"face",
"camera",
"photo",
"raw",
"file",
"video",
"label",
"portrait",
"folder",
"album",
"broken",
"uncached"
],
"in": "path",
"name": "icon",
"required": true,
"type": "string"
}
],
"produces": [
"image/svg+xml"
],
"responses": {
"200": {
"description": "SVG icon",
"schema": {
"type": "string"
}
},
"404": {
"description": "Icon not found",
"schema": {
"$ref": "#/definitions/gin.H"
}
}
},
"summary": "returns SVG placeholder symbols for UI fallbacks",
"tags": [
"Assets"
]
}
},
"/api/v1/t/{thumb}/{token}/{size}": {
"get": {
"description": "Fore more information see:\n- https://docs.photoprism.app/developer-guide/api/thumbnails/#image-endpoint-uri",
@@ -12382,6 +12538,168 @@
"CORS"
]
}
},
"/s/{token}": {
"get": {
"operationId": "ShareToken",
"parameters": [
{
"description": "Share token",
"in": "path",
"name": "token",
"required": true,
"type": "string"
}
],
"produces": [
"text/html"
],
"responses": {
"200": {
"description": "Rendered HTML page",
"schema": {
"type": "string"
}
},
"302": {
"description": "Redirect to the base site when the token is invalid",
"schema": {
"type": "string"
}
}
},
"summary": "renders the generic share landing page",
"tags": [
"Shares"
]
}
},
"/s/{token}/{shared}": {
"get": {
"operationId": "ShareTokenShared",
"parameters": [
{
"description": "Share token",
"in": "path",
"name": "token",
"required": true,
"type": "string"
},
{
"description": "Shared resource UID",
"in": "path",
"name": "shared",
"required": true,
"type": "string"
}
],
"produces": [
"text/html"
],
"responses": {
"200": {
"description": "Rendered HTML page",
"schema": {
"type": "string"
}
},
"302": {
"description": "Redirect to the base site when the token is invalid",
"schema": {
"type": "string"
}
}
},
"summary": "renders the album/page view for an individual share",
"tags": [
"Shares"
]
}
},
"/s/{token}/{shared}/preview": {
"get": {
"operationId": "SharePreview",
"parameters": [
{
"description": "Share token",
"in": "path",
"name": "token",
"required": true,
"type": "string"
},
{
"description": "Shared resource UID",
"in": "path",
"name": "shared",
"required": true,
"type": "string"
}
],
"produces": [
"image/jpeg"
],
"responses": {
"200": {
"description": "Preview image",
"schema": {
"type": "file"
}
},
"302": {
"description": "Redirect to the default preview page",
"schema": {
"type": "string"
}
}
},
"summary": "returns a share preview image when the token is valid",
"tags": [
"Shares"
]
}
},
"/swagger.json": {
"get": {
"operationId": "GetDocs",
"produces": [
"application/json"
],
"responses": {
"200": {
"description": "Swagger JSON",
"schema": {
"$ref": "#/definitions/gin.H"
}
}
},
"summary": "serves embedded Swagger documentation (debug builds only)",
"tags": [
"System"
]
}
},
"/ws": {
"get": {
"operationId": "WebSocket",
"responses": {
"101": {
"description": "Switching Protocols",
"schema": {
"type": "string"
}
},
"403": {
"description": "Forbidden",
"schema": {
"type": "string"
}
}
},
"summary": "opens a WebSocket connection for real-time events",
"tags": [
"WebSocket"
]
}
}
},
"security": [

View File

@@ -11,6 +11,13 @@ import (
)
// WebSocket registers the /ws endpoint for establishing websocket connections.
//
// @Summary opens a WebSocket connection for real-time events
// @Id WebSocket
// @Tags WebSocket
// @Success 101 {string} string "Switching Protocols"
// @Failure 403 {string} string "Forbidden"
// @Router /ws [get]
func WebSocket(router *gin.RouterGroup) {
if router == nil {
return

View File

@@ -16,7 +16,8 @@ func registerSharingRoutes(router *gin.Engine, conf *config.Config) {
s := router.Group(conf.BaseUri("/s"))
{
api.Shares(s)
api.ShareToken(s)
api.ShareTokenShared(s)
api.SharePreview(s)
}
}