34 Commits

Author SHA1 Message Date
b4bb732303 fix(config): typoed defaults
All checks were successful
ci/woodpecker/tag/release Pipeline was successful
ci/woodpecker/push/build Pipeline was successful
2024-12-16 17:02:49 +01:00
3dee3e72af fix: Bad dependency
All checks were successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/tag/release Pipeline was successful
2024-12-16 16:41:52 +01:00
f5914e5c38 style: Formatting
Some checks failed
ci/woodpecker/push/build Pipeline failed
2024-12-16 16:39:56 +01:00
5a5a7179e9 feat: Use CORS
Some checks failed
ci/woodpecker/push/build Pipeline failed
ci/woodpecker/tag/release Pipeline failed
2024-12-16 16:39:02 +01:00
6c57d63891 fix(config): Added SPONSORING_DISCLAIMER default
All checks were successful
ci/woodpecker/push/build Pipeline was successful
2024-12-16 16:31:08 +01:00
b502e2fbd5 fix(config): Defaults for everyone
Some checks failed
ci/woodpecker/push/build Pipeline failed
ci/woodpecker/tag/release Pipeline was successful
2024-12-16 16:30:02 +01:00
c9475d0093 feat(config): log config on boot
All checks were successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/tag/release Pipeline was successful
2024-12-16 16:17:26 +01:00
1cc19e0085 fix(config): Default apikey
Some checks failed
ci/woodpecker/push/build Pipeline failed
ci/woodpecker/tag/release Pipeline was successful
2024-12-16 16:15:11 +01:00
b792806481 docs(swagger): New barcode padding docs
All checks were successful
ci/woodpecker/tag/release Pipeline was successful
ci/woodpecker/push/build Pipeline was successful
2024-12-12 19:17:29 +01:00
ea6a4a7080 refactor(barcode): Switch to inclusive padding 2024-12-12 19:16:55 +01:00
de6fe4991c feat(barcode): Padding 2024-12-12 19:11:01 +01:00
1d068b2655 docs(swagger): New swaggerdoc
All checks were successful
ci/woodpecker/push/build Pipeline was successful
2024-12-12 18:50:21 +01:00
ef25adf5ed refactor(models): Group now only has a name
Some checks failed
ci/woodpecker/push/build Pipeline failed
ci/woodpecker/tag/release Pipeline was successful
2024-12-12 18:48:39 +01:00
c09c00ec68 feat(pdfs): Set download names for pdf generation responses
All checks were successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/tag/release Pipeline was successful
2024-12-12 18:24:07 +01:00
1f4981b0a9 fix(config): Don't fail on missing env
Some checks failed
ci/woodpecker/push/build Pipeline failed
ci/woodpecker/tag/release Pipeline was successful
2024-12-12 18:18:41 +01:00
c9f28612be docs(README): Updated benchmark section
All checks were successful
ci/woodpecker/tag/release Pipeline was successful
2024-12-12 18:05:52 +01:00
c2d192d8a3 docs(README): Added performance results 2024-12-12 17:59:33 +01:00
31734596f6 feat(barcode): Implement cache 2024-12-12 17:59:14 +01:00
850fa0a760 feat(barcode): Baseline for barcode caching 2024-12-12 17:44:41 +01:00
1c0a9860fa feat(container): Provide default redis 2024-12-12 17:44:26 +01:00
7c32f1aad2 perf(templates): Cache templates in map 2024-12-12 17:34:55 +01:00
b9e550d6f5 refactor(services): Move staticservice to struct and interface 2024-12-12 17:11:59 +01:00
2a4f126377 feat(barcodes): Make width and height configureable 2024-12-12 17:03:10 +01:00
5c932158e9 refactor(handlers): Use shared gotenberg 2024-12-12 16:53:51 +01:00
1296c9e399 refactor(pdf): Share templater 2024-12-12 16:52:06 +01:00
0f2452eca0 feat(handlers): Implement barcode generation 2024-12-12 16:49:33 +01:00
b6cc98a165 refactor(services): Extract barcode generation 2024-12-12 16:43:14 +01:00
c5da33f10f chore: Formatting 2024-12-12 16:33:23 +01:00
69f4de6739 docs(swagger): Move security annotations 2024-12-12 16:33:10 +01:00
649ac2a3c2 docs(swagger): Added barcode generation docs 2024-12-12 16:31:07 +01:00
5587fdaaa8 feat(barcodes): Baseline for implementation 2024-12-12 16:30:46 +01:00
8f676f08a9 refactor: Switch to pdf subpath 2024-12-12 16:28:06 +01:00
28de60d375 Merge pull request 'go' (#49) from go into main
Reviewed-on: #49
Reviewed-by: Philipp Dormann <philipp@noreply.git.odit.services>
2024-12-12 15:11:30 +00:00
d19029b5ad docs(swagger): Updated swagger metadata 2024-12-11 19:45:05 +01:00
21 changed files with 612 additions and 163 deletions

1
.env
View File

@@ -4,6 +4,7 @@ APIKEY=lfk
EVENTNAME=Lauf für Kaya! 2025
CURRENCYSYMBOL=
GOTENBERG_BASEURL=http://localhost:3001
REDIS_ADDR=localhost:6379
CARD_SUBTITLE=Kaya ist cool
CARD_BARCODEFORMAT=ean13

View File

@@ -68,3 +68,29 @@ docker build -t registry.odit.services/lfk/document-server:latest .
# multiarch
docker buildx build --platform=linux/amd64,linux/arm64 -t registry.odit.services/lfk/document-server:latest --push .
```
## ⏱️ Benchmarks
### Barcode Generation
- Requests: 10000
- Concurrency: Unlimited
- Data: `123456789123`
- Width: 1000
- Height: 500
#### No cache (cold start)
| Format | Data | Requests/sec | Slowest | Fastest | Average |
| ------- | -------------- | ------------ | ------- | ------- | ------- |
| Code128 | `123456789123` | 763.3996 | 0.7995 | 0.0172 | 0.0654 |
| EAN13 | `123456789123` | 767.1170 | 0.7607 | 0.0171 | 0.0651 |
| QR | `123456789123` | 683.8472 | 0.6528 | 0.0178 | 0.0730 |
#### Redis cache (warm start)
| Format | Data | Requests/sec | Slowest | Fastest | Average |
| ------- | -------------- | ------------ | ------- | ------- | ------- |
| Code128 | `123456789123` | 15611.5521 | 0.0965 | 0.0006 | 0.0032 |
| EAN13 | `123456789123` | 14985.4401 | 0.0925 | 0.0006 | 0.0033 |
| QR | `123456789123` | 14306.2540 | 0.1143 | 0.0005 | 0.0035 |

View File

@@ -2,4 +2,8 @@ services:
gotenberg:
image: gotenberg/gotenberg:8
ports:
- "3001:3000"
- "3001:3000"
redis:
image: docker.dragonflydb.io/dragonflydb/dragonfly
ports:
- "6379:6379"

View File

@@ -8,4 +8,8 @@ services:
gotenberg:
image: gotenberg/gotenberg:8
ports:
- "3001:3000"
- "3001:3000"
redis:
image: docker.dragonflydb.io/dragonflydb/dragonfly
ports:
- "6379:6379"

View File

@@ -4,6 +4,7 @@ APIKEY=lfk
EVENTNAME=Lauf für Kaya! 2025
CURRENCYSYMBOL=
GOTENBERG_BASEURL=http://gotenberg:3000
REDIS_ADDR=redis:6379
CARD_SUBTITLE=Kaya ist cool
CARD_BARCODEFORMAT=ean13

View File

@@ -9,14 +9,88 @@ const docTemplate = `{
"info": {
"description": "{{escape .Description}}",
"title": "{{.Title}}",
"contact": {},
"termsOfService": "https://lauf-fuer-kaya.de/datenschutz",
"contact": {
"name": "ODIT.Services UG (haftungsbeschränkt)",
"url": "https://odit.services",
"email": "info@odit.services"
},
"license": {
"name": "CC BY-NC-SA 4.0"
},
"version": "{{.Version}}"
},
"host": "{{.Host}}",
"basePath": "{{.BasePath}}",
"paths": {
"/cards": {
"/v1/barcodes/{type}/{content}": {
"get": {
"description": "Generate barcodes based on the provided data",
"produces": [
"image/png"
],
"tags": [
"barcodes"
],
"summary": "Generate barcodes",
"parameters": [
{
"enum": [
"ean13",
"code128"
],
"type": "string",
"description": "Barcode type",
"name": "type",
"in": "path",
"required": true
},
{
"minLength": 1,
"type": "string",
"description": "Barcode content",
"name": "content",
"in": "path",
"required": true
},
{
"maximum": 10000,
"minimum": 1,
"type": "integer",
"default": 1000,
"description": "Barcode width",
"name": "width",
"in": "query"
},
{
"maximum": 10000,
"minimum": 1,
"type": "integer",
"default": 1000,
"description": "Barcode height",
"name": "height",
"in": "query"
},
{
"maximum": 100,
"minimum": 0,
"type": "integer",
"default": 10,
"description": "Padding around the barcode (included in image size)",
"name": "padding",
"in": "query"
}
],
"responses": {}
}
},
"/v1/pdfs/cards": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Generate cards based on the provided data",
"consumes": [
"application/json"
@@ -25,7 +99,7 @@ const docTemplate = `{
"application/pdf"
],
"tags": [
"cards"
"pdfs"
],
"summary": "Generate runner cards",
"parameters": [
@@ -42,8 +116,13 @@ const docTemplate = `{
"responses": {}
}
},
"/certificates": {
"/v1/pdfs/certificates": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Generate certificates based on the provided data",
"consumes": [
"application/json"
@@ -52,7 +131,7 @@ const docTemplate = `{
"application/pdf"
],
"tags": [
"certificates"
"pdfs"
],
"summary": "Generate runner certificates",
"parameters": [
@@ -69,8 +148,13 @@ const docTemplate = `{
"responses": {}
}
},
"/contracts": {
"/v1/pdfs/contracts": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Generate a contract based on the provided data",
"consumes": [
"application/json"
@@ -79,7 +163,7 @@ const docTemplate = `{
"application/pdf"
],
"tags": [
"contracts"
"pdfs"
],
"summary": "Generate a contract",
"parameters": [
@@ -237,18 +321,11 @@ const docTemplate = `{
"models.Group": {
"type": "object",
"required": [
"id",
"name"
],
"properties": {
"id": {
"type": "integer"
},
"name": {
"type": "string"
},
"parent_group": {
"$ref": "#/definitions/models.Group"
}
}
},
@@ -327,12 +404,7 @@ const docTemplate = `{
"name": "key",
"in": "query"
}
},
"security": [
{
"ApiKeyAuth": []
}
]
}
}`
// SwaggerInfo holds exported Swagger Info so clients can modify it

View File

@@ -3,11 +3,85 @@
"info": {
"description": "This is the API documentation for the LfK Document Server - a tool for pdf generation.",
"title": "LfK Document Server API",
"contact": {}
"termsOfService": "https://lauf-fuer-kaya.de/datenschutz",
"contact": {
"name": "ODIT.Services UG (haftungsbeschränkt)",
"url": "https://odit.services",
"email": "info@odit.services"
},
"license": {
"name": "CC BY-NC-SA 4.0"
}
},
"paths": {
"/cards": {
"/v1/barcodes/{type}/{content}": {
"get": {
"description": "Generate barcodes based on the provided data",
"produces": [
"image/png"
],
"tags": [
"barcodes"
],
"summary": "Generate barcodes",
"parameters": [
{
"enum": [
"ean13",
"code128"
],
"type": "string",
"description": "Barcode type",
"name": "type",
"in": "path",
"required": true
},
{
"minLength": 1,
"type": "string",
"description": "Barcode content",
"name": "content",
"in": "path",
"required": true
},
{
"maximum": 10000,
"minimum": 1,
"type": "integer",
"default": 1000,
"description": "Barcode width",
"name": "width",
"in": "query"
},
{
"maximum": 10000,
"minimum": 1,
"type": "integer",
"default": 1000,
"description": "Barcode height",
"name": "height",
"in": "query"
},
{
"maximum": 100,
"minimum": 0,
"type": "integer",
"default": 10,
"description": "Padding around the barcode (included in image size)",
"name": "padding",
"in": "query"
}
],
"responses": {}
}
},
"/v1/pdfs/cards": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Generate cards based on the provided data",
"consumes": [
"application/json"
@@ -16,7 +90,7 @@
"application/pdf"
],
"tags": [
"cards"
"pdfs"
],
"summary": "Generate runner cards",
"parameters": [
@@ -33,8 +107,13 @@
"responses": {}
}
},
"/certificates": {
"/v1/pdfs/certificates": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Generate certificates based on the provided data",
"consumes": [
"application/json"
@@ -43,7 +122,7 @@
"application/pdf"
],
"tags": [
"certificates"
"pdfs"
],
"summary": "Generate runner certificates",
"parameters": [
@@ -60,8 +139,13 @@
"responses": {}
}
},
"/contracts": {
"/v1/pdfs/contracts": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Generate a contract based on the provided data",
"consumes": [
"application/json"
@@ -70,7 +154,7 @@
"application/pdf"
],
"tags": [
"contracts"
"pdfs"
],
"summary": "Generate a contract",
"parameters": [
@@ -228,18 +312,11 @@
"models.Group": {
"type": "object",
"required": [
"id",
"name"
],
"properties": {
"id": {
"type": "integer"
},
"name": {
"type": "string"
},
"parent_group": {
"$ref": "#/definitions/models.Group"
}
}
},
@@ -318,10 +395,5 @@
"name": "key",
"in": "query"
}
},
"security": [
{
"ApiKeyAuth": []
}
]
}
}

View File

@@ -94,14 +94,9 @@ definitions:
type: object
models.Group:
properties:
id:
type: integer
name:
type: string
parent_group:
$ref: '#/definitions/models.Group'
required:
- id
- name
type: object
models.Runner:
@@ -152,12 +147,63 @@ definitions:
- last_name
type: object
info:
contact: {}
contact:
email: info@odit.services
name: ODIT.Services UG (haftungsbeschränkt)
url: https://odit.services
description: This is the API documentation for the LfK Document Server - a tool
for pdf generation.
license:
name: CC BY-NC-SA 4.0
termsOfService: https://lauf-fuer-kaya.de/datenschutz
title: LfK Document Server API
paths:
/cards:
/v1/barcodes/{type}/{content}:
get:
description: Generate barcodes based on the provided data
parameters:
- description: Barcode type
enum:
- ean13
- code128
in: path
name: type
required: true
type: string
- description: Barcode content
in: path
minLength: 1
name: content
required: true
type: string
- default: 1000
description: Barcode width
in: query
maximum: 10000
minimum: 1
name: width
type: integer
- default: 1000
description: Barcode height
in: query
maximum: 10000
minimum: 1
name: height
type: integer
- default: 10
description: Padding around the barcode (included in image size)
in: query
maximum: 100
minimum: 0
name: padding
type: integer
produces:
- image/png
responses: {}
summary: Generate barcodes
tags:
- barcodes
/v1/pdfs/cards:
post:
consumes:
- application/json
@@ -172,10 +218,12 @@ paths:
produces:
- application/pdf
responses: {}
security:
- ApiKeyAuth: []
summary: Generate runner cards
tags:
- cards
/certificates:
- pdfs
/v1/pdfs/certificates:
post:
consumes:
- application/json
@@ -190,10 +238,12 @@ paths:
produces:
- application/pdf
responses: {}
security:
- ApiKeyAuth: []
summary: Generate runner certificates
tags:
- certificates
/contracts:
- pdfs
/v1/pdfs/contracts:
post:
consumes:
- application/json
@@ -208,11 +258,11 @@ paths:
produces:
- application/pdf
responses: {}
security:
- ApiKeyAuth: []
summary: Generate a contract
tags:
- contracts
security:
- ApiKeyAuth: []
- pdfs
securityDefinitions:
ApiKeyAuth:
in: query

3
go.mod
View File

@@ -14,6 +14,8 @@ require (
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/boombuler/barcode v1.0.2 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
@@ -32,6 +34,7 @@ require (
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/oxplot/papersizes v0.0.0-20181201065918-90a3a5ae1915 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/redis/go-redis/v9 v9.7.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect

6
go.sum
View File

@@ -4,9 +4,13 @@ github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7X
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/boombuler/barcode v1.0.2 h1:79yrbttoZrLGkL/oOI8hBrUKucwOL0oOjUgEguGMcJ4=
github.com/boombuler/barcode v1.0.2/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
@@ -54,6 +58,8 @@ github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E=
github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=

64
handlers/barcode.go Normal file
View File

@@ -0,0 +1,64 @@
package handlers
import (
"strconv"
"github.com/gofiber/fiber/v2"
)
// GenerateBarcode godoc
//
// @Summary Generate barcodes
// @Description Generate barcodes based on the provided data
// @Tags barcodes
// @Param type path string true "Barcode type" Enums(ean13, code128)
// @Param content path string true "Barcode content" MinLength(1)
// @Param width query int false "Barcode width" Minimum(1) Maximum(10000) default(1000)
// @Param height query int false "Barcode height" Minimum(1) Maximum(10000) default(1000)
// @Param padding query int false "Padding around the barcode (included in image size)" Minimum(0) Maximum(100) default(10)
// @Produce image/png
// @Router /v1/barcodes/{type}/{content} [get]
func (h *DefaultHandler) GenerateBarcode(c *fiber.Ctx) error {
// Get the type and content from the URL
barcodeType := c.Params("type")
barcodeContent := c.Params("content")
// Get the width and height from the query parameters
widthStr := c.Query("width", "1000")
heightStr := c.Query("height", "1000")
paddingStr := c.Query("padding", "10")
// Convert width and height to integers
width, err := strconv.Atoi(widthStr)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Invalid width parameter",
})
}
height, err := strconv.Atoi(heightStr)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Invalid height parameter",
})
}
padding, err := strconv.Atoi(paddingStr)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Invalid padding parameter",
})
}
// Generate the barcode
barcode, err := h.BarcodeService.GenerateBarcode(barcodeType, barcodeContent, width, height, padding)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": err.Error(),
})
}
c.Set(fiber.HeaderContentType, "image/png")
return c.Send(barcode.Bytes())
}

View File

@@ -5,18 +5,19 @@ import (
"slices"
"git.odit.services/lfk/document-server/models"
"git.odit.services/lfk/document-server/services"
"github.com/gofiber/fiber/v2"
)
// GenerateCard godoc
// @Summary Generate runner cards
// @Description Generate cards based on the provided data
// @Tags cards
// @Accept json
// @Param data body models.CardRequest true "Card data"
// @Produce application/pdf
// @Router /cards [post]
//
// @Summary Generate runner cards
// @Description Generate cards based on the provided data
// @Tags pdfs
// @Accept json
// @Param data body models.CardRequest true "Card data"
// @Produce application/pdf
// @Security ApiKeyAuth
// @Router /v1/pdfs/cards [post]
func (h *DefaultHandler) GenerateCard(c *fiber.Ctx) error {
cardRequest := new(models.CardRequest)
if err := c.BodyParser(cardRequest); err != nil {
@@ -30,15 +31,14 @@ func (h *DefaultHandler) GenerateCard(c *fiber.Ctx) error {
})
}
generator := services.DefaultTemplater{}
templateString, err := services.GetTemplate(cardRequest.Locale, "card")
templateString, err := h.StaticService.GetTemplate(cardRequest.Locale, "card")
if err != nil {
log.Println(err)
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Template not found",
})
}
template, err := generator.StringToTemplate(templateString)
template, err := h.Templater.StringToTemplate(templateString)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": err.Error(),
@@ -53,15 +53,15 @@ func (h *DefaultHandler) GenerateCard(c *fiber.Ctx) error {
BarcodePrefix: h.Config.CardBarcodePrefix,
}
result, err := generator.Execute(template, genConfig)
result, err := h.Templater.Execute(template, genConfig)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": err.Error(),
})
}
c.Set(fiber.HeaderContentType, "text/html")
converter := services.GotenbergConverter{BaseUrl: h.Config.GotenbergBaseUrl}
pdf, err := converter.ToPdf(result, "a4", false)
pdf, err := h.Converter.ToPdf(result, "a4", false)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": err.Error(),
@@ -69,6 +69,7 @@ func (h *DefaultHandler) GenerateCard(c *fiber.Ctx) error {
}
c.Set(fiber.HeaderContentType, "application/pdf")
c.Set(fiber.HeaderContentDisposition, "attachment; filename=runner-cards.pdf")
return c.Send(pdf)
}

View File

@@ -5,18 +5,19 @@ import (
"slices"
"git.odit.services/lfk/document-server/models"
"git.odit.services/lfk/document-server/services"
"github.com/gofiber/fiber/v2"
)
// GenerateCertificate godoc
// @Summary Generate runner certificates
// @Description Generate certificates based on the provided data
// @Tags certificates
// @Accept json
// @Param data body models.CertificateRequest true "Certificate data"
// @Produce application/pdf
// @Router /certificates [post]
//
// @Summary Generate runner certificates
// @Description Generate certificates based on the provided data
// @Tags pdfs
// @Accept json
// @Param data body models.CertificateRequest true "Certificate data"
// @Produce application/pdf
// @Security ApiKeyAuth
// @Router /v1/pdfs/certificates [post]
func (h *DefaultHandler) GenerateCertificate(c *fiber.Ctx) error {
certificateRequest := new(models.CertificateRequest)
if err := c.BodyParser(certificateRequest); err != nil {
@@ -30,15 +31,14 @@ func (h *DefaultHandler) GenerateCertificate(c *fiber.Ctx) error {
})
}
generator := services.DefaultTemplater{}
templateString, err := services.GetTemplate(certificateRequest.Locale, "certificate")
templateString, err := h.StaticService.GetTemplate(certificateRequest.Locale, "certificate")
if err != nil {
log.Println(err)
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Template not found",
})
}
template, err := generator.StringToTemplate(templateString)
template, err := h.Templater.StringToTemplate(templateString)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": err.Error(),
@@ -53,15 +53,15 @@ func (h *DefaultHandler) GenerateCertificate(c *fiber.Ctx) error {
Locale: certificateRequest.Locale,
}
result, err := generator.Execute(template, genConfig)
result, err := h.Templater.Execute(template, genConfig)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": err.Error(),
})
}
c.Set(fiber.HeaderContentType, "text/html")
converter := services.GotenbergConverter{BaseUrl: h.Config.GotenbergBaseUrl}
pdf, err := converter.ToPdf(result, "a4", false)
pdf, err := h.Converter.ToPdf(result, "a4", false)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": err.Error(),
@@ -69,6 +69,7 @@ func (h *DefaultHandler) GenerateCertificate(c *fiber.Ctx) error {
}
c.Set(fiber.HeaderContentType, "application/pdf")
c.Set(fiber.HeaderContentDisposition, "attachment; filename=certificate.pdf")
return c.Send(pdf)
}

View File

@@ -5,18 +5,19 @@ import (
"slices"
"git.odit.services/lfk/document-server/models"
"git.odit.services/lfk/document-server/services"
"github.com/gofiber/fiber/v2"
)
// GenerateContract godoc
// @Summary Generate a contract
// @Description Generate a contract based on the provided data
// @Tags contracts
// @Accept json
// @Param data body models.ContractRequest true "Contract data"
// @Produce application/pdf
// @Router /contracts [post]
//
// @Summary Generate a contract
// @Description Generate a contract based on the provided data
// @Tags pdfs
// @Accept json
// @Param data body models.ContractRequest true "Contract data"
// @Produce application/pdf
// @Security ApiKeyAuth
// @Router /v1/pdfs/contracts [post]
func (h *DefaultHandler) GenerateContract(c *fiber.Ctx) error {
contract := new(models.ContractRequest)
if err := c.BodyParser(contract); err != nil {
@@ -32,15 +33,14 @@ func (h *DefaultHandler) GenerateContract(c *fiber.Ctx) error {
contract.Runners = repeatRunnerArrayItems(contract.Runners, 2)
generator := services.DefaultTemplater{}
templateString, err := services.GetTemplate(contract.Locale, "contract")
templateString, err := h.StaticService.GetTemplate(contract.Locale, "contract")
if err != nil {
log.Println(err)
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Template not found",
})
}
template, err := generator.StringToTemplate(templateString)
template, err := h.Templater.StringToTemplate(templateString)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": err.Error(),
@@ -57,15 +57,14 @@ func (h *DefaultHandler) GenerateContract(c *fiber.Ctx) error {
BarcodePrefix: h.Config.SponsoringBarcodePrefix,
}
result, err := generator.Execute(template, genConfig)
result, err := h.Templater.Execute(template, genConfig)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": err.Error(),
})
}
converter := services.GotenbergConverter{BaseUrl: h.Config.GotenbergBaseUrl}
pdf, err := converter.ToPdf(result, "a5", true)
pdf, err := h.Converter.ToPdf(result, "a5", true)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": err.Error(),
@@ -73,6 +72,7 @@ func (h *DefaultHandler) GenerateContract(c *fiber.Ctx) error {
}
c.Set(fiber.HeaderContentType, "application/pdf")
c.Set(fiber.HeaderContentDisposition, "attachment; filename=runner-contracts.pdf")
return c.Send(pdf)
}

View File

@@ -2,6 +2,7 @@ package handlers
import (
"git.odit.services/lfk/document-server/models"
"git.odit.services/lfk/document-server/services"
"github.com/gofiber/fiber/v2"
)
@@ -9,8 +10,13 @@ type Handler interface {
GenerateCard(*fiber.Ctx) error
GenerateContract(*fiber.Ctx) error
GenerateCertificate(*fiber.Ctx) error
GenerateBarcode(*fiber.Ctx) error
}
type DefaultHandler struct {
Config *models.Config
Config *models.Config
BarcodeService services.BarcodeService
Templater services.Templater
Converter services.Converter
StaticService services.StaticService
}

75
main.go
View File

@@ -8,9 +8,12 @@ import (
"git.odit.services/lfk/document-server/docs" // Correct import path for docs
"git.odit.services/lfk/document-server/handlers"
"git.odit.services/lfk/document-server/models"
"git.odit.services/lfk/document-server/services"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/gofiber/fiber/v2/middleware/keyauth"
"github.com/gofiber/swagger"
"github.com/redis/go-redis/v9"
"github.com/spf13/viper"
)
@@ -30,11 +33,21 @@ func validateAPIKey(c *fiber.Ctx, key string) (bool, error) {
func loadEnv() error {
viper.SetDefault("PRODUCION", true)
viper.SetDefault("PORT", "3000")
viper.SetDefault("APIKEY", "lfk")
viper.SetDefault("EVENTNAME", "Demo Event")
viper.SetDefault("CURRENCYSYMBOL", "€")
viper.SetDefault("CARD_SUBTITLE", "Runner Card")
viper.SetDefault("CARD_BARCODEFORMAT", "ean13")
viper.SetDefault("CARD_BARCODEPREFIX", "")
viper.SetDefault("SPONSORING_RECEIPTMINIMUM", 0)
viper.SetDefault("SPONSORING_DISCLAIMER", "Disclaimer")
viper.SetDefault("SPONSORING_BARCODEFORMAT", "code128")
viper.SetDefault("SPONSORING_BARCODEPREFIX", "")
viper.SetDefault("CERTIFICATE_FOOTER", "Footer")
viper.SetDefault("GOTENBERG_BASEURL", "")
viper.SetDefault("REDIS_ADDR", "")
// Load .env file
viper.SetConfigFile(".env")
@@ -43,7 +56,7 @@ func loadEnv() error {
viper.AutomaticEnv()
err := viper.ReadInConfig()
if err != nil {
return err
log.Println("No .env file found")
}
// Unmarshal the config from file and env into the config struct
@@ -52,14 +65,21 @@ func loadEnv() error {
return err
}
log.Printf("Loaded config: %+v\n", config)
return nil
}
// @title LfK Document Server API
// @description This is the API documentation for the LfK Document Server - a tool for pdf generation.
// @securityDefinitions.apiKey ApiKeyAuth
// @in query
// @name key
// @title LfK Document Server API
// @description This is the API documentation for the LfK Document Server - a tool for pdf generation.
// @license.name CC BY-NC-SA 4.0
// @termsOfService https://lauf-fuer-kaya.de/datenschutz
// @contact.name ODIT.Services UG (haftungsbeschränkt)
// @contact.url https://odit.services
// @contact.email info@odit.services
// @securityDefinitions.apiKey ApiKeyAuth
// @in query
// @name key
func main() {
err := loadEnv()
@@ -67,8 +87,31 @@ func main() {
log.Fatal(err)
}
var redisClient *redis.Client
if config.RedisAddr != "" {
log.Println("Using redis at", config.RedisAddr)
redisClient = redis.NewClient(&redis.Options{
Addr: config.RedisAddr,
})
}
barcodeGenerator := &services.DefaultBarcodeService{
RedisClient: redisClient,
}
staticService := &services.DefaultStaticService{
Cache: make(map[string]string),
}
handler := handlers.DefaultHandler{
Config: config,
Config: config,
BarcodeService: barcodeGenerator,
StaticService: staticService,
Templater: &services.DefaultTemplater{
BarcodeService: barcodeGenerator,
StaticService: staticService,
},
Converter: &services.GotenbergConverter{
BaseUrl: config.GotenbergBaseUrl,
},
}
// Create a new Fiber instance
@@ -76,22 +119,24 @@ func main() {
Prefork: config.Prod,
})
app.Use(cors.New())
// Swagger documentation route
app.Get("/swagger/*", swagger.HandlerDefault)
// @Security ApiKeyAuth
v1 := app.Group("/v1")
v1.Use(keyauth.New(keyauth.Config{
pdfv1 := v1.Group("/pdfs")
pdfv1.Use(keyauth.New(keyauth.Config{
KeyLookup: "query:key",
Validator: validateAPIKey,
}))
v1.Get("/", func(c *fiber.Ctx) error {
return c.SendString("Hello, World!")
})
v1.Post("/contracts", handler.GenerateContract)
v1.Post("/cards", handler.GenerateCard)
v1.Post("/certificates", handler.GenerateCertificate)
pdfv1.Post("/contracts", handler.GenerateContract)
pdfv1.Post("/cards", handler.GenerateCard)
pdfv1.Post("/certificates", handler.GenerateCertificate)
v1.Get("/barcodes/:type/:content", handler.GenerateBarcode)
app.Use(handler.NotFoundHandler)
docs.SwaggerInfo.BasePath = "/"

View File

@@ -15,4 +15,5 @@ type Config struct {
SponsoringBarcodePrefix string `mapstructure:"SPONSORING_BARCODEPREFIX"`
CertificateFooter string `mapstructure:"CERTIFICATE_FOOTER"`
GotenbergBaseUrl string `mapstructure:"GOTENBERG_BASEURL"`
RedisAddr string `mapstructure:"REDIS_ADDR"`
}

View File

@@ -14,9 +14,7 @@ type Runner struct {
}
type Group struct {
ID int `json:"id" validate:"required"`
Name string `json:"name" validate:"required"`
ParentGroup *Group `json:"parent_group" validate:"optional"`
Name string `json:"name" validate:"required"`
}
type ContractTemplateOptions struct {

109
services/barcode.go Normal file
View File

@@ -0,0 +1,109 @@
package services
import (
"bytes"
"context"
"fmt"
"image"
"image/color"
"image/draw"
"image/png"
"log"
"slices"
"time"
"github.com/boombuler/barcode"
"github.com/boombuler/barcode/code128"
"github.com/boombuler/barcode/ean"
"github.com/boombuler/barcode/qr"
"github.com/redis/go-redis/v9"
)
type BarcodeService interface {
GenerateBarcode(format string, content string, width int, height int, padding int) (bytes.Buffer, error)
IsTypeSupported(format string) bool
}
type DefaultBarcodeService struct {
RedisClient *redis.Client
}
func (b *DefaultBarcodeService) GenerateBarcode(format string, content string, width int, height int, padding int) (bytes.Buffer, error) {
ctx := context.Background()
if !b.IsTypeSupported(format) {
return bytes.Buffer{}, fmt.Errorf("unsupported barcode type: %s", format)
}
if b.RedisClient != nil {
cachedBarcode, err := b.RedisClient.Get(ctx, fmt.Sprintf("barcode:%s:%s:%d:%d:%d", format, content, width, height, padding)).Result()
if err == nil {
log.Printf("Cache hit for barcode:%s:%s:%d:%d", format, content, width, height)
buf := bytes.Buffer{}
buf.Write([]byte(cachedBarcode))
return buf, nil
}
}
var generatedCode barcode.Barcode
var err error
switch format {
case "ean13":
generatedCode, err = ean.Encode(content)
if err != nil {
return bytes.Buffer{}, err
}
break
case "code128":
generatedCode, err = code128.Encode(content)
if err != nil {
return bytes.Buffer{}, err
}
break
case "qr":
generatedCode, err = qr.Encode(content, qr.M, qr.AlphaNumeric)
if err != nil {
return bytes.Buffer{}, err
}
break
}
// Create a white background image
bg := image.NewRGBA(image.Rect(0, 0, width, height))
white := color.RGBA{255, 255, 255, 255}
draw.Draw(bg, bg.Bounds(), &image.Uniform{white}, image.Point{}, draw.Src)
// Calculate the new size for the barcode to fit within the padding
newWidth := width - 2*padding
newHeight := height - 2*padding
// Scale the barcode to the new size
scaledCode, err := barcode.Scale(generatedCode, newWidth, newHeight)
if err != nil {
return bytes.Buffer{}, err
}
// Draw the barcode on top of the white background with padding
draw.Draw(bg, scaledCode.Bounds().Add(image.Point{padding, padding}), scaledCode, image.Point{}, draw.Over)
var buf bytes.Buffer
err = png.Encode(&buf, bg)
if err != nil {
return bytes.Buffer{}, err
}
if b.RedisClient != nil {
err = b.RedisClient.Set(ctx, fmt.Sprintf("barcode:%s:%s:%d:%d", format, content, width, height), buf.String(), 10*time.Minute).Err()
if err != nil {
return bytes.Buffer{}, err
}
}
return buf, nil
}
func (b *DefaultBarcodeService) IsTypeSupported(format string) bool {
supportedTypes := []string{"ean13", "code128", "qr"}
return slices.Contains(supportedTypes, format)
}

View File

@@ -6,13 +6,7 @@ import (
"errors"
"fmt"
"html/template"
"image/png"
"strings"
"github.com/boombuler/barcode"
"github.com/boombuler/barcode/code128"
"github.com/boombuler/barcode/ean"
"github.com/boombuler/barcode/qr"
)
type Templater interface {
@@ -21,6 +15,8 @@ type Templater interface {
}
type DefaultTemplater struct {
BarcodeService BarcodeService
StaticService StaticService
}
func idToEan13(id string, prefix string) (string, error) {
@@ -37,60 +33,30 @@ func idToEan13(id string, prefix string) (string, error) {
}
func (t *DefaultTemplater) GenerateBarcode(code string, format string, prefix string) (string, error) {
var generatedCode barcode.Barcode
var err error
switch format {
case "ean13":
encodedEan, err := idToEan13(code, prefix)
if format == "ean13" {
code, err = idToEan13(code, prefix)
if err != nil {
return "", err
}
generatedCode, err = ean.Encode(encodedEan)
if err != nil {
return "", err
}
break
case "code128":
generatedCode, err = code128.Encode(prefix + code)
if err != nil {
return "", err
}
break
case "qr":
generatedCode, err = qr.Encode(prefix+code, qr.M, qr.AlphaNumeric)
if err != nil {
return "", err
}
break
default:
return "", errors.New("unknown barcode format")
}
scaledCode, err := barcode.Scale(generatedCode, 1000, 500)
if err != nil {
return "", err
}
buf, err := t.BarcodeService.GenerateBarcode(format, code, 1000, 500, 0)
var buf bytes.Buffer
err = png.Encode(&buf, scaledCode)
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(buf.Bytes()), nil
return base64.StdEncoding.EncodeToString(buf.Bytes()), err
}
func (t *DefaultTemplater) SelectSponsorImage(id int) (string, error) {
sponsors, err := ListFilesInStaticSubFolder("images/sponsors")
sponsors, err := t.StaticService.ListFilesInStaticSubFolder("images/sponsors")
if err != nil {
return "", err
}
return GetImage("sponsors/" + strings.TrimSuffix(sponsors[id%len(sponsors)], ".base64")), nil
return t.StaticService.GetImage("sponsors/" + strings.TrimSuffix(sponsors[id%len(sponsors)], ".base64")), nil
}
func (t *DefaultTemplater) LoadImage(name string) (string, error) {
return GetImage(name), nil
return t.StaticService.GetImage(name), nil
}
func (t *DefaultTemplater) FormatUnit(unit string, locale string, amount int) (string, error) {

View File

@@ -7,16 +7,35 @@ import (
"os"
)
func GetTemplate(locale, templateName string) (string, error) {
type StaticService interface {
GetTemplate(locale, templateName string) (string, error)
ListFilesInStaticSubFolder(folderName string) ([]string, error)
GetImage(imageName string) string
}
type DefaultStaticService struct {
Cache map[string]string
}
func (s *DefaultStaticService) GetTemplate(locale, templateName string) (string, error) {
if s.Cache[locale+templateName] != "" {
log.Printf("returning cached template %s with locale %s", templateName, locale)
return s.Cache[locale+templateName], nil
}
content, err := os.ReadFile(fmt.Sprintf("static/templates/%s/%s.html", templateName, locale))
if content == nil || err != nil {
log.Printf("error reading template %s with locale %s: %v", templateName, locale, err)
return "", err
}
s.Cache[locale+templateName] = string(content)
return string(content), nil
}
func ListFilesInStaticSubFolder(folderName string) ([]string, error) {
func (s *DefaultStaticService) ListFilesInStaticSubFolder(folderName string) ([]string, error) {
files, err := os.ReadDir(fmt.Sprintf("static/%s", folderName))
if err != nil {
log.Printf("error reading files from folder %s: %v", folderName, err)
@@ -32,7 +51,7 @@ func ListFilesInStaticSubFolder(folderName string) ([]string, error) {
return images, nil
}
func GetImage(imageName string) string {
func (s *DefaultStaticService) GetImage(imageName string) string {
content, err := os.ReadFile("static/images/" + imageName + ".base64")
if content == nil || err != nil {
log.Printf("error reading image %s: %v", imageName, err)