+5
.gitignore
+5
.gitignore
+111
cmd/myaur/main.go
+111
cmd/myaur/main.go
···
1
+
package main
2
+
3
+
import (
4
+
"context"
5
+
"fmt"
6
+
"log"
7
+
"os"
8
+
9
+
"github.com/haileyok/myaur/myaur/populate"
10
+
"github.com/haileyok/myaur/myaur/server"
11
+
"github.com/urfave/cli/v2"
12
+
)
13
+
14
+
func main() {
15
+
app := cli.App{
16
+
Name: "myaur",
17
+
Usage: "a AUR mirror service",
18
+
Commands: cli.Commands{
19
+
&cli.Command{
20
+
Name: "populate",
21
+
Flags: []cli.Flag{
22
+
&cli.StringFlag{
23
+
Name: "database-path",
24
+
Usage: "path to database file",
25
+
Value: "./myaur.db",
26
+
},
27
+
&cli.StringFlag{
28
+
Name: "repo-path",
29
+
Usage: "path to store/update the AUR git mirror",
30
+
Value: "./aur-mirror",
31
+
},
32
+
&cli.BoolFlag{
33
+
Name: "debug",
34
+
Usage: "flag to enable debug logs",
35
+
},
36
+
&cli.IntFlag{
37
+
Name: "concurrency",
38
+
Usage: "worker concurrency for parsing and adding packages to database",
39
+
Value: 10, // TODO: is this a good default
40
+
},
41
+
},
42
+
Action: func(cmd *cli.Context) error {
43
+
ctx := context.Background()
44
+
45
+
p, err := populate.New(&populate.Args{
46
+
DatabasePath: cmd.String("database-path"),
47
+
RepoPath: cmd.String("repo-path"),
48
+
Debug: cmd.Bool("debug"),
49
+
Concurrency: cmd.Int("concurrency"),
50
+
})
51
+
if err != nil {
52
+
return fmt.Errorf("failed to create populate client: %w", err)
53
+
}
54
+
55
+
if err := p.Run(ctx); err != nil {
56
+
return fmt.Errorf("failed to populate database: %w", err)
57
+
}
58
+
59
+
return nil
60
+
},
61
+
},
62
+
&cli.Command{
63
+
Name: "serve",
64
+
Flags: []cli.Flag{
65
+
&cli.StringFlag{
66
+
Name: "listen-addr",
67
+
Usage: "address to listen on for the web service",
68
+
Value: ":8080",
69
+
},
70
+
&cli.StringFlag{
71
+
Name: "metrics-listen-addr",
72
+
Usage: "metrics listen address",
73
+
Value: ":8081",
74
+
},
75
+
&cli.StringFlag{
76
+
Name: "database-path",
77
+
Usage: "path to database file",
78
+
Value: "./myaur.db",
79
+
},
80
+
&cli.BoolFlag{
81
+
Name: "debug",
82
+
Usage: "flag to enable debug logs",
83
+
},
84
+
},
85
+
Action: func(cmd *cli.Context) error {
86
+
ctx := context.Background()
87
+
88
+
s, err := server.New(&server.Args{
89
+
Addr: cmd.String("listen-addr"),
90
+
MetricsAddr: cmd.String("metrics-listen-addr"),
91
+
DatabasePath: cmd.String("database-path"),
92
+
Debug: cmd.Bool("debug"),
93
+
})
94
+
if err != nil {
95
+
return fmt.Errorf("failed to create new myaur server: %w", err)
96
+
}
97
+
98
+
if err := s.Serve(ctx); err != nil {
99
+
return fmt.Errorf("failed to serve myaur server: %w", err)
100
+
}
101
+
102
+
return nil
103
+
},
104
+
},
105
+
},
106
+
}
107
+
108
+
if err := app.Run(os.Args); err != nil {
109
+
log.Fatal(err)
110
+
}
111
+
}
+29
go.mod
+29
go.mod
···
1
+
module github.com/haileyok/myaur
2
+
3
+
go 1.25.3
4
+
5
+
require (
6
+
github.com/labstack/echo/v4 v4.13.4
7
+
github.com/labstack/gommon v0.4.2
8
+
github.com/urfave/cli/v2 v2.27.7
9
+
golang.org/x/sync v0.14.0
10
+
gorm.io/driver/sqlite v1.6.0
11
+
gorm.io/gorm v1.31.0
12
+
)
13
+
14
+
require (
15
+
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
16
+
github.com/jinzhu/inflection v1.0.0 // indirect
17
+
github.com/jinzhu/now v1.1.5 // indirect
18
+
github.com/mattn/go-colorable v0.1.14 // indirect
19
+
github.com/mattn/go-isatty v0.0.20 // indirect
20
+
github.com/mattn/go-sqlite3 v1.14.22 // indirect
21
+
github.com/russross/blackfriday/v2 v2.1.0 // indirect
22
+
github.com/valyala/bytebufferpool v1.0.0 // indirect
23
+
github.com/valyala/fasttemplate v1.2.2 // indirect
24
+
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
25
+
golang.org/x/crypto v0.38.0 // indirect
26
+
golang.org/x/net v0.40.0 // indirect
27
+
golang.org/x/sys v0.33.0 // indirect
28
+
golang.org/x/text v0.25.0 // indirect
29
+
)
+49
go.sum
+49
go.sum
···
1
+
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
2
+
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
3
+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
4
+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
5
+
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
6
+
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
7
+
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
8
+
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
9
+
github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA=
10
+
github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ=
11
+
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
12
+
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
13
+
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
14
+
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
15
+
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
16
+
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
17
+
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
18
+
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
19
+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
20
+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
21
+
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
22
+
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
23
+
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
24
+
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
25
+
github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=
26
+
github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4=
27
+
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
28
+
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
29
+
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
30
+
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
31
+
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
32
+
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
33
+
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
34
+
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
35
+
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
36
+
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
37
+
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
38
+
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
39
+
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
40
+
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
41
+
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
42
+
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
43
+
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
44
+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
45
+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
46
+
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
47
+
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
48
+
gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY=
49
+
gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
+100
myaur/database/database.go
+100
myaur/database/database.go
···
1
+
package database
2
+
3
+
import (
4
+
"fmt"
5
+
"log/slog"
6
+
"os"
7
+
8
+
"gorm.io/driver/sqlite"
9
+
"gorm.io/gorm"
10
+
)
11
+
12
+
type Database struct {
13
+
logger *slog.Logger
14
+
db *gorm.DB
15
+
}
16
+
17
+
type Args struct {
18
+
DatabasePath string
19
+
Debug bool
20
+
}
21
+
22
+
func New(args *Args) (*Database, error) {
23
+
level := slog.LevelInfo
24
+
if args.Debug {
25
+
level = slog.LevelDebug
26
+
}
27
+
28
+
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
29
+
Level: level,
30
+
}))
31
+
32
+
logger = logger.With("component", "database")
33
+
34
+
gormDb, err := gorm.Open(sqlite.Open(args.DatabasePath))
35
+
if err != nil {
36
+
return nil, fmt.Errorf("failed to open database: %w", err)
37
+
}
38
+
39
+
if err := gormDb.AutoMigrate(
40
+
&PackageInfo{},
41
+
); err != nil {
42
+
return nil, fmt.Errorf("failed to migrate db: %w", err)
43
+
}
44
+
45
+
db := Database{
46
+
logger: logger,
47
+
db: gormDb,
48
+
}
49
+
50
+
return &db, nil
51
+
}
52
+
53
+
func (db *Database) UpsertPackage(pkg *PackageInfo) error {
54
+
result := db.db.Where("name = ?", pkg.Name).FirstOrCreate(pkg)
55
+
if result.Error != nil {
56
+
return result.Error
57
+
}
58
+
59
+
if result.RowsAffected == 0 {
60
+
if err := db.db.Model(pkg).Where("name = ?", pkg.Name).Updates(pkg).Error; err != nil {
61
+
return err
62
+
}
63
+
}
64
+
65
+
return nil
66
+
}
67
+
68
+
func (db *Database) GetPackageByName(name string) (*PackageInfo, error) {
69
+
var pkg PackageInfo
70
+
if err := db.db.Where("name = ?", name).First(&pkg).Error; err != nil {
71
+
return nil, err
72
+
}
73
+
return &pkg, nil
74
+
}
75
+
76
+
func (db *Database) GetPackagesByName(name string) ([]PackageInfo, error) {
77
+
var pkgs []PackageInfo
78
+
searchTerm := "%" + name + "%"
79
+
if err := db.db.Where("name LIKE ?", searchTerm).Find(&pkgs).Error; err != nil {
80
+
return nil, err
81
+
}
82
+
return pkgs, nil
83
+
}
84
+
85
+
func (db *Database) GetPackageByDescriptionOrName(query string) (*PackageInfo, error) {
86
+
var pkg PackageInfo
87
+
if err := db.db.Where("name = ? OR description = ?", query, query).First(&pkg).Error; err != nil {
88
+
return nil, err
89
+
}
90
+
return &pkg, nil
91
+
}
92
+
93
+
func (db *Database) GetPackagesByDescriptionOrName(query string) ([]PackageInfo, error) {
94
+
var pkgs []PackageInfo
95
+
searchTerm := "%" + query + "%"
96
+
if err := db.db.Where("name LIKE ? OR description LIKE ?", searchTerm, searchTerm).Find(&pkgs).Error; err != nil {
97
+
return nil, err
98
+
}
99
+
return pkgs, nil
100
+
}
+56
myaur/database/models.go
+56
myaur/database/models.go
···
1
+
package database
2
+
3
+
import (
4
+
"database/sql/driver"
5
+
"encoding/json"
6
+
"fmt"
7
+
)
8
+
9
+
type StringSlice []string
10
+
11
+
func (s StringSlice) Value() (driver.Value, error) {
12
+
if len(s) == 0 {
13
+
return "[]", nil
14
+
}
15
+
return json.Marshal(s)
16
+
}
17
+
18
+
func (s *StringSlice) Scan(value any) error {
19
+
if value == nil {
20
+
*s = []string{}
21
+
return nil
22
+
}
23
+
24
+
bytes, ok := value.([]byte)
25
+
if !ok {
26
+
return fmt.Errorf("failed to unmarshal StringSlice value: %v", value)
27
+
}
28
+
29
+
return json.Unmarshal(bytes, s)
30
+
}
31
+
32
+
type PackageInfo struct {
33
+
Id int64 `gorm:"primaryKey;autoIncrement" json:"ID"`
34
+
Name string `gorm:"uniqueIndex;not null" json:"Name"`
35
+
PackageBaseID string `json:"PackageBaseID"`
36
+
PackageBase string `gorm:"index" json:"PackageBase"`
37
+
Version string `json:"Version"`
38
+
Description string `gorm:"index:idx_description" json:"Description"`
39
+
Url string `json:"URL"`
40
+
NumVotes int64 `json:"NumVotes"`
41
+
Popularity float64 `json:"Popularity"`
42
+
OutOfDate *int64 `json:"OutOfDate"`
43
+
Maintainer string `gorm:"index" json:"Maintainer"`
44
+
FirstSubmitted int64 `json:"FirstSubmitted"`
45
+
LastModified int64 `json:"LastModified"`
46
+
UrlPath string `json:"URLPath"`
47
+
Depends StringSlice `gorm:"type:text" json:"Depends"`
48
+
MakeDepends StringSlice `gorm:"type:text" json:"MakeDepends"`
49
+
License StringSlice `gorm:"type:text" json:"License"`
50
+
Keywords StringSlice `gorm:"type:text" json:"Keywords"`
51
+
}
52
+
53
+
// set the tablename so gorm doesn't mess it up
54
+
func (PackageInfo) TableName() string {
55
+
return "package_info"
56
+
}
+132
myaur/gitrepo/repo.go
+132
myaur/gitrepo/repo.go
···
1
+
package gitrepo
2
+
3
+
import (
4
+
"bufio"
5
+
"fmt"
6
+
"log/slog"
7
+
"net/url"
8
+
"os"
9
+
"os/exec"
10
+
"path/filepath"
11
+
"strings"
12
+
)
13
+
14
+
const (
15
+
DefaultAurRepoUrl = "https://github.com/archlinux/aur.git"
16
+
)
17
+
18
+
type Repo struct {
19
+
logger *slog.Logger
20
+
repoPath string
21
+
aurRepoUrl string
22
+
}
23
+
24
+
type Args struct {
25
+
RepoPath string
26
+
AurRepoUrl string
27
+
Debug bool
28
+
}
29
+
30
+
func New(args *Args) (*Repo, error) {
31
+
level := slog.LevelInfo
32
+
if args.Debug {
33
+
level = slog.LevelDebug
34
+
}
35
+
36
+
if args.RepoPath == "" {
37
+
return nil, fmt.Errorf("must supply a valid `RepoPath`")
38
+
}
39
+
40
+
if args.AurRepoUrl == "" {
41
+
args.AurRepoUrl = DefaultAurRepoUrl
42
+
}
43
+
44
+
if _, err := url.Parse(args.AurRepoUrl); err != nil {
45
+
return nil, fmt.Errorf("failed to parse AUR repo url: %w", err)
46
+
}
47
+
48
+
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
49
+
Level: level,
50
+
}))
51
+
52
+
logger = logger.With("component", "gitrepo", "aururl", args.AurRepoUrl)
53
+
54
+
return &Repo{
55
+
logger: logger,
56
+
repoPath: args.RepoPath,
57
+
aurRepoUrl: args.AurRepoUrl,
58
+
}, nil
59
+
}
60
+
61
+
func (r *Repo) EnsureRepo() error {
62
+
if _, err := os.Stat(r.repoPath); os.IsNotExist(err) {
63
+
r.logger.Info("aur repo does not exist, cloning...", "path", r.repoPath)
64
+
return r.clone()
65
+
}
66
+
67
+
r.logger.Info("aur repo exists, fetching updates...", "path", r.repoPath)
68
+
return r.fetch()
69
+
}
70
+
71
+
func (r *Repo) clone() error {
72
+
cmd := exec.Command("git", "clone", "--mirror", r.aurRepoUrl, r.repoPath)
73
+
cmd.Stdout = os.Stdout
74
+
cmd.Stderr = os.Stderr
75
+
76
+
if err := cmd.Run(); err != nil {
77
+
return fmt.Errorf("failed to clone repo: %w", err)
78
+
}
79
+
80
+
r.logger.Info("repo cloned successfully")
81
+
return nil
82
+
}
83
+
84
+
func (r *Repo) fetch() error {
85
+
cmd := exec.Command("git", "-C", r.repoPath, "fetch", "--all", "--prune")
86
+
cmd.Stdout = os.Stdout
87
+
cmd.Stderr = os.Stderr
88
+
89
+
if err := cmd.Run(); err != nil {
90
+
return fmt.Errorf("failed to fetch updates: %w", err)
91
+
}
92
+
93
+
r.logger.Info("repo updated successfully")
94
+
return nil
95
+
}
96
+
97
+
func (r *Repo) ListBranches() ([]string, error) {
98
+
cmd := exec.Command("git", "-C", r.repoPath, "for-each-ref", "--format=%(refname:short)", "refs/heads/")
99
+
output, err := cmd.Output()
100
+
if err != nil {
101
+
return nil, fmt.Errorf("failed to list branches: %w", err)
102
+
}
103
+
104
+
var branches []string
105
+
scanner := bufio.NewScanner(strings.NewReader(string(output)))
106
+
for scanner.Scan() {
107
+
branch := strings.TrimSpace(scanner.Text())
108
+
if branch != "" {
109
+
branches = append(branches, branch)
110
+
}
111
+
}
112
+
113
+
if err := scanner.Err(); err != nil {
114
+
return nil, fmt.Errorf("error scanning branch list: %w", err)
115
+
}
116
+
117
+
r.logger.Info("found branches", "count", len(branches))
118
+
return branches, nil
119
+
}
120
+
121
+
func (r *Repo) GetFileContent(branch, filePath string) (string, error) {
122
+
ref := filepath.Join("refs/heads", branch)
123
+
gitPath := fmt.Sprintf("%s:%s", ref, filePath)
124
+
125
+
cmd := exec.Command("git", "-C", r.repoPath, "show", gitPath)
126
+
output, err := cmd.Output()
127
+
if err != nil {
128
+
return "", fmt.Errorf("failed to get file content for branch %s: %w", branch, err)
129
+
}
130
+
131
+
return string(output), nil
132
+
}
+154
myaur/populate/populate.go
+154
myaur/populate/populate.go
···
1
+
package populate
2
+
3
+
import (
4
+
"context"
5
+
"fmt"
6
+
"log/slog"
7
+
"os"
8
+
"sync"
9
+
"sync/atomic"
10
+
11
+
"github.com/haileyok/myaur/myaur/database"
12
+
"github.com/haileyok/myaur/myaur/gitrepo"
13
+
"github.com/haileyok/myaur/myaur/srcinfo"
14
+
"golang.org/x/sync/semaphore"
15
+
)
16
+
17
+
type Populate struct {
18
+
logger *slog.Logger
19
+
repo *gitrepo.Repo
20
+
db *database.Database
21
+
sem *semaphore.Weighted
22
+
}
23
+
24
+
type Args struct {
25
+
DatabasePath string
26
+
RepoPath string
27
+
Debug bool
28
+
Concurrency int
29
+
}
30
+
31
+
func New(args *Args) (*Populate, error) {
32
+
level := slog.LevelInfo
33
+
if args.Debug {
34
+
level = slog.LevelDebug
35
+
}
36
+
37
+
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
38
+
Level: level,
39
+
}))
40
+
41
+
logger = logger.With("component", "populate")
42
+
43
+
if args.Concurrency == 0 {
44
+
// TODO: good default? idk
45
+
args.Concurrency = 10
46
+
}
47
+
48
+
repo, err := gitrepo.New(&gitrepo.Args{
49
+
RepoPath: args.RepoPath,
50
+
Debug: args.Debug,
51
+
})
52
+
if err != nil {
53
+
return nil, fmt.Errorf("failed to create repo client: %w", err)
54
+
}
55
+
56
+
db, err := database.New(&database.Args{
57
+
DatabasePath: args.DatabasePath,
58
+
Debug: args.Debug,
59
+
})
60
+
if err != nil {
61
+
return nil, fmt.Errorf("failed to create database client: %w", err)
62
+
}
63
+
64
+
sem := semaphore.NewWeighted(int64(args.Concurrency))
65
+
66
+
return &Populate{
67
+
logger: logger,
68
+
repo: repo,
69
+
db: db,
70
+
sem: sem,
71
+
}, nil
72
+
}
73
+
74
+
func (p *Populate) Run(ctx context.Context) error {
75
+
p.logger.Info("starting populate process")
76
+
77
+
// get the repo if we need to
78
+
if err := p.repo.EnsureRepo(); err != nil {
79
+
return fmt.Errorf("failed to ensure repository: %w", err)
80
+
}
81
+
82
+
// get all the branches that exist
83
+
branches, err := p.repo.ListBranches()
84
+
if err != nil {
85
+
return fmt.Errorf("failed to list branches: %w", err)
86
+
}
87
+
88
+
p.logger.Info("processing branches", "total", len(branches))
89
+
90
+
return p.processBranches(ctx, branches)
91
+
}
92
+
93
+
func (p *Populate) processBranches(ctx context.Context, branches []string) error {
94
+
var wg sync.WaitGroup
95
+
96
+
var processed, succeeded, failed atomic.Int64
97
+
98
+
logger := p.logger.With("component", "branch-processor")
99
+
100
+
for _, b := range branches {
101
+
if err := p.sem.Acquire(ctx, 1); err != nil {
102
+
logger.Error("failed to acuiqre semaphore", "err", err)
103
+
continue
104
+
}
105
+
106
+
wg.Add(1)
107
+
go func() {
108
+
defer func() {
109
+
wg.Done()
110
+
p.sem.Release(1)
111
+
}()
112
+
113
+
if err := p.processBranch(b); err != nil {
114
+
logger.Error("failed to process branch", "branch", b, "err", err)
115
+
failed.Add(1)
116
+
} else {
117
+
succeeded.Add(1)
118
+
}
119
+
processed.Add(1)
120
+
121
+
logger.Info("progress", "processed", processed.Load(), "succeeded", succeeded.Load(), "failed", failed.Load(), "total", len(branches))
122
+
}()
123
+
}
124
+
125
+
wg.Wait()
126
+
127
+
logger.Info("database populated successfully", "processed", processed.Load(), "succeeded", succeeded.Load(), "failed", failed.Load())
128
+
129
+
return nil
130
+
}
131
+
132
+
func (p *Populate) processBranch(branch string) error {
133
+
content, err := p.repo.GetFileContent(branch, ".SRCINFO")
134
+
if err != nil {
135
+
return fmt.Errorf("failed to get .SRCINFO: %w", err)
136
+
}
137
+
138
+
pkg, err := srcinfo.Parse(content)
139
+
if err != nil {
140
+
return fmt.Errorf("failed to parse .SRCINFO: %w", err)
141
+
}
142
+
143
+
if pkg.PackageBase == "" {
144
+
pkg.PackageBase = branch
145
+
}
146
+
147
+
if err := p.db.UpsertPackage(pkg); err != nil {
148
+
return fmt.Errorf("failed to upsert package: %w", err)
149
+
}
150
+
151
+
p.logger.Debug("processed package", "name", pkg.Name, "version", pkg.Version)
152
+
153
+
return nil
154
+
}
+7
myaur/server/handle_get_info.go
+7
myaur/server/handle_get_info.go
+100
myaur/server/handle_get_search.go
+100
myaur/server/handle_get_search.go
···
1
+
package server
2
+
3
+
import (
4
+
"strings"
5
+
6
+
"github.com/haileyok/myaur/myaur/database"
7
+
"github.com/labstack/echo/v4"
8
+
)
9
+
10
+
var (
11
+
GetSearchInputByAllowedValues = map[string]struct{}{
12
+
"name": {},
13
+
"name-desc": {},
14
+
"maintainer": {},
15
+
"depends": {},
16
+
"makedepends": {},
17
+
"optdepends": {},
18
+
"checkdepends": {},
19
+
}
20
+
)
21
+
22
+
type GetSearchInput struct {
23
+
By string `query:"by"`
24
+
}
25
+
26
+
type GetSearchOutput struct {
27
+
Version int `json:"version"`
28
+
Type string `json:"type"`
29
+
ResultCount int `json:"resultcount"`
30
+
Results []database.PackageInfo `json:"results"`
31
+
Error *string `json:"error,omitempty"`
32
+
}
33
+
34
+
// Depending on what the `by` parameter is, should receive one of the following as path:
35
+
// `name`: search by package name
36
+
// `name-desc`: search by package name and description
37
+
// `maintainer`: search by maintainer name
38
+
// `depends`: search for packages that depend on a keyword
39
+
// `makedepends`: search for packages that makedepend on a keyword
40
+
// `optdepends`: search for packages that optdepends on a keyword
41
+
// `checkdepends`: search for packages that checkdepends on a keyword
42
+
func (s *Server) handleGetSearch(e echo.Context) error {
43
+
makeErrJson := func(error string) GetSearchOutput {
44
+
return GetSearchOutput{
45
+
Version: 5,
46
+
Type: "error",
47
+
ResultCount: 0,
48
+
Results: []database.PackageInfo{},
49
+
Error: &error,
50
+
}
51
+
}
52
+
logger := s.logger.With("handler", "getSearch")
53
+
54
+
var input GetSearchInput
55
+
if err := e.Bind(&input); err != nil {
56
+
logger.Error("failed to bind request", "err", err)
57
+
return e.JSON(400, makeErrJson("Failed to bind request"))
58
+
}
59
+
60
+
logger = logger.With("input", input)
61
+
62
+
if input.By != "" {
63
+
if _, ok := GetSearchInputByAllowedValues[input.By]; !ok {
64
+
logger.Error("invalid by supplied", "by", input.By)
65
+
return e.JSON(400, makeErrJson("Invalid `by` supplied. Valid values are name, name-desc, maintainer, depends, optdepends, checkdepends"))
66
+
}
67
+
} else {
68
+
input.By = "name"
69
+
}
70
+
71
+
termPts := strings.Split(e.Request().URL.Path, "/")
72
+
term := termPts[len(termPts)-1]
73
+
74
+
var pkgs []database.PackageInfo
75
+
var err error
76
+
switch input.By {
77
+
case "name":
78
+
pkgs, err = s.db.GetPackagesByName(term)
79
+
case "name-desc":
80
+
pkgs, err = s.db.GetPackagesByDescriptionOrName(term)
81
+
default:
82
+
return e.JSON(500, makeErrJson("Search method not implemented"))
83
+
// case "maintainer":
84
+
// case "depends":
85
+
// case "makedepends":
86
+
// case "optdepends":
87
+
// case "checkdepends":
88
+
}
89
+
90
+
if err != nil {
91
+
return e.JSON(500, makeErrJson("Error searching for packages"))
92
+
}
93
+
94
+
return e.JSON(200, GetSearchOutput{
95
+
Version: 5,
96
+
Type: "search",
97
+
ResultCount: len(pkgs),
98
+
Results: pkgs,
99
+
})
100
+
}
+163
myaur/server/server.go
+163
myaur/server/server.go
···
1
+
package server
2
+
3
+
import (
4
+
"context"
5
+
"fmt"
6
+
"log/slog"
7
+
"net/http"
8
+
"os"
9
+
"os/signal"
10
+
"syscall"
11
+
"time"
12
+
13
+
"github.com/haileyok/myaur/myaur/database"
14
+
"github.com/labstack/echo/v4"
15
+
"github.com/labstack/gommon/log"
16
+
)
17
+
18
+
type Server struct {
19
+
logger *slog.Logger
20
+
echo *echo.Echo
21
+
httpd *http.Server
22
+
metricsHttpd *http.Server
23
+
db *database.Database
24
+
}
25
+
26
+
type Args struct {
27
+
Addr string
28
+
MetricsAddr string
29
+
DatabasePath string
30
+
Debug bool
31
+
}
32
+
33
+
func New(args *Args) (*Server, error) {
34
+
level := slog.LevelInfo
35
+
if args.Debug {
36
+
level = slog.LevelDebug
37
+
}
38
+
39
+
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
40
+
Level: level,
41
+
}))
42
+
43
+
e := echo.New()
44
+
45
+
httpd := http.Server{
46
+
Addr: args.Addr,
47
+
Handler: e,
48
+
}
49
+
50
+
metricsHttpd := http.Server{
51
+
Addr: args.MetricsAddr,
52
+
}
53
+
54
+
db, err := database.New(&database.Args{
55
+
DatabasePath: args.DatabasePath,
56
+
Debug: args.Debug,
57
+
})
58
+
if err != nil {
59
+
return nil, fmt.Errorf("failed to create new database client: %w", err)
60
+
}
61
+
62
+
s := Server{
63
+
echo: e,
64
+
httpd: &httpd,
65
+
metricsHttpd: &metricsHttpd,
66
+
db: db,
67
+
logger: logger,
68
+
}
69
+
70
+
return &s, nil
71
+
}
72
+
73
+
func (s *Server) Serve(ctx context.Context) error {
74
+
go func() {
75
+
logger := s.logger.With("component", "metrics-httpd")
76
+
77
+
go func() {
78
+
if err := s.metricsHttpd.ListenAndServe(); err != http.ErrServerClosed {
79
+
logger.Error("error listening", "err", err)
80
+
}
81
+
}()
82
+
83
+
logger.Info("myaur metrics server listening", "addr", s.metricsHttpd.Addr)
84
+
}()
85
+
86
+
shutdownEcho := make(chan struct{})
87
+
echoShutdown := make(chan struct{})
88
+
go func() {
89
+
logger := s.logger.With("component", "echo")
90
+
91
+
logger.Info("adding routes...")
92
+
s.addRoutes()
93
+
logger.Info("routes added")
94
+
95
+
go func() {
96
+
if err := s.httpd.ListenAndServe(); err != http.ErrServerClosed {
97
+
logger.Error("error listning", "err", err)
98
+
close(shutdownEcho)
99
+
}
100
+
}()
101
+
102
+
logger.Info("myaur api server listening", "addr", s.httpd.Addr)
103
+
104
+
<-shutdownEcho
105
+
106
+
logger.Info("shutting down myaur api server")
107
+
108
+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
109
+
defer func() {
110
+
cancel()
111
+
close(echoShutdown)
112
+
}()
113
+
114
+
if err := s.httpd.Shutdown(ctx); err != nil {
115
+
logger.Error("failed to shutdown myaur api server", "err", err)
116
+
return
117
+
}
118
+
119
+
log.Info("myaur api server shutdown")
120
+
}()
121
+
122
+
signals := make(chan os.Signal, 1)
123
+
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM)
124
+
125
+
select {
126
+
// if we receive a signal to shutdown, do so gracefully
127
+
case sig := <-signals:
128
+
s.logger.Info("shutting down on signal", "signal", sig)
129
+
130
+
// close echo here since it shouldn't have been closed yet
131
+
close(shutdownEcho)
132
+
// if echo shutdowns unexepectdly, cleanup
133
+
case <-echoShutdown:
134
+
s.logger.Warn("echo shutdown unexpectedly")
135
+
// echo should have already been closed
136
+
}
137
+
138
+
select {
139
+
case <-echoShutdown:
140
+
s.logger.Info("echo shutdown gracefully")
141
+
case <-time.After(5 * time.Second):
142
+
s.logger.Warn("echo did not shut down after five seconds. forcefully exiting.")
143
+
}
144
+
145
+
s.logger.Info("myaur shutdown")
146
+
147
+
return nil
148
+
}
149
+
150
+
func (s *Server) addRoutes() {
151
+
s.echo.GET("/rpc/v5/info", s.handleGetInfo)
152
+
s.echo.GET("/rpc/v5/search", s.handleGetSearch)
153
+
}
154
+
155
+
func makeErrorJson(error string, message string) map[string]string {
156
+
jsonMap := map[string]string{
157
+
"error": error,
158
+
}
159
+
if message != "" {
160
+
jsonMap["message"] = message
161
+
}
162
+
return jsonMap
163
+
}
+70
myaur/srcinfo/parser.go
+70
myaur/srcinfo/parser.go
···
1
+
package srcinfo
2
+
3
+
import (
4
+
"bufio"
5
+
"fmt"
6
+
"strings"
7
+
8
+
"github.com/haileyok/myaur/myaur/database"
9
+
)
10
+
11
+
func Parse(content string) (*database.PackageInfo, error) {
12
+
pkg := &database.PackageInfo{}
13
+
scanner := bufio.NewScanner(strings.NewReader(content))
14
+
15
+
// each looks like `key = val`. most of the lines will have whitespace infront of
16
+
// them, so we remove that
17
+
for scanner.Scan() {
18
+
// grab the line, removing any whitespace
19
+
line := strings.TrimSpace(scanner.Text())
20
+
21
+
// skip any empty lines or ones that are commented out
22
+
if line == "" || strings.HasPrefix(line, "#") {
23
+
continue
24
+
}
25
+
26
+
// we could probably split on ` = `, but i suppose its possible for these to have
27
+
// any number of spaces infront of/after the `=`, so we'll be safe
28
+
pts := strings.SplitN(line, "=", 2)
29
+
if len(pts) != 2 {
30
+
continue
31
+
}
32
+
33
+
// remove the extra whitespace
34
+
key := strings.TrimSpace(pts[0])
35
+
value := strings.TrimSpace(pts[1])
36
+
37
+
switch key {
38
+
case "pkgname":
39
+
pkg.Name = value
40
+
case "pkgbase":
41
+
pkg.PackageBase = value
42
+
case "pkgver":
43
+
pkg.Version = value
44
+
case "pkgrel":
45
+
if pkg.Version != "" {
46
+
pkg.Version = pkg.Version + "-" + value
47
+
}
48
+
case "pkgdesc":
49
+
pkg.Description = value
50
+
case "url":
51
+
pkg.Url = value
52
+
case "depends":
53
+
pkg.Depends = append(pkg.Depends, value)
54
+
case "makedepends":
55
+
pkg.MakeDepends = append(pkg.MakeDepends, value)
56
+
case "license":
57
+
pkg.License = append(pkg.License, value)
58
+
}
59
+
}
60
+
61
+
if err := scanner.Err(); err != nil {
62
+
return nil, fmt.Errorf("error scanning srcinfo: %w", err)
63
+
}
64
+
65
+
if pkg.Name == "" {
66
+
return nil, fmt.Errorf("missing required field: pkgname")
67
+
}
68
+
69
+
return pkg, nil
70
+
}