+67
-1
README.md
+67
-1
README.md
···
1
1
# myaur
2
2
3
-
A simple AUR mirror.
3
+
A simple, self-hosted AUR (Arch User Repository) mirror written in Go. Provides both RPC API endpoints and git protocol access to AUR packages.
4
+
5
+
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.
6
+
7
+
## Installation
8
+
9
+
### Using Docker Compose
10
+
11
+
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.
12
+
13
+
```bash
14
+
docker compose up -d
15
+
```
16
+
17
+
If you wish to use your own domain, modify the `Caddyfile` and change `:443` to your domain.
18
+
19
+
### Building from Source
20
+
21
+
Requirements:
22
+
- Go 1.25.3 or later
23
+
- Git
24
+
25
+
```bash
26
+
go build -o myaur ./cmd/myaur
27
+
```
28
+
29
+
## Usage
30
+
31
+
### Populate Database
32
+
33
+
If you wish to clone the mirror repo and populate the database, you can do so without actually serving the mirror API.
34
+
35
+
```bash
36
+
./myaur populate \
37
+
--database-path ./myaur.db \
38
+
--repo-path ./aur-mirror \
39
+
--concurrency 10
40
+
```
41
+
42
+
Options:
43
+
- `--database-path`: Path to SQLite database file (default: `./myaur.db`)
44
+
- `--repo-path`: Path to clone/update AUR git mirror (default: `./aur-mirror`)
45
+
- `--remote-repo-url`: Remote AUR repository URL (default: `https://github.com/archlinux/aur.git`)
46
+
- `--concurrency`: Number of worker threads for parsing (default: `10`)
47
+
- `--debug`: Enable debug logging
48
+
49
+
### Serve
50
+
51
+
To serve the API:
52
+
53
+
```bash
54
+
./myaur serve \
55
+
--listen-addr :8080 \
56
+
--database-path ./myaur.db \
57
+
--repo-path ./aur-mirror \
58
+
--concurrency 10
59
+
```
60
+
61
+
Options:
62
+
- `--listen-addr`: HTTP server listen address (default: `:8080`)
63
+
- `--database-path`: Path to SQLite database file (default: `./myaur.db`)
64
+
- `--repo-path`: Path to AUR git mirror (default: `./aur-mirror`)
65
+
- `--remote-repo-url`: Remote AUR repository URL (default: `https://github.com/archlinux/aur.git`)
66
+
- `--concurrency`: Number of worker threads for parsing (default: `10`)
67
+
- `--auto-update`: Whether or not to automtically fetch updates from the remote repo (default: `true`)
68
+
- `--update-interval`: Time between automatic fetches (default: `1h`)
69
+
- `--debug`: Enable debug logging
+25
-7
cmd/myaur/main.go
+25
-7
cmd/myaur/main.go
···
5
5
"fmt"
6
6
"log"
7
7
"os"
8
+
"time"
8
9
9
10
"github.com/haileyok/myaur/myaur/gitrepo"
10
11
"github.com/haileyok/myaur/myaur/populate"
···
43
44
&cli.IntFlag{
44
45
Name: "concurrency",
45
46
Usage: "worker concurrency for parsing and adding packages to database",
46
-
Value: 10, // TODO: is this a good default
47
+
Value: 10,
47
48
},
48
49
},
49
50
Action: func(cmd *cli.Context) error {
···
99
100
Name: "debug",
100
101
Usage: "flag to enable debug logs",
101
102
},
103
+
&cli.IntFlag{
104
+
Name: "concurrency",
105
+
Usage: "worker concurrency for parsing and adding packages to database",
106
+
Value: 10,
107
+
},
108
+
&cli.BoolFlag{
109
+
Name: "auto-update",
110
+
Usage: "automatically pull updates from the remote repo at the set interval",
111
+
Value: true,
112
+
},
113
+
&cli.DurationFlag{
114
+
Name: "update-interval",
115
+
Usage: "the interval at which updates will be fetched. note that this should likely be at most one hour.",
116
+
Value: time.Hour,
117
+
},
102
118
},
103
119
Action: func(cmd *cli.Context) error {
104
120
ctx := context.Background()
105
121
106
122
s, err := server.New(&server.Args{
107
-
Addr: cmd.String("listen-addr"),
108
-
MetricsAddr: cmd.String("metrics-listen-addr"),
109
-
DatabasePath: cmd.String("database-path"),
110
-
RemoteRepoUrl: cmd.String("remote-repo-url"),
111
-
RepoPath: cmd.String("repo-path"),
112
-
Debug: cmd.Bool("debug"),
123
+
Addr: cmd.String("listen-addr"),
124
+
DatabasePath: cmd.String("database-path"),
125
+
RemoteRepoUrl: cmd.String("remote-repo-url"),
126
+
RepoPath: cmd.String("repo-path"),
127
+
Concurrency: cmd.Int("concurrency"),
128
+
AutoUpdate: cmd.Bool("auto-update"),
129
+
UpdateInterval: cmd.Duration("update-interval"),
130
+
Debug: cmd.Bool("debug"),
113
131
})
114
132
if err != nil {
115
133
return fmt.Errorf("failed to create new myaur server: %w", err)
+61
-67
myaur/server/server.go
+61
-67
myaur/server/server.go
···
21
21
)
22
22
23
23
type Server struct {
24
-
logger *slog.Logger
25
-
echo *echo.Echo
26
-
httpd *http.Server
27
-
metricsHttpd *http.Server
28
-
db *database.Database
29
-
populator *populate.Populate
30
-
remoteRepoUrl string
31
-
repoPath string
24
+
logger *slog.Logger
25
+
echo *echo.Echo
26
+
httpd *http.Server
27
+
db *database.Database
28
+
populator *populate.Populate
29
+
remoteRepoUrl string
30
+
repoPath string
31
+
autoUpdate bool
32
+
updateInterval time.Duration
32
33
}
33
34
34
35
type Args struct {
35
-
Addr string
36
-
MetricsAddr string
37
-
DatabasePath string
38
-
RemoteRepoUrl string
39
-
RepoPath string
40
-
Debug bool
36
+
Addr string
37
+
DatabasePath string
38
+
RemoteRepoUrl string
39
+
RepoPath string
40
+
Concurrency int
41
+
AutoUpdate bool
42
+
UpdateInterval time.Duration
43
+
Debug bool
41
44
}
42
45
43
46
func New(args *Args) (*Server, error) {
···
64
67
Handler: e,
65
68
}
66
69
67
-
metricsHttpd := http.Server{
68
-
Addr: args.MetricsAddr,
69
-
}
70
-
71
70
db, err := database.New(&database.Args{
72
71
DatabasePath: args.DatabasePath,
73
72
Debug: args.Debug,
···
81
80
RepoPath: args.RepoPath,
82
81
RemoteRepoUrl: args.RemoteRepoUrl,
83
82
Debug: args.Debug,
84
-
Concurrency: 20, // TODO: make an env-var for this
83
+
Concurrency: args.Concurrency,
85
84
})
86
85
87
86
s := Server{
88
-
echo: e,
89
-
httpd: &httpd,
90
-
metricsHttpd: &metricsHttpd,
91
-
db: db,
92
-
populator: populator,
93
-
logger: logger,
94
-
remoteRepoUrl: args.RemoteRepoUrl,
95
-
repoPath: args.RepoPath,
87
+
echo: e,
88
+
httpd: &httpd,
89
+
db: db,
90
+
populator: populator,
91
+
logger: logger,
92
+
remoteRepoUrl: args.RemoteRepoUrl,
93
+
repoPath: args.RepoPath,
94
+
autoUpdate: args.AutoUpdate,
95
+
updateInterval: args.UpdateInterval,
96
96
}
97
97
98
98
return &s, nil
99
99
}
100
100
101
101
func (s *Server) Serve(ctx context.Context) error {
102
-
go func() {
103
-
logger := s.logger.With("component", "metrics-httpd")
104
-
105
-
go func() {
106
-
if err := s.metricsHttpd.ListenAndServe(); err != http.ErrServerClosed {
107
-
logger.Error("error listening", "err", err)
108
-
}
109
-
}()
110
-
111
-
logger.Info("myaur metrics server listening", "addr", s.metricsHttpd.Addr)
112
-
}()
113
-
114
102
shutdownTicker := make(chan struct{})
115
103
tickerShutdown := make(chan struct{})
116
-
go func() {
117
-
logger := s.logger.With("component", "update-routine")
118
-
119
-
ticker := time.NewTicker(1 * time.Hour)
120
-
104
+
if s.autoUpdate {
121
105
go func() {
122
-
logger.Info("performing initial database population")
106
+
logger := s.logger.With("component", "update-routine")
123
107
124
-
if err := s.populator.Run(ctx); err != nil {
125
-
logger.Info("error populating", "err", err)
126
-
}
108
+
ticker := time.NewTicker(s.updateInterval)
127
109
128
-
for range ticker.C {
110
+
go func() {
111
+
logger.Info("performing initial database population")
112
+
129
113
if err := s.populator.Run(ctx); err != nil {
130
114
logger.Info("error populating", "err", err)
131
115
}
132
-
}
133
116
134
-
close(tickerShutdown)
135
-
}()
117
+
for range ticker.C {
118
+
if err := s.populator.Run(ctx); err != nil {
119
+
logger.Info("error populating", "err", err)
120
+
}
121
+
}
136
122
137
-
<-shutdownTicker
123
+
close(tickerShutdown)
124
+
}()
138
125
139
-
ticker.Stop()
140
-
}()
126
+
<-shutdownTicker
127
+
128
+
ticker.Stop()
129
+
}()
130
+
}
141
131
142
132
shutdownEcho := make(chan struct{})
143
133
echoShutdown := make(chan struct{})
···
191
181
// echo should have already been closed
192
182
}
193
183
194
-
close(shutdownTicker)
184
+
if s.autoUpdate {
185
+
close(shutdownTicker)
186
+
}
195
187
196
188
s.logger.Info("send ctrl+c to forcefully shutdown without waiting for routines to finish")
197
189
···
211
203
}
212
204
})
213
205
214
-
wg.Go(func() {
215
-
s.logger.Info("waiting up to 60 seconds for ticker to shut down")
216
-
select {
217
-
case <-tickerShutdown:
218
-
s.logger.Info("ticker shutdown gracefully")
219
-
case <-time.After(60 * time.Second):
220
-
s.logger.Warn("waited 60 seconds for ticker to shut down. forcefully exiting.")
221
-
case <-forceShutdownSignals:
222
-
s.logger.Warn("received forceful shutdown signal before ticker shut down")
223
-
}
224
-
})
206
+
if s.autoUpdate {
207
+
wg.Go(func() {
208
+
s.logger.Info("waiting up to 60 seconds for ticker to shut down")
209
+
select {
210
+
case <-tickerShutdown:
211
+
s.logger.Info("ticker shutdown gracefully")
212
+
case <-time.After(60 * time.Second):
213
+
s.logger.Warn("waited 60 seconds for ticker to shut down. forcefully exiting.")
214
+
case <-forceShutdownSignals:
215
+
s.logger.Warn("received forceful shutdown signal before ticker shut down")
216
+
}
217
+
})
218
+
}
225
219
226
220
s.logger.Info("waiting for routines to finish")
227
221
wg.Wait()