Added proper CI
All checks were successful
OpenClimb Docker Deploy / build-and-push (pull_request) Successful in 2m3s
All checks were successful
OpenClimb Docker Deploy / build-and-push (pull_request) Successful in 2m3s
This commit is contained in:
@@ -1,14 +0,0 @@
|
||||
# OpenClimb Sync Server Configuration
|
||||
|
||||
# Required: Secret token for authentication
|
||||
# Generate a secure random token and share it between your apps and server
|
||||
AUTH_TOKEN=your-secure-secret-token-here
|
||||
|
||||
# Optional: Port to run the server on (default: 8080)
|
||||
PORT=8080
|
||||
|
||||
# Optional: Path to store the sync data (default: ./data/climb_data.json)
|
||||
DATA_FILE=./data/climb_data.json
|
||||
|
||||
# Optional: Directory to store images (default: ./data/images)
|
||||
IMAGES_DIR=./data/images
|
||||
16
sync-server/.gitignore
vendored
16
sync-server/.gitignore
vendored
@@ -1,16 +0,0 @@
|
||||
# Binaries
|
||||
sync-server
|
||||
openclimb-sync
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
|
||||
# Data directory
|
||||
data/
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.local
|
||||
|
||||
# OS generated files
|
||||
.DS_Store
|
||||
303
sync-server/DEPLOY.md
Normal file
303
sync-server/DEPLOY.md
Normal file
@@ -0,0 +1,303 @@
|
||||
# OpenClimb Sync Server Deployment Guide
|
||||
|
||||
This guide covers deploying the OpenClimb Sync Server using the automated Docker build and deployment system.
|
||||
|
||||
## Overview
|
||||
|
||||
The sync server is automatically built into a Docker container via GitHub Actions and can be deployed to any Docker-compatible environment.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Docker and Docker Compose installed
|
||||
- Access to the container registry (configured in GitHub secrets)
|
||||
- Basic understanding of Docker deployments
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Automated Deployment (Recommended)
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone <your-repo-url>
|
||||
cd OpenClimb/sync-server
|
||||
|
||||
# Run the deployment script
|
||||
./deploy.sh
|
||||
```
|
||||
|
||||
The script will:
|
||||
- Create necessary directories
|
||||
- Pull the latest container image
|
||||
- Stop any existing containers
|
||||
- Start the new container
|
||||
- Verify deployment success
|
||||
|
||||
### 2. Manual Deployment
|
||||
|
||||
```bash
|
||||
# Pull the latest image
|
||||
docker pull your-registry.com/username/openclimb-sync-server:latest
|
||||
|
||||
# Create environment file
|
||||
cp .env.example .env.prod
|
||||
# Edit .env.prod with your configuration
|
||||
|
||||
# Deploy with docker-compose
|
||||
docker-compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Create a `.env.prod` file with the following variables:
|
||||
|
||||
```bash
|
||||
# Container registry settings
|
||||
REPO_HOST=your-registry.example.com
|
||||
REPO_OWNER=your-username
|
||||
|
||||
# Server configuration
|
||||
AUTH_TOKEN=your-secure-auth-token-here-make-it-long-and-random
|
||||
PORT=8080
|
||||
|
||||
# Optional: Custom domain (for Traefik)
|
||||
TRAEFIK_HOST=sync.openclimb.example.com
|
||||
```
|
||||
|
||||
### Required Secrets (GitHub)
|
||||
|
||||
Configure these secrets in your GitHub repository settings:
|
||||
|
||||
- `REPO_HOST`: Your container registry hostname
|
||||
- `DEPLOY_TOKEN`: Authentication token for the registry
|
||||
|
||||
## Container Build Process
|
||||
|
||||
The GitHub Action (`sync-server-deploy.yml`) automatically:
|
||||
|
||||
1. **Triggers on:**
|
||||
- Push to `main` branch (when sync-server files change)
|
||||
- Pull requests to `main` branch
|
||||
|
||||
2. **Build Process:**
|
||||
- Uses multi-stage Docker build
|
||||
- Compiles Go binary in builder stage
|
||||
- Creates minimal Alpine-based runtime image
|
||||
- Pushes to container registry with tags:
|
||||
- `latest` (always points to newest)
|
||||
- `<commit-sha>` (specific version)
|
||||
|
||||
3. **Caching:**
|
||||
- Uses GitHub Actions cache for faster builds
|
||||
- Incremental builds when possible
|
||||
|
||||
## Deployment Options
|
||||
|
||||
### Option 1: Simple Docker Run
|
||||
```bash
|
||||
docker run -d \
|
||||
--name openclimb-sync-server \
|
||||
-p 8080:8080 \
|
||||
-v $(pwd)/data:/root/data \
|
||||
-e AUTH_TOKEN=your-token-here \
|
||||
your-registry.com/username/openclimb-sync-server:latest
|
||||
```
|
||||
|
||||
### Option 2: Docker Compose (Recommended)
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
### Option 3: Kubernetes
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: openclimb-sync-server
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: openclimb-sync-server
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: openclimb-sync-server
|
||||
spec:
|
||||
containers:
|
||||
- name: sync-server
|
||||
image: your-registry.com/username/openclimb-sync-server:latest
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
env:
|
||||
- name: AUTH_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: openclimb-secrets
|
||||
key: auth-token
|
||||
volumeMounts:
|
||||
- name: data-volume
|
||||
mountPath: /root/data
|
||||
volumes:
|
||||
- name: data-volume
|
||||
persistentVolumeClaim:
|
||||
claimName: openclimb-data
|
||||
```
|
||||
|
||||
## Data Persistence
|
||||
|
||||
The sync server stores data in `/root/data` inside the container. **Always mount a volume** to preserve data:
|
||||
|
||||
```bash
|
||||
# Local directory mounting
|
||||
-v $(pwd)/data:/root/data
|
||||
|
||||
# Named volume (recommended for production)
|
||||
-v openclimb-data:/root/data
|
||||
```
|
||||
|
||||
### Data Structure
|
||||
```
|
||||
data/
|
||||
├── climb_data.json # Main sync data
|
||||
├── images/ # Uploaded images
|
||||
│ ├── problem_*.jpg
|
||||
│ └── ...
|
||||
└── logs/ # Server logs (optional)
|
||||
```
|
||||
|
||||
## Monitoring and Maintenance
|
||||
|
||||
### Health Check
|
||||
```bash
|
||||
curl http://localhost:8080/health
|
||||
```
|
||||
|
||||
### View Logs
|
||||
```bash
|
||||
# Docker Compose
|
||||
docker-compose -f docker-compose.prod.yml logs -f
|
||||
|
||||
# Direct Docker
|
||||
docker logs -f openclimb-sync-server
|
||||
```
|
||||
|
||||
### Update to Latest Version
|
||||
```bash
|
||||
# Using deploy script
|
||||
./deploy.sh
|
||||
|
||||
# Manual update
|
||||
docker-compose -f docker-compose.prod.yml pull
|
||||
docker-compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
## Reverse Proxy Setup (Optional)
|
||||
|
||||
### Nginx
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name sync.openclimb.example.com;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:8080;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Traefik (Labels included in docker-compose.prod.yml)
|
||||
```yaml
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.openclimb-sync.rule=Host(`sync.openclimb.example.com`)"
|
||||
- "traefik.http.routers.openclimb-sync.tls.certresolver=letsencrypt"
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **AUTH_TOKEN**: Use a long, random token (32+ characters)
|
||||
2. **HTTPS**: Always use HTTPS in production (via reverse proxy)
|
||||
3. **Firewall**: Only expose port 8080 to your reverse proxy, not publicly
|
||||
4. **Updates**: Regularly update to the latest container image
|
||||
5. **Backups**: Regularly backup the `data/` directory
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Container Won't Start
|
||||
```bash
|
||||
# Check logs
|
||||
docker logs openclimb-sync-server
|
||||
|
||||
# Common issues:
|
||||
# - Missing AUTH_TOKEN environment variable
|
||||
# - Port 8080 already in use
|
||||
# - Insufficient permissions on data directory
|
||||
```
|
||||
|
||||
### Sync Fails from Mobile Apps
|
||||
```bash
|
||||
# Verify server is accessible
|
||||
curl -H "Authorization: Bearer your-token" http://your-server:8080/sync
|
||||
|
||||
# Check server logs for authentication errors
|
||||
docker logs openclimb-sync-server | grep "401\|403"
|
||||
```
|
||||
|
||||
### Image Upload Issues
|
||||
```bash
|
||||
# Check disk space
|
||||
df -h
|
||||
|
||||
# Verify data directory permissions
|
||||
ls -la data/
|
||||
```
|
||||
|
||||
## Performance Tuning
|
||||
|
||||
For high-load deployments:
|
||||
|
||||
```yaml
|
||||
# docker-compose.prod.yml
|
||||
services:
|
||||
openclimb-sync-server:
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 512M
|
||||
cpus: '0.5'
|
||||
reservations:
|
||||
memory: 256M
|
||||
cpus: '0.25'
|
||||
```
|
||||
|
||||
## Backup Strategy
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# backup.sh - Run daily via cron
|
||||
|
||||
DATE=$(date +%Y%m%d_%H%M%S)
|
||||
BACKUP_DIR="/backups/openclimb"
|
||||
|
||||
# Create backup directory
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
# Backup data directory
|
||||
tar -czf "$BACKUP_DIR/openclimb_data_$DATE.tar.gz" \
|
||||
-C /path/to/sync-server data/
|
||||
|
||||
# Keep only last 30 days
|
||||
find "$BACKUP_DIR" -name "openclimb_data_*.tar.gz" -mtime +30 -delete
|
||||
```
|
||||
|
||||
## Support
|
||||
|
||||
- **Issues**: Create an issue in the GitHub repository
|
||||
- **Documentation**: Check the main OpenClimb README
|
||||
- **Logs**: Always
|
||||
@@ -1,14 +0,0 @@
|
||||
FROM golang:1.25-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -o sync-server .
|
||||
|
||||
FROM alpine:latest
|
||||
RUN apk --no-cache add ca-certificates
|
||||
WORKDIR /root/
|
||||
|
||||
COPY --from=builder /app/sync-server .
|
||||
|
||||
EXPOSE 8080
|
||||
CMD ["./sync-server"]
|
||||
@@ -1,14 +0,0 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
openclimb-sync:
|
||||
build: .
|
||||
ports:
|
||||
- "8080:8080"
|
||||
environment:
|
||||
- AUTH_TOKEN=${AUTH_TOKEN:-your-secret-token-here}
|
||||
- DATA_FILE=/data/climb_data.json
|
||||
- IMAGES_DIR=/data/images
|
||||
volumes:
|
||||
- ./data:/data
|
||||
restart: unless-stopped
|
||||
@@ -1,3 +0,0 @@
|
||||
module openclimb-sync
|
||||
|
||||
go 1.25
|
||||
@@ -1,358 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
type ClimbDataBackup struct {
|
||||
ExportedAt string `json:"exportedAt"`
|
||||
Version string `json:"version"`
|
||||
FormatVersion string `json:"formatVersion"`
|
||||
Gyms []BackupGym `json:"gyms"`
|
||||
Problems []BackupProblem `json:"problems"`
|
||||
Sessions []BackupClimbSession `json:"sessions"`
|
||||
Attempts []BackupAttempt `json:"attempts"`
|
||||
}
|
||||
|
||||
type BackupGym struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Location *string `json:"location,omitempty"`
|
||||
SupportedClimbTypes []string `json:"supportedClimbTypes"`
|
||||
DifficultySystems []string `json:"difficultySystems"`
|
||||
CustomDifficultyGrades []string `json:"customDifficultyGrades"`
|
||||
Notes *string `json:"notes,omitempty"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
}
|
||||
|
||||
type BackupProblem struct {
|
||||
ID string `json:"id"`
|
||||
GymID string `json:"gymId"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
ClimbType string `json:"climbType"`
|
||||
Difficulty DifficultyGrade `json:"difficulty"`
|
||||
Tags []string `json:"tags"`
|
||||
Location *string `json:"location,omitempty"`
|
||||
ImagePaths []string `json:"imagePaths,omitempty"`
|
||||
IsActive bool `json:"isActive"`
|
||||
DateSet *string `json:"dateSet,omitempty"`
|
||||
Notes *string `json:"notes,omitempty"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
}
|
||||
|
||||
type DifficultyGrade struct {
|
||||
System string `json:"system"`
|
||||
Grade string `json:"grade"`
|
||||
NumericValue int `json:"numericValue"`
|
||||
}
|
||||
|
||||
type BackupClimbSession struct {
|
||||
ID string `json:"id"`
|
||||
GymID string `json:"gymId"`
|
||||
Date string `json:"date"`
|
||||
StartTime *string `json:"startTime,omitempty"`
|
||||
EndTime *string `json:"endTime,omitempty"`
|
||||
Duration *int64 `json:"duration,omitempty"`
|
||||
Status string `json:"status"`
|
||||
Notes *string `json:"notes,omitempty"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
}
|
||||
|
||||
type BackupAttempt struct {
|
||||
ID string `json:"id"`
|
||||
SessionID string `json:"sessionId"`
|
||||
ProblemID string `json:"problemId"`
|
||||
Result string `json:"result"`
|
||||
HighestHold *string `json:"highestHold,omitempty"`
|
||||
Notes *string `json:"notes,omitempty"`
|
||||
Duration *int64 `json:"duration,omitempty"`
|
||||
RestTime *int64 `json:"restTime,omitempty"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
}
|
||||
|
||||
type SyncServer struct {
|
||||
authToken string
|
||||
dataFile string
|
||||
imagesDir string
|
||||
}
|
||||
|
||||
func (s *SyncServer) authenticate(r *http.Request) bool {
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if !strings.HasPrefix(authHeader, "Bearer ") {
|
||||
return false
|
||||
}
|
||||
|
||||
token := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
return subtle.ConstantTimeCompare([]byte(token), []byte(s.authToken)) == 1
|
||||
}
|
||||
|
||||
func (s *SyncServer) loadData() (*ClimbDataBackup, error) {
|
||||
log.Printf("Loading data from: %s", s.dataFile)
|
||||
|
||||
if _, err := os.Stat(s.dataFile); os.IsNotExist(err) {
|
||||
log.Printf("Data file does not exist, creating empty backup")
|
||||
return &ClimbDataBackup{
|
||||
ExportedAt: time.Now().UTC().Format(time.RFC3339),
|
||||
Version: "2.0",
|
||||
FormatVersion: "2.0",
|
||||
Gyms: []BackupGym{},
|
||||
Problems: []BackupProblem{},
|
||||
Sessions: []BackupClimbSession{},
|
||||
Attempts: []BackupAttempt{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(s.dataFile)
|
||||
if err != nil {
|
||||
log.Printf("Failed to read data file: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Printf("Read %d bytes from data file", len(data))
|
||||
log.Printf("File content preview: %s", string(data[:min(200, len(data))]))
|
||||
|
||||
var backup ClimbDataBackup
|
||||
if err := json.Unmarshal(data, &backup); err != nil {
|
||||
log.Printf("Failed to unmarshal JSON: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Printf("Loaded backup: gyms=%d, problems=%d, sessions=%d, attempts=%d",
|
||||
len(backup.Gyms), len(backup.Problems), len(backup.Sessions), len(backup.Attempts))
|
||||
|
||||
return &backup, nil
|
||||
}
|
||||
|
||||
func (s *SyncServer) saveData(backup *ClimbDataBackup) error {
|
||||
backup.ExportedAt = time.Now().UTC().Format(time.RFC3339)
|
||||
|
||||
data, err := json.MarshalIndent(backup, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dir := filepath.Dir(s.dataFile)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Ensure images directory exists
|
||||
if err := os.MkdirAll(s.imagesDir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(s.dataFile, data, 0644)
|
||||
}
|
||||
|
||||
func (s *SyncServer) handleGet(w http.ResponseWriter, r *http.Request) {
|
||||
if !s.authenticate(r) {
|
||||
log.Printf("Unauthorized access attempt from %s", r.RemoteAddr)
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("GET /sync request from %s", r.RemoteAddr)
|
||||
backup, err := s.loadData()
|
||||
if err != nil {
|
||||
log.Printf("Failed to load data: %v", err)
|
||||
http.Error(w, "Failed to load data", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Sending data to %s: gyms=%d, problems=%d, sessions=%d, attempts=%d",
|
||||
r.RemoteAddr, len(backup.Gyms), len(backup.Problems), len(backup.Sessions), len(backup.Attempts))
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(backup)
|
||||
}
|
||||
|
||||
func (s *SyncServer) handlePut(w http.ResponseWriter, r *http.Request) {
|
||||
if !s.authenticate(r) {
|
||||
log.Printf("Unauthorized sync attempt from %s", r.RemoteAddr)
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
var backup ClimbDataBackup
|
||||
if err := json.NewDecoder(r.Body).Decode(&backup); err != nil {
|
||||
log.Printf("Invalid JSON from %s: %v", r.RemoteAddr, err)
|
||||
http.Error(w, "Invalid JSON", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.saveData(&backup); err != nil {
|
||||
log.Printf("Failed to save data: %v", err)
|
||||
http.Error(w, "Failed to save data", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Data synced by %s", r.RemoteAddr)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(backup)
|
||||
}
|
||||
|
||||
func (s *SyncServer) handleHealth(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"status": "healthy",
|
||||
"time": time.Now().UTC().Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
|
||||
func (s *SyncServer) handleImageUpload(w http.ResponseWriter, r *http.Request) {
|
||||
if !s.authenticate(r) {
|
||||
log.Printf("Unauthorized image upload attempt from %s", r.RemoteAddr)
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
filename := r.URL.Query().Get("filename")
|
||||
if filename == "" {
|
||||
http.Error(w, "Missing filename parameter", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
imageData, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to read image data", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
imagePath := filepath.Join(s.imagesDir, filename)
|
||||
if err := os.WriteFile(imagePath, imageData, 0644); err != nil {
|
||||
log.Printf("Failed to save image %s: %v", filename, err)
|
||||
http.Error(w, "Failed to save image", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Image uploaded: %s (%d bytes) by %s", filename, len(imageData), r.RemoteAddr)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "uploaded"})
|
||||
}
|
||||
|
||||
func (s *SyncServer) handleImageDownload(w http.ResponseWriter, r *http.Request) {
|
||||
if !s.authenticate(r) {
|
||||
log.Printf("Unauthorized image download attempt from %s", r.RemoteAddr)
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
filename := r.URL.Query().Get("filename")
|
||||
if filename == "" {
|
||||
http.Error(w, "Missing filename parameter", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
imagePath := filepath.Join(s.imagesDir, filename)
|
||||
imageData, err := os.ReadFile(imagePath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
http.Error(w, "Image not found", http.StatusNotFound)
|
||||
} else {
|
||||
http.Error(w, "Failed to read image", http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Set appropriate content type based on file extension
|
||||
ext := filepath.Ext(filename)
|
||||
switch ext {
|
||||
case ".jpg", ".jpeg":
|
||||
w.Header().Set("Content-Type", "image/jpeg")
|
||||
case ".png":
|
||||
w.Header().Set("Content-Type", "image/png")
|
||||
case ".gif":
|
||||
w.Header().Set("Content-Type", "image/gif")
|
||||
case ".webp":
|
||||
w.Header().Set("Content-Type", "image/webp")
|
||||
default:
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(imageData)
|
||||
}
|
||||
|
||||
func (s *SyncServer) handleSync(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
s.handleGet(w, r)
|
||||
case http.MethodPut:
|
||||
s.handlePut(w, r)
|
||||
default:
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
authToken := os.Getenv("AUTH_TOKEN")
|
||||
if authToken == "" {
|
||||
log.Fatal("AUTH_TOKEN environment variable is required")
|
||||
}
|
||||
|
||||
dataFile := os.Getenv("DATA_FILE")
|
||||
if dataFile == "" {
|
||||
dataFile = "./data/climb_data.json"
|
||||
}
|
||||
|
||||
imagesDir := os.Getenv("IMAGES_DIR")
|
||||
if imagesDir == "" {
|
||||
imagesDir = "./data/images"
|
||||
}
|
||||
|
||||
port := os.Getenv("PORT")
|
||||
if port == "" {
|
||||
port = "8080"
|
||||
}
|
||||
|
||||
server := &SyncServer{
|
||||
authToken: authToken,
|
||||
dataFile: dataFile,
|
||||
imagesDir: imagesDir,
|
||||
}
|
||||
|
||||
http.HandleFunc("/sync", server.handleSync)
|
||||
http.HandleFunc("/health", server.handleHealth)
|
||||
http.HandleFunc("/images/upload", server.handleImageUpload)
|
||||
http.HandleFunc("/images/download", server.handleImageDownload)
|
||||
|
||||
fmt.Printf("OpenClimb sync server starting on port %s\n", port)
|
||||
fmt.Printf("Data file: %s\n", dataFile)
|
||||
fmt.Printf("Images directory: %s\n", imagesDir)
|
||||
fmt.Printf("Health check available at /health\n")
|
||||
fmt.Printf("Image upload: POST /images/upload?filename=<name>\n")
|
||||
fmt.Printf("Image download: GET /images/download?filename=<name>\n")
|
||||
|
||||
log.Fatal(http.ListenAndServe(":"+port, nil))
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
#!/bin/bash
|
||||
# OpenClimb Sync Server Runner
|
||||
set -e
|
||||
|
||||
# Default values
|
||||
AUTH_TOKEN=${AUTH_TOKEN:-}
|
||||
PORT=${PORT:-8080}
|
||||
DATA_FILE=${DATA_FILE:-./data/climb_data.json}
|
||||
|
||||
# Check if AUTH_TOKEN is set
|
||||
if [ -z "$AUTH_TOKEN" ]; then
|
||||
echo "Error: AUTH_TOKEN environment variable must be set"
|
||||
echo "Usage: AUTH_TOKEN=your-secret-token ./run.sh"
|
||||
echo "Or: export AUTH_TOKEN=your-secret-token && ./run.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create data directory if it doesn't exist
|
||||
mkdir -p "$(dirname "$DATA_FILE")"
|
||||
|
||||
# Build and run
|
||||
echo "Building OpenClimb sync server..."
|
||||
go build -o sync-server .
|
||||
|
||||
echo "Starting server on port $PORT"
|
||||
echo "Data will be stored in: $DATA_FILE"
|
||||
echo "Images will be stored in: ${IMAGES_DIR:-./data/images}"
|
||||
echo "Use Authorization: Bearer $AUTH_TOKEN in your requests"
|
||||
echo ""
|
||||
|
||||
exec ./sync-server
|
||||
Reference in New Issue
Block a user