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

readme

Changed files
+153 -75
cmd
myaur
myaur
server
+67 -1
README.md
···
# myaur
-
A simple AUR mirror.
+
A simple, self-hosted AUR (Arch User Repository) mirror written in Go. Provides both RPC API endpoints and git protocol access to AUR packages.
+
+
myaur takes advantage of the [official AUR mirror](https://github.com/archlinux/aur.git) on GitHub. You may use any mirror that you wish, however, note that it must have the same format as the official repo, in that each individual package should be a branch within the repo.
+
+
## Installation
+
+
### Using Docker Compose
+
+
The easiest way to start the mirror is to use `docker compose up -d`. This will start both the myaur service and set up a Caddy reverse proxy.
+
+
```bash
+
docker compose up -d
+
```
+
+
If you wish to use your own domain, modify the `Caddyfile` and change `:443` to your domain.
+
+
### Building from Source
+
+
Requirements:
+
- Go 1.25.3 or later
+
- Git
+
+
```bash
+
go build -o myaur ./cmd/myaur
+
```
+
+
## Usage
+
+
### Populate Database
+
+
If you wish to clone the mirror repo and populate the database, you can do so without actually serving the mirror API.
+
+
```bash
+
./myaur populate \
+
--database-path ./myaur.db \
+
--repo-path ./aur-mirror \
+
--concurrency 10
+
```
+
+
Options:
+
- `--database-path`: Path to SQLite database file (default: `./myaur.db`)
+
- `--repo-path`: Path to clone/update AUR git mirror (default: `./aur-mirror`)
+
- `--remote-repo-url`: Remote AUR repository URL (default: `https://github.com/archlinux/aur.git`)
+
- `--concurrency`: Number of worker threads for parsing (default: `10`)
+
- `--debug`: Enable debug logging
+
+
### Serve
+
+
To serve the API:
+
+
```bash
+
./myaur serve \
+
--listen-addr :8080 \
+
--database-path ./myaur.db \
+
--repo-path ./aur-mirror \
+
--concurrency 10
+
```
+
+
Options:
+
- `--listen-addr`: HTTP server listen address (default: `:8080`)
+
- `--database-path`: Path to SQLite database file (default: `./myaur.db`)
+
- `--repo-path`: Path to AUR git mirror (default: `./aur-mirror`)
+
- `--remote-repo-url`: Remote AUR repository URL (default: `https://github.com/archlinux/aur.git`)
+
- `--concurrency`: Number of worker threads for parsing (default: `10`)
+
- `--auto-update`: Whether or not to automtically fetch updates from the remote repo (default: `true`)
+
- `--update-interval`: Time between automatic fetches (default: `1h`)
+
- `--debug`: Enable debug logging
+25 -7
cmd/myaur/main.go
···
"fmt"
"log"
"os"
+
"time"
"github.com/haileyok/myaur/myaur/gitrepo"
"github.com/haileyok/myaur/myaur/populate"
···
&cli.IntFlag{
Name: "concurrency",
Usage: "worker concurrency for parsing and adding packages to database",
-
Value: 10, // TODO: is this a good default
+
Value: 10,
},
},
Action: func(cmd *cli.Context) error {
···
Name: "debug",
Usage: "flag to enable debug logs",
},
+
&cli.IntFlag{
+
Name: "concurrency",
+
Usage: "worker concurrency for parsing and adding packages to database",
+
Value: 10,
+
},
+
&cli.BoolFlag{
+
Name: "auto-update",
+
Usage: "automatically pull updates from the remote repo at the set interval",
+
Value: true,
+
},
+
&cli.DurationFlag{
+
Name: "update-interval",
+
Usage: "the interval at which updates will be fetched. note that this should likely be at most one hour.",
+
Value: time.Hour,
+
},
},
Action: func(cmd *cli.Context) error {
ctx := context.Background()
s, err := server.New(&server.Args{
-
Addr: cmd.String("listen-addr"),
-
MetricsAddr: cmd.String("metrics-listen-addr"),
-
DatabasePath: cmd.String("database-path"),
-
RemoteRepoUrl: cmd.String("remote-repo-url"),
-
RepoPath: cmd.String("repo-path"),
-
Debug: cmd.Bool("debug"),
+
Addr: cmd.String("listen-addr"),
+
DatabasePath: cmd.String("database-path"),
+
RemoteRepoUrl: cmd.String("remote-repo-url"),
+
RepoPath: cmd.String("repo-path"),
+
Concurrency: cmd.Int("concurrency"),
+
AutoUpdate: cmd.Bool("auto-update"),
+
UpdateInterval: cmd.Duration("update-interval"),
+
Debug: cmd.Bool("debug"),
})
if err != nil {
return fmt.Errorf("failed to create new myaur server: %w", err)
+61 -67
myaur/server/server.go
···
)
type Server struct {
-
logger *slog.Logger
-
echo *echo.Echo
-
httpd *http.Server
-
metricsHttpd *http.Server
-
db *database.Database
-
populator *populate.Populate
-
remoteRepoUrl string
-
repoPath string
+
logger *slog.Logger
+
echo *echo.Echo
+
httpd *http.Server
+
db *database.Database
+
populator *populate.Populate
+
remoteRepoUrl string
+
repoPath string
+
autoUpdate bool
+
updateInterval time.Duration
}
type Args struct {
-
Addr string
-
MetricsAddr string
-
DatabasePath string
-
RemoteRepoUrl string
-
RepoPath string
-
Debug bool
+
Addr string
+
DatabasePath string
+
RemoteRepoUrl string
+
RepoPath string
+
Concurrency int
+
AutoUpdate bool
+
UpdateInterval time.Duration
+
Debug bool
}
func New(args *Args) (*Server, error) {
···
Handler: e,
}
-
metricsHttpd := http.Server{
-
Addr: args.MetricsAddr,
-
}
-
db, err := database.New(&database.Args{
DatabasePath: args.DatabasePath,
Debug: args.Debug,
···
RepoPath: args.RepoPath,
RemoteRepoUrl: args.RemoteRepoUrl,
Debug: args.Debug,
-
Concurrency: 20, // TODO: make an env-var for this
+
Concurrency: args.Concurrency,
})
s := Server{
-
echo: e,
-
httpd: &httpd,
-
metricsHttpd: &metricsHttpd,
-
db: db,
-
populator: populator,
-
logger: logger,
-
remoteRepoUrl: args.RemoteRepoUrl,
-
repoPath: args.RepoPath,
+
echo: e,
+
httpd: &httpd,
+
db: db,
+
populator: populator,
+
logger: logger,
+
remoteRepoUrl: args.RemoteRepoUrl,
+
repoPath: args.RepoPath,
+
autoUpdate: args.AutoUpdate,
+
updateInterval: args.UpdateInterval,
}
return &s, nil
}
func (s *Server) Serve(ctx context.Context) error {
-
go func() {
-
logger := s.logger.With("component", "metrics-httpd")
-
-
go func() {
-
if err := s.metricsHttpd.ListenAndServe(); err != http.ErrServerClosed {
-
logger.Error("error listening", "err", err)
-
}
-
}()
-
-
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)
-
+
if s.autoUpdate {
go func() {
-
logger.Info("performing initial database population")
+
logger := s.logger.With("component", "update-routine")
-
if err := s.populator.Run(ctx); err != nil {
-
logger.Info("error populating", "err", err)
-
}
+
ticker := time.NewTicker(s.updateInterval)
-
for range ticker.C {
+
go func() {
+
logger.Info("performing initial database population")
+
if err := s.populator.Run(ctx); err != nil {
logger.Info("error populating", "err", err)
}
-
}
-
close(tickerShutdown)
-
}()
+
for range ticker.C {
+
if err := s.populator.Run(ctx); err != nil {
+
logger.Info("error populating", "err", err)
+
}
+
}
-
<-shutdownTicker
+
close(tickerShutdown)
+
}()
-
ticker.Stop()
-
}()
+
<-shutdownTicker
+
+
ticker.Stop()
+
}()
+
}
shutdownEcho := make(chan struct{})
echoShutdown := make(chan struct{})
···
// echo should have already been closed
}
-
close(shutdownTicker)
+
if s.autoUpdate {
+
close(shutdownTicker)
+
}
s.logger.Info("send ctrl+c to forcefully shutdown without waiting for routines to finish")
···
}
})
-
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")
-
}
-
})
+
if s.autoUpdate {
+
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()