first commit

This commit is contained in:
Tomáš Dvořák
2025-10-02 12:39:28 +02:00
commit 0fc92f8464
60 changed files with 11834 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
*.sqlite
*.db
logos/
.git
.gitignore
README.md
+30
View File
@@ -0,0 +1,30 @@
# Binaries
*.exe
*.exe~
*.dll
*.so
*.dylib
main
# Test binary
*.test
# Output of the go coverage tool
*.out
# Database files
*.sqlite
*.db
# Logos directory
logos/
# Go workspace file
go.work
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
+36
View File
@@ -0,0 +1,36 @@
# Build stage
FROM golang:1.21-alpine AS builder
WORKDIR /app
# Install build dependencies
RUN apk add --no-cache gcc musl-dev sqlite-dev
# Copy go mod files
COPY go.mod go.sum* ./
RUN go mod download
# Copy source code
COPY . .
# Build the application
RUN CGO_ENABLED=1 GOOS=linux go build -a -installsuffix cgo -o main .
# Runtime stage
FROM alpine:latest
RUN apk --no-cache add ca-certificates sqlite-libs
WORKDIR /root/
# Copy the binary from builder
COPY --from=builder /app/main .
# Create directories
RUN mkdir -p /root/logos
# Expose port
EXPOSE 8080
# Run the application
CMD ["./main"]
+242
View File
@@ -0,0 +1,242 @@
# 🇨🇿 Czech Clubs Logos API - Backend
A Go backend API for serving high-quality, transparent background logos of Czech football & futsal clubs. Logos are mapped by FAČR UUIDs to ensure consistency across projects.
## 🔧 Tech Stack
- **Go 1.21+** - Fast and efficient backend
- **Gin** - Web framework
- **SQLite** - Lightweight database
- **FAČR API** - External club data source
## ✨ Features
- ⚽ Fetch Czech clubs metadata from FAČR Scraper API
- 🖼️ Upload & store full-quality transparent logos (SVG/PNG)
- 🔄 Reuse FAČR UUID as a unique identifier
- 🌐 Serve logos through a simple CDN-style API
- 📝 Optional metadata (club name, city, colors, competition)
- 📦 Self-hosted with local or cloud storage
## 🚀 Quick Start
### Prerequisites
- Go 1.21 or higher
- GCC (for SQLite compilation)
### Installation
1. Navigate to the backend directory:
```bash
cd backend
```
2. Install dependencies:
```bash
go mod download
```
3. Run the server:
```bash
go run .
```
The API will start on `http://localhost:8080`
## 🐳 Docker
### Build and run with Docker:
```bash
docker build -t czech-clubs-backend .
docker run -p 8080:8080 -v $(pwd)/logos:/root/logos czech-clubs-backend
```
### Or use Docker Compose (recommended):
From the root directory:
```bash
docker-compose up
```
## 📂 Project Structure
```
backend/
├── main.go # Application entrypoint
├── handlers.go # API route handlers
├── facr_client.go # FAČR API client
├── go.mod # Go dependencies
├── go.sum # Dependency checksums
├── Dockerfile # Docker configuration
└── README.md # This file
```
## 🚀 API Endpoints
### Health Check
```
GET /health
```
Returns server health status.
### Search Clubs
```
GET /clubs/search?q=sparta
```
Search for clubs by name. Proxies to FAČR API with fallback to demo data.
**Response:**
```json
[
{
"id": "22222222-3333-4444-5555-666666666666",
"name": "AC Sparta Praha",
"city": "Praha",
"type": "football"
}
]
```
### Get Club Info
```
GET /clubs/:id
```
Get detailed club information by UUID.
### Upload Logo
```
POST /logos/:id
```
Upload a logo for a specific club UUID.
**Parameters:**
- `file` (multipart/form-data) - SVG or PNG file
**Example with curl:**
```bash
curl -X POST http://localhost:8080/logos/22222222-3333-4444-5555-666666666666 \
-F "file=@sparta.svg"
```
**Response:**
```json
{
"success": true,
"id": "22222222-3333-4444-5555-666666666666",
"filename": "22222222-3333-4444-5555-666666666666.svg",
"size": 12345,
"message": "logo uploaded successfully"
}
```
### Get Logo
```
GET /logos/:id
```
Retrieve a logo by UUID. Returns the image file.
**Response Headers:**
- `Content-Type`: `image/svg+xml` or `image/png`
- `Cache-Control`: `public, max-age=31536000`
### Get Logo with Metadata
```
GET /logos/:id/json
```
Get logo metadata in JSON format.
**Response:**
```json
{
"id": "22222222-3333-4444-5555-666666666666",
"club_name": "AC Sparta Praha",
"club_city": "Praha",
"club_type": "football",
"logo_url": "http://localhost:8080/logos/22222222-3333-4444-5555-666666666666",
"file_size": 12345,
"created_at": "2024-01-01T12:00:00Z",
"updated_at": "2024-01-01T12:00:00Z"
}
```
## 📊 Database Schema
### logos table
| Column | Type | Description |
|---------------|----------|--------------------------------|
| id | TEXT | UUID (Primary Key) |
| club_name | TEXT | Club name |
| club_city | TEXT | City |
| club_type | TEXT | Type (football/futsal) |
| file_extension| TEXT | .svg or .png |
| file_size | INTEGER | File size in bytes |
| created_at | DATETIME | Creation timestamp |
| updated_at | DATETIME | Last update timestamp |
## 🔌 External APIs
### FAČR Scraper API
- **Base URL:** `https://facr.tdvorak.dev`
- **Endpoints Used:**
- `/club/search?q={query}` - Search clubs
- `/club/{id}` - Get club details
## 🔒 Security Features
- UUID format validation
- File type validation (SVG/PNG only)
- CORS enabled for frontend integration
- Input sanitization
## 🌟 Environment Variables
| Variable | Default | Description |
|----------|---------|----------------------|
| PORT | 8080 | Server port |
## 📝 Example Workflow
1. **Search for a club:**
```bash
curl "http://localhost:8080/clubs/search?q=Slavia"
```
2. **Upload logo for club:**
```bash
curl -X POST http://localhost:8080/logos/11111111-2222-3333-4444-555555555555 \
-F "file=@slavia.svg"
```
3. **Get logo:**
```bash
curl http://localhost:8080/logos/11111111-2222-3333-4444-555555555555 \
-o slavia.svg
```
4. **Get logo with metadata:**
```bash
curl http://localhost:8080/logos/11111111-2222-3333-4444-555555555555/json
```
## 🔮 Future Enhancements
- ✍️ Admin authentication
- 🎨 Auto background remover integration
- 🔎 Advanced logo search capabilities
- 📦 Cloud storage support (S3, R2, Supabase)
- 🗄️ PostgreSQL support
## 🤝 Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
## 📄 License
This project is part of the Czech Clubs Logos API system.
---
Built with ❤️ for Czech Football
+165
View File
@@ -0,0 +1,165 @@
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
)
const FACR_API_BASE = "https://facr.tdvorak.dev"
type FACRClient struct {
httpClient *http.Client
}
func NewFACRClient() *FACRClient {
return &FACRClient{
httpClient: &http.Client{
Timeout: 10 * time.Second,
},
}
}
// Club represents a club from the FAČR API
type Club struct {
ID string `json:"id"`
Name string `json:"name"`
City string `json:"city,omitempty"`
Type string `json:"type,omitempty"`
Website string `json:"website,omitempty"`
}
// FACRSearchResponse represents the search response from FAČR API
type FACRSearchResponse struct {
Query string `json:"query"`
Count int `json:"count"`
Results []FACRSearchResult `json:"results"`
}
// FACRSearchResult represents a single search result from FAČR API
type FACRSearchResult struct {
Name string `json:"name"`
ClubID string `json:"club_id"`
ClubType string `json:"club_type"`
URL string `json:"url"`
LogoURL string `json:"logo_url"`
Category string `json:"category"`
Address string `json:"address"`
}
// FACRClubResponse represents the club details response from FAČR API
type FACRClubResponse struct {
Name string `json:"name"`
ClubID string `json:"club_id"`
ClubType string `json:"club_type"`
ClubInternalID string `json:"club_internal_id"`
URL string `json:"url"`
LogoURL string `json:"logo_url"`
Address string `json:"address"`
Category string `json:"category"`
}
// SearchClubs searches for clubs by query
func (c *FACRClient) SearchClubs(query string) ([]Club, error) {
url := fmt.Sprintf("%s/club/search?q=%s", FACR_API_BASE, query)
resp, err := c.httpClient.Get(url)
if err != nil {
return nil, fmt.Errorf("failed to fetch from FAČR API: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("FAČR API returned status %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
var searchResp FACRSearchResponse
if err := json.Unmarshal(body, &searchResp); err != nil {
return nil, fmt.Errorf("failed to parse response: %w", err)
}
// Convert FACR results to our Club format
clubs := make([]Club, 0, len(searchResp.Results))
for _, result := range searchResp.Results {
// Extract city from address if available
city := extractCityFromAddress(result.Address)
clubs = append(clubs, Club{
ID: result.ClubID,
Name: result.Name,
City: city,
Type: result.ClubType,
Website: "", // Not provided in search results
})
}
return clubs, nil
}
// extractCityFromAddress extracts city name from address string
// Address format: "Street, PostalCode City"
func extractCityFromAddress(address string) string {
if address == "" {
return ""
}
// Try to extract city after postal code (format: "Street, 12345 City")
parts := strings.Split(address, ",")
if len(parts) < 2 {
return ""
}
// Get the part after comma and split by space
lastPart := strings.TrimSpace(parts[len(parts)-1])
words := strings.Fields(lastPart)
if len(words) >= 2 {
// Skip postal code (first word) and return the rest as city
return strings.Join(words[1:], " ")
}
return ""
}
// GetClub gets a club by ID
func (c *FACRClient) GetClub(id string) (*Club, error) {
// Try football first, then futsal
url := fmt.Sprintf("%s/club/football/%s", FACR_API_BASE, id)
resp, err := c.httpClient.Get(url)
if err != nil {
return nil, fmt.Errorf("failed to fetch from FAČR API: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("FAČR API returned status %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
var clubResp FACRClubResponse
if err := json.Unmarshal(body, &clubResp); err != nil {
return nil, fmt.Errorf("failed to parse response: %w", err)
}
// Extract city from address
city := extractCityFromAddress(clubResp.Address)
club := &Club{
ID: clubResp.ClubID,
Name: clubResp.Name,
City: city,
Type: clubResp.ClubType,
Website: "", // Not provided in FACR API
}
return club, nil
}
+39
View File
@@ -0,0 +1,39 @@
module czech-clubs-logos-api
go 1.21
require (
github.com/gin-contrib/cors v1.7.0
github.com/gin-gonic/gin v1.9.1
github.com/google/uuid v1.5.0
github.com/mattn/go-sqlite3 v1.14.19
)
require (
github.com/bytedance/sonic v1.11.2 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
github.com/chenzhuoyu/iasm v0.9.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.19.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.7.0 // indirect
golang.org/x/crypto v0.21.0 // indirect
golang.org/x/net v0.22.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+104
View File
@@ -0,0 +1,104 @@
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
github.com/bytedance/sonic v1.11.2 h1:ywfwo0a/3j9HR8wsYGWsIWl2mvRsI950HyoxiBERw5A=
github.com/bytedance/sonic v1.11.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0=
github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
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/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gin-contrib/cors v1.7.0 h1:wZX2wuZ0o7rV2/1i7gb4Jn+gW7HBqaP91fizJkBUJOA=
github.com/gin-contrib/cors v1.7.0/go.mod h1:cI+h6iOAyxKRtUtC6iF/Si1KSFvGm/gK+kshxlCi8ro=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn0+wvQ3bZ8b/AU4=
github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI=
github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
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/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc=
golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
+468
View File
@@ -0,0 +1,468 @@
package main
import (
"database/sql"
"fmt"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
var facrClient = NewFACRClient()
// ==================== Club Handlers ====================
func searchClubs(c *gin.Context) {
query := c.Query("q")
if query == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "query parameter 'q' is required"})
return
}
clubs, err := facrClient.SearchClubs(query)
if err != nil {
// Return demo data if FAČR API is unavailable
c.JSON(http.StatusOK, getDemoClubs(query))
return
}
c.JSON(http.StatusOK, clubs)
}
func getClub(c *gin.Context) {
id := c.Param("id")
if id == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "club ID is required"})
return
}
club, err := facrClient.GetClub(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "club not found"})
return
}
c.JSON(http.StatusOK, club)
}
// Demo data fallback
func getDemoClubs(query string) []Club {
demoClubs := []Club{
{
ID: "11111111-2222-3333-4444-555555555555",
Name: "SK Slavia Praha",
City: "Praha",
Type: "football",
Website: "https://www.slavia.cz",
},
{
ID: "22222222-3333-4444-5555-666666666666",
Name: "AC Sparta Praha",
City: "Praha",
Type: "football",
Website: "https://www.sparta.cz",
},
{
ID: "33333333-4444-5555-6666-777777777777",
Name: "FC Viktoria Plzeň",
City: "Plzeň",
Type: "football",
Website: "https://www.fcviktoria.cz",
},
{
ID: "44444444-5555-6666-7777-888888888888",
Name: "FC Baník Ostrava",
City: "Ostrava",
Type: "football",
Website: "https://www.fcb.cz",
},
{
ID: "55555555-6666-7777-8888-999999999999",
Name: "SK Sigma Olomouc",
City: "Olomouc",
Type: "football",
Website: "https://www.sigmafotbal.cz",
},
}
var results []Club
lowerQuery := strings.ToLower(query)
for _, club := range demoClubs {
if strings.Contains(strings.ToLower(club.Name), lowerQuery) {
results = append(results, club)
}
}
return results
}
// ==================== Logo Handlers ====================
type LogoMetadata struct {
ID string `json:"id"`
ClubName string `json:"club_name"`
ClubCity string `json:"club_city,omitempty"`
ClubType string `json:"club_type,omitempty"`
ClubWebsite string `json:"club_website,omitempty"`
HasSVG bool `json:"has_svg"`
HasPNG bool `json:"has_png"`
PrimaryFormat string `json:"primary_format"`
LogoURL string `json:"logo_url"`
LogoURLSVG string `json:"logo_url_svg,omitempty"`
LogoURLPNG string `json:"logo_url_png,omitempty"`
FileSizeSVG int64 `json:"file_size_svg,omitempty"`
FileSizePNG int64 `json:"file_size_png,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// getLogo returns the logo file (PNG preferred, SVG fallback)
func getLogo(c *gin.Context) {
id := c.Param("id")
if id == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "logo ID is required"})
return
}
// Validate UUID format
if _, err := uuid.Parse(id); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid UUID format"})
return
}
// Check format preference from query
format := c.Query("format") // can be "svg" or "png"
var logoPath string
var contentType string
var found bool
// Try PNG first (primary format)
if format == "" || format == "png" {
pngPath := filepath.Join("./logos/png", id+".png")
if _, err := os.Stat(pngPath); err == nil {
logoPath = pngPath
contentType = "image/png"
found = true
}
}
// Try SVG if PNG not found or explicitly requested
if !found && (format == "" || format == "svg") {
svgPath := filepath.Join("./logos/svg", id+".svg")
if _, err := os.Stat(svgPath); err == nil {
logoPath = svgPath
contentType = "image/svg+xml"
found = true
}
}
if !found {
c.JSON(http.StatusNotFound, gin.H{"error": "logo not found"})
return
}
c.Header("Content-Type", contentType)
c.Header("Cache-Control", "public, max-age=31536000")
c.File(logoPath)
}
func getLogoWithMetadata(c *gin.Context) {
id := c.Param("id")
if id == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "logo ID is required"})
return
}
// Validate UUID format
if _, err := uuid.Parse(id); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid UUID format"})
return
}
// Get metadata from database
var metadata LogoMetadata
var hasSVG, hasPNG int
err := db.QueryRow(`
SELECT id, club_name, club_city, club_type, club_website,
has_svg, has_png, primary_format,
file_size_svg, file_size_png,
created_at, updated_at
FROM logos WHERE id = ?
`, id).Scan(
&metadata.ID,
&metadata.ClubName,
&metadata.ClubCity,
&metadata.ClubType,
&metadata.ClubWebsite,
&hasSVG,
&hasPNG,
&metadata.PrimaryFormat,
&metadata.FileSizeSVG,
&metadata.FileSizePNG,
&metadata.CreatedAt,
&metadata.UpdatedAt,
)
if err == sql.ErrNoRows {
c.JSON(http.StatusNotFound, gin.H{"error": "logo not found"})
return
}
if err != nil {
log.Printf("Database error: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "database error"})
return
}
metadata.HasSVG = hasSVG == 1
metadata.HasPNG = hasPNG == 1
// Construct logo URLs
scheme := "http"
if c.Request.TLS != nil {
scheme = "https"
}
baseURL := fmt.Sprintf("%s://%s", scheme, c.Request.Host)
// Primary URL (PNG preferred)
if metadata.HasPNG {
metadata.LogoURL = fmt.Sprintf("%s/logos/%s?format=png", baseURL, id)
} else if metadata.HasSVG {
metadata.LogoURL = fmt.Sprintf("%s/logos/%s?format=svg", baseURL, id)
}
// Format-specific URLs
if metadata.HasSVG {
metadata.LogoURLSVG = fmt.Sprintf("%s/logos/%s?format=svg", baseURL, id)
}
if metadata.HasPNG {
metadata.LogoURLPNG = fmt.Sprintf("%s/logos/%s?format=png", baseURL, id)
}
c.JSON(http.StatusOK, metadata)
}
// List all logos
func listLogos(c *gin.Context) {
rows, err := db.Query(`
SELECT id, club_name, club_city, club_type, club_website,
has_svg, has_png, primary_format,
created_at, updated_at
FROM logos
ORDER BY club_name
`)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "database error"})
return
}
defer rows.Close()
var logos []LogoMetadata
scheme := "http"
if c.Request.TLS != nil {
scheme = "https"
}
baseURL := fmt.Sprintf("%s://%s", scheme, c.Request.Host)
for rows.Next() {
var logo LogoMetadata
var hasSVG, hasPNG int
err := rows.Scan(
&logo.ID,
&logo.ClubName,
&logo.ClubCity,
&logo.ClubType,
&logo.ClubWebsite,
&hasSVG,
&hasPNG,
&logo.PrimaryFormat,
&logo.CreatedAt,
&logo.UpdatedAt,
)
if err != nil {
continue
}
logo.HasSVG = hasSVG == 1
logo.HasPNG = hasPNG == 1
if logo.HasPNG {
logo.LogoURL = fmt.Sprintf("%s/logos/%s?format=png", baseURL, logo.ID)
} else if logo.HasSVG {
logo.LogoURL = fmt.Sprintf("%s/logos/%s?format=svg", baseURL, logo.ID)
}
logos = append(logos, logo)
}
c.JSON(http.StatusOK, logos)
}
func uploadLogo(c *gin.Context) {
id := c.Param("id")
if id == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "logo ID is required"})
return
}
// Validate UUID format
if _, err := uuid.Parse(id); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid UUID format"})
return
}
// Get club name from form (required)
clubName := c.PostForm("club_name")
if clubName == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "club_name is required"})
return
}
// Optional fields
clubCity := c.PostForm("club_city")
clubType := c.PostForm("club_type")
clubWebsite := c.PostForm("club_website")
// Get uploaded file
file, err := c.FormFile("file")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "no file provided"})
return
}
// Validate file type
ext := strings.ToLower(filepath.Ext(file.Filename))
if ext != ".svg" && ext != ".png" && ext != ".pdf" {
c.JSON(http.StatusBadRequest, gin.H{"error": "only .svg, .png and .pdf files are allowed"})
return
}
// Determine storage paths
var svgPath, pngPath string
var hasSVG, hasPNG int
var sizeSVG, sizePNG int64
if ext == ".svg" || ext == ".pdf" {
pngPath = filepath.Join("./logos/png", id+".png")
if ext == ".svg" {
svgPath = filepath.Join("./logos/svg", id+".svg")
// Save SVG
if err := c.SaveUploadedFile(file, svgPath); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save SVG file"})
return
}
// Get SVG file size
if stat, err := os.Stat(svgPath); err == nil {
sizeSVG = stat.Size()
}
hasSVG = 1
// Convert SVG to PNG
log.Printf("Converting SVG to PNG for club: %s", clubName)
if err := ConvertSVGToPNG(svgPath, pngPath, 512); err != nil {
log.Printf("Warning: Failed to convert SVG to PNG: %v", err)
// Don't fail the upload, just log the warning
} else {
// Optimize PNG
if err := OptimizePNG(pngPath); err != nil {
log.Printf("Warning: Failed to optimize PNG: %v", err)
}
// Get PNG file size
if stat, err := os.Stat(pngPath); err == nil {
sizePNG = stat.Size()
hasPNG = 1
}
}
} else {
// PDF file - convert directly to PNG
pdfTempPath := filepath.Join("./logos/temp", id+".pdf")
os.MkdirAll("./logos/temp", 0755)
if err := c.SaveUploadedFile(file, pdfTempPath); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save PDF file"})
return
}
log.Printf("Converting PDF to PNG for club: %s", clubName)
if err := ConvertPDFToPNG(pdfTempPath, pngPath, 512); err != nil {
log.Printf("Error: Failed to convert PDF to PNG: %v", err)
os.Remove(pdfTempPath)
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to convert PDF to PNG"})
return
}
// Clean up temp PDF
os.Remove(pdfTempPath)
// Optimize PNG
if err := OptimizePNG(pngPath); err != nil {
log.Printf("Warning: Failed to optimize PNG: %v", err)
}
// Get PNG file size
if stat, err := os.Stat(pngPath); err == nil {
sizePNG = stat.Size()
hasPNG = 1
}
}
} else {
// PNG upload
pngPath = filepath.Join("./logos/png", id+".png")
if err := c.SaveUploadedFile(file, pngPath); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save PNG file"})
return
}
// Optimize PNG
if err := OptimizePNG(pngPath); err != nil {
log.Printf("Warning: Failed to optimize PNG: %v", err)
}
// Get PNG file size
if stat, err := os.Stat(pngPath); err == nil {
sizePNG = stat.Size()
}
hasPNG = 1
}
// Save metadata to database
_, err = db.Exec(`
INSERT OR REPLACE INTO logos (
id, club_name, club_city, club_type, club_website,
has_svg, has_png, primary_format,
file_size_svg, file_size_png, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, 'png', ?, ?, CURRENT_TIMESTAMP)
`, id, clubName, clubCity, clubType, clubWebsite, hasSVG, hasPNG, sizeSVG, sizePNG)
if err != nil {
log.Printf("Database error: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save metadata"})
return
}
response := gin.H{
"success": true,
"id": id,
"club_name": clubName,
"has_svg": hasSVG == 1,
"has_png": hasPNG == 1,
"size_svg": sizeSVG,
"size_png": sizePNG,
"message": "logo uploaded successfully",
}
c.JSON(http.StatusOK, response)
}
+175
View File
@@ -0,0 +1,175 @@
package main
import (
"bytes"
"fmt"
"image"
"image/png"
"io"
"os"
"os/exec"
"path/filepath"
)
// ConvertSVGToPNG converts an SVG file to PNG format
// Uses ImageMagick/Inkscape if available, otherwise returns error
func ConvertSVGToPNG(svgPath, pngPath string, width int) error {
// Try using ImageMagick convert command
if err := convertWithImageMagick(svgPath, pngPath, width); err == nil {
return nil
}
// Try using Inkscape
if err := convertWithInkscape(svgPath, pngPath, width); err == nil {
return nil
}
// If no converter available, copy SVG as fallback and log warning
return fmt.Errorf("no SVG converter available (install ImageMagick or Inkscape)")
}
// ConvertPDFToPNG converts a PDF file to PNG format
// Uses ImageMagick/Ghostscript if available, otherwise returns error
func ConvertPDFToPNG(pdfPath, pngPath string, width int) error {
// Try using ImageMagick convert command (requires Ghostscript)
cmd := exec.Command("convert",
"-background", "none",
"-density", "300",
"-resize", fmt.Sprintf("%dx%d", width, width),
fmt.Sprintf("%s[0]", pdfPath), // Only first page
pngPath,
)
var stderr bytes.Buffer
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("PDF conversion failed (install ImageMagick and Ghostscript): %v - %s", err, stderr.String())
}
return nil
}
func convertWithImageMagick(svgPath, pngPath string, width int) error {
cmd := exec.Command("convert",
"-background", "none",
"-density", "300",
"-resize", fmt.Sprintf("%dx%d", width, width),
svgPath,
pngPath,
)
var stderr bytes.Buffer
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("imagemagick conversion failed: %v - %s", err, stderr.String())
}
return nil
}
func convertWithInkscape(svgPath, pngPath string, width int) error {
cmd := exec.Command("inkscape",
"--export-type=png",
fmt.Sprintf("--export-filename=%s", pngPath),
fmt.Sprintf("--export-width=%d", width),
svgPath,
)
var stderr bytes.Buffer
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("inkscape conversion failed: %v - %s", err, stderr.String())
}
return nil
}
// OptimizePNG optimizes a PNG file (basic implementation)
func OptimizePNG(pngPath string) error {
// Open the file
file, err := os.Open(pngPath)
if err != nil {
return err
}
defer file.Close()
// Decode the image
img, _, err := image.Decode(file)
if err != nil {
return err
}
// Create temp file
tempPath := pngPath + ".tmp"
tempFile, err := os.Create(tempPath)
if err != nil {
return err
}
defer tempFile.Close()
// Encode with compression
encoder := png.Encoder{
CompressionLevel: png.BestCompression,
}
if err := encoder.Encode(tempFile, img); err != nil {
os.Remove(tempPath)
return err
}
// Replace original with optimized
if err := os.Rename(tempPath, pngPath); err != nil {
os.Remove(tempPath)
return err
}
return nil
}
// IsSVGFile checks if the file is an SVG by reading its content
func IsSVGFile(file io.Reader) (bool, error) {
buf := make([]byte, 512)
n, err := file.Read(buf)
if err != nil && err != io.EOF {
return false, err
}
content := string(buf[:n])
return bytes.Contains([]byte(content), []byte("<svg")) ||
bytes.Contains([]byte(content), []byte("<?xml")), nil
}
// ValidateImageFile validates that a file is a valid SVG or PNG
func ValidateImageFile(filePath string) (string, error) {
ext := filepath.Ext(filePath)
file, err := os.Open(filePath)
if err != nil {
return "", err
}
defer file.Close()
if ext == ".svg" {
isSVG, err := IsSVGFile(file)
if err != nil {
return "", err
}
if !isSVG {
return "", fmt.Errorf("file is not a valid SVG")
}
return "svg", nil
}
if ext == ".png" {
_, err := png.Decode(file)
if err != nil {
return "", fmt.Errorf("file is not a valid PNG: %v", err)
}
return "png", nil
}
return "", fmt.Errorf("unsupported file format: %s", ext)
}
+124
View File
@@ -0,0 +1,124 @@
package main
import (
"database/sql"
"log"
"os"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
_ "github.com/mattn/go-sqlite3"
)
var db *sql.DB
func main() {
// Initialize database
var err error
db, err = initDB()
if err != nil {
log.Fatal("Failed to initialize database:", err)
}
defer db.Close()
// Create logos directory if it doesn't exist
if err := os.MkdirAll("./logos", 0755); err != nil {
log.Fatal("Failed to create logos directory:", err)
}
// Create subdirectories for SVG and PNG
if err := os.MkdirAll("./logos/svg", 0755); err != nil {
log.Fatal("Failed to create logos/svg directory:", err)
}
if err := os.MkdirAll("./logos/png", 0755); err != nil {
log.Fatal("Failed to create logos/png directory:", err)
}
// Initialize Gin router
r := gin.Default()
// CORS middleware
r.Use(cors.New(cors.Config{
AllowOrigins: []string{"*"},
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"},
ExposeHeaders: []string{"Content-Length"},
AllowCredentials: true,
}))
// Routes
setupRoutes(r)
// Start server
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
log.Printf("🚀 Server starting on port %s", port)
log.Printf("📁 Logos directory: ./logos")
log.Printf("💾 Database: ./data/db.sqlite")
if err := r.Run(":" + port); err != nil {
log.Fatal("Failed to start server:", err)
}
}
func setupRoutes(r *gin.Engine) {
// Health check
r.GET("/health", func(c *gin.Context) {
c.JSON(200, gin.H{"status": "ok"})
})
// Club routes (proxy to FAČR API)
clubs := r.Group("/clubs")
{
clubs.GET("/search", searchClubs)
clubs.GET("/:id", getClub)
}
// Logo routes
logos := r.Group("/logos")
{
logos.GET("", listLogos)
logos.GET("/:id", getLogo)
logos.GET("/:id/json", getLogoWithMetadata)
logos.POST("/:id", uploadLogo)
}
}
func initDB() (*sql.DB, error) {
// Create data directory if it doesn't exist
if err := os.MkdirAll("./data", 0755); err != nil {
return nil, err
}
db, err := sql.Open("sqlite3", "./data/db.sqlite")
if err != nil {
return nil, err
}
// Create tables
_, err = db.Exec(`
CREATE TABLE IF NOT EXISTS logos (
id TEXT PRIMARY KEY,
club_name TEXT NOT NULL,
club_city TEXT,
club_type TEXT,
club_website TEXT,
has_svg INTEGER DEFAULT 0,
has_png INTEGER DEFAULT 0,
primary_format TEXT DEFAULT 'png',
file_size_svg INTEGER,
file_size_png INTEGER,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`)
if err != nil {
return nil, err
}
log.Println("✓ Database initialized")
return db, nil
}