An AUR (Arch User Repository) mirror service written in Go

dockerfile

Changed files
+186 -7
cmd
myaur
myaur
populate
server
+10
.dockerignore
···
+
.git
+
.gitignore
+
Dockerfile
+
docker-compose.yml
+
.dockerignore
+
aur-mirror
+
myaur.db
+
*.db
+
*.db-shm
+
*.db-wal
+26
Caddyfile
···
+
# Replace with your actual domain name
+
# For local development, you can use localhost
+
:80 {
+
# Redirect to HTTPS
+
redir https://{host}{uri} permanent
+
}
+
+
# Main application
+
:443 {
+
# Automatic HTTPS with self-signed cert (for localhost)
+
# For production, replace with your domain and Caddy will get a Let's Encrypt cert
+
# Example: myaur.example.com
+
+
# Reverse proxy to myaur application
+
reverse_proxy localhost:8080
+
+
# Enable logging
+
log {
+
output file /var/log/caddy/access.log
+
}
+
}
+
+
# Metrics endpoint (optional - uncomment if you want metrics exposed)
+
# :8081 {
+
# reverse_proxy localhost:8081
+
# }
+36
Dockerfile
···
+
# Build stage
+
FROM golang:1.25.3-bookworm AS builder
+
+
RUN apt-get update && apt-get install -y \
+
git \
+
ca-certificates \
+
&& rm -rf /var/lib/apt/lists/*
+
+
WORKDIR /build
+
+
COPY go.mod go.sum ./
+
+
RUN go mod download
+
+
COPY . .
+
+
RUN CGO_ENABLED=1 go build -o myaur-bin ./cmd/myaur
+
+
# Runtime stage
+
FROM debian:bookworm-slim
+
+
RUN apt-get update && apt-get install -y \
+
ca-certificates \
+
git \
+
&& rm -rf /var/lib/apt/lists/*
+
+
WORKDIR /app
+
+
COPY --from=builder /build/myaur-bin /app/myaur-bin
+
+
RUN mkdir -p /app/data
+
+
EXPOSE 8080 8081
+
+
# Set default command
+
CMD ["/app/myaur-bin", "serve", "--listen-addr", ":8080", "--metrics-listen-addr", ":8081", "--database-path", "/app/data/myaur.db", "--repo-path", "/app/aur-mirror"]
+1
cmd/myaur/main.go
···
"github.com/haileyok/myaur/myaur/gitrepo"
"github.com/haileyok/myaur/myaur/populate"
"github.com/haileyok/myaur/myaur/server"
+
_ "github.com/joho/godotenv/autoload"
"github.com/urfave/cli/v2"
)
+31
docker-compose.yml
···
+
version: '3.8'
+
+
services:
+
myaur:
+
build:
+
context: .
+
dockerfile: Dockerfile
+
container_name: myaur
+
network_mode: host
+
volumes:
+
- myaur-data:/app/data
+
restart: unless-stopped
+
environment:
+
- TZ=UTC
+
+
caddy:
+
image: caddy:2-alpine
+
container_name: caddy
+
network_mode: host
+
volumes:
+
- ./Caddyfile:/etc/caddy/Caddyfile
+
- caddy-data:/data
+
- caddy-config:/config
+
restart: unless-stopped
+
depends_on:
+
- myaur
+
+
volumes:
+
myaur-data:
+
caddy-data:
+
caddy-config:
+1
go.mod
···
go 1.25.3
require (
+
github.com/joho/godotenv v1.5.1
github.com/labstack/echo/v4 v4.13.4
github.com/labstack/gommon v0.4.2
github.com/samber/slog-echo v1.18.0
+2
go.sum
···
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
+
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
+
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA=
github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
+5 -1
myaur/populate/populate.go
···
}
processed.Add(1)
-
logger.Info("progress", "processed", processed.Load(), "succeeded", succeeded.Load(), "failed", failed.Load(), "total", len(branches))
+
processed := processed.Load()
+
if processed%500 == 0 {
+
logger.Info("progress", "processed", processed, "succeeded", succeeded.Load(), "failed", failed.Load(), "total", len(branches))
+
}
+
logger.Debug("progress", "processed", processed, "succeeded", succeeded.Load(), "failed", failed.Load(), "total", len(branches))
}()
}
+74 -6
myaur/server/server.go
···
"net/http"
"os"
"os/signal"
+
"sync"
"syscall"
"time"
"github.com/haileyok/myaur/myaur/database"
"github.com/haileyok/myaur/myaur/gitrepo"
+
"github.com/haileyok/myaur/myaur/populate"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/labstack/gommon/log"
···
httpd *http.Server
metricsHttpd *http.Server
db *database.Database
+
populator *populate.Populate
remoteRepoUrl string
repoPath string
}
···
return nil, fmt.Errorf("failed to create new database client: %w", err)
}
+
populator, err := populate.New(&populate.Args{
+
DatabasePath: args.DatabasePath,
+
RepoPath: args.RepoPath,
+
RemoteRepoUrl: args.RemoteRepoUrl,
+
Debug: args.Debug,
+
Concurrency: 20, // TODO: make an env-var for this
+
})
+
s := Server{
echo: e,
httpd: &httpd,
metricsHttpd: &metricsHttpd,
db: db,
+
populator: populator,
logger: logger,
remoteRepoUrl: args.RemoteRepoUrl,
repoPath: args.RepoPath,
···
}()
logger.Info("myaur metrics server listening", "addr", s.metricsHttpd.Addr)
+
}()
+
+
shutdownTicker := make(chan struct{})
+
tickerShutdown := make(chan struct{})
+
go func() {
+
logger := s.logger.With("component", "update-routine")
+
+
ticker := time.NewTicker(1 * time.Hour)
+
+
go func() {
+
logger.Info("performing initial database population")
+
+
if err := s.populator.Run(ctx); err != nil {
+
logger.Info("error populating", "err", err)
+
}
+
+
for range ticker.C {
+
if err := s.populator.Run(ctx); err != nil {
+
logger.Info("error populating", "err", err)
+
}
+
}
+
+
close(tickerShutdown)
+
}()
+
+
<-shutdownTicker
+
+
ticker.Stop()
}()
shutdownEcho := make(chan struct{})
···
// echo should have already been closed
}
-
select {
-
case <-echoShutdown:
-
s.logger.Info("echo shutdown gracefully")
-
case <-time.After(5 * time.Second):
-
s.logger.Warn("echo did not shut down after five seconds. forcefully exiting.")
-
}
+
close(shutdownTicker)
+
+
s.logger.Info("send ctrl+c to forcefully shutdown without waiting for routines to finish")
+
+
forceShutdownSignals := make(chan os.Signal, 1)
+
signal.Notify(forceShutdownSignals, syscall.SIGINT, syscall.SIGTERM)
+
+
var wg sync.WaitGroup
+
wg.Go(func() {
+
s.logger.Info("waiting up to 5 seconds for echo to shut down")
+
select {
+
case <-echoShutdown:
+
s.logger.Info("echo shutdown gracefully")
+
case <-time.After(5 * time.Second):
+
s.logger.Warn("echo did not shut down after five seconds. forcefully exiting.")
+
case <-forceShutdownSignals:
+
s.logger.Warn("received forceful shutdown signal before echo shut down")
+
}
+
})
+
+
wg.Go(func() {
+
s.logger.Info("waiting up to 60 seconds for ticker to shut down")
+
select {
+
case <-tickerShutdown:
+
s.logger.Info("ticker shutdown gracefully")
+
case <-time.After(60 * time.Second):
+
s.logger.Warn("waited 60 seconds for ticker to shut down. forcefully exiting.")
+
case <-forceShutdownSignals:
+
s.logger.Warn("received forceful shutdown signal before ticker shut down")
+
}
+
})
+
+
s.logger.Info("waiting for routines to finish")
+
wg.Wait()
s.logger.Info("myaur shutdown")