yay/pkg/metadata/metadata_aur.go
2022-11-13 23:53:37 +01:00

256 lines
5.5 KiB
Go

package metadata
import (
"context"
"encoding/json"
"fmt"
"log"
"os"
"time"
"github.com/Jguer/aur"
"github.com/itchyny/gojq"
"github.com/ohler55/ojg/oj"
)
const (
searchCacheCap = 300
cacheValidity = 1 * time.Hour
)
type AURCache struct {
cache []byte
searchCache map[string][]*aur.Pkg
cachePath string
unmarshalledCache []interface{}
cacheHits int
gojqCode *gojq.Code
DebugLoggerFn func(a ...interface{})
}
type AURQuery struct {
Needles []string
By aur.By
Contains bool // if true, search for packages containing the needle, not exact matches
}
func NewAURCache(cachePath string) (*AURCache, error) {
aurCache, err := MakeOrReadCache(cachePath)
if err != nil {
return nil, err
}
inputStruct, err := oj.Parse(aurCache)
return &AURCache{
cache: aurCache,
cachePath: cachePath,
searchCache: make(map[string][]*aur.Pkg, searchCacheCap),
unmarshalledCache: inputStruct.([]interface{}),
gojqCode: makeGoJQ(),
}, nil
}
// needsUpdate checks if cachepath is older than 24 hours
func (a *AURCache) needsUpdate() (bool, error) {
// check if cache is older than 24 hours
info, err := os.Stat(a.cachePath)
if err != nil {
return false, fmt.Errorf("unable to read cache: %w", err)
}
return info.ModTime().Before(time.Now().Add(-cacheValidity)), nil
}
func (a *AURCache) cacheKey(needle string, byProvides, byBase, byName bool) string {
return fmt.Sprintf("%s-%v-%v-%v", needle, byProvides, byBase, byName)
}
func (a *AURCache) DebugInfo() {
fmt.Println("Byte Cache", len(a.cache))
fmt.Println("Entries Cached", len(a.searchCache))
fmt.Println("Cache Hits", a.cacheHits)
}
func (a *AURCache) SetProvideCache(needle string, pkgs []*aur.Pkg) {
a.searchCache[needle] = pkgs
}
// Get returns a list of packages that provide the given search term.
func (a *AURCache) Get(ctx context.Context, query *AURQuery) ([]*aur.Pkg, error) {
update, err := a.needsUpdate()
if err != nil {
return nil, err
}
if update {
if a.DebugLoggerFn != nil {
a.DebugLoggerFn("AUR Cache is out of date, updating")
}
var makeErr error
if a.cache, makeErr = MakeCache(a.cachePath); makeErr != nil {
return nil, makeErr
}
inputStruct, unmarshallErr := oj.Parse(a.cache)
if unmarshallErr != nil {
return nil, unmarshallErr
}
a.unmarshalledCache = inputStruct.([]interface{})
}
found := make([]*aur.Pkg, 0, len(query.Needles))
if len(query.Needles) == 0 {
return found, nil
}
iterFound, errNeedle := a.gojqGetBatch(ctx, query)
if errNeedle != nil {
return nil, errNeedle
}
found = append(found, iterFound...)
return found, nil
}
// Get returns a list of packages that provide the given search term
func (a *AURCache) FindPackage(ctx context.Context, needle string) ([]*aur.Pkg, error) {
cacheKey := a.cacheKey(needle, true, true, true)
if pkgs, ok := a.searchCache[cacheKey]; ok {
a.cacheHits++
return pkgs, nil
}
final, error := a.gojqGet(ctx, needle)
if error != nil {
return nil, error
}
a.searchCache[cacheKey] = final
return final, nil
}
func (a *AURCache) gojqGetBatch(ctx context.Context, query *AURQuery) ([]*aur.Pkg, error) {
pattern := ".[] | select("
for i, searchTerm := range query.Needles {
if i != 0 {
pattern += " or "
}
bys := toSearchBy(query.By)
for j, by := range bys {
if query.Contains {
pattern += fmt.Sprintf("(.%s // empty | test(\"%s\"))", by, searchTerm)
} else {
pattern += fmt.Sprintf("(.%s == \"%s\")", by, searchTerm)
}
if j != len(bys)-1 {
pattern += " , "
}
}
}
pattern += ")"
if a.DebugLoggerFn != nil {
a.DebugLoggerFn("AUR metadata query", pattern)
}
parsed, err := gojq.Parse(pattern)
if err != nil {
log.Fatalln(err)
}
final := make([]*aur.Pkg, 0, len(query.Needles))
iter := parsed.RunWithContext(ctx, a.unmarshalledCache) // or query.RunWithContext
for v, ok := iter.Next(); ok; v, ok = iter.Next() {
if err, ok := v.(error); ok {
return nil, err
}
pkg := new(aur.Pkg)
bValue, err := gojq.Marshal(v)
if err != nil {
log.Fatalln(err)
}
oj.Unmarshal(bValue, pkg)
final = append(final, pkg)
}
if a.DebugLoggerFn != nil {
a.DebugLoggerFn("AUR metadata query found", len(final))
}
return final, nil
}
func (a *AURCache) gojqGet(ctx context.Context, searchTerm string) ([]*aur.Pkg, error) {
final := make([]*aur.Pkg, 0, 1)
iter := a.gojqCode.RunWithContext(ctx, a.unmarshalledCache, searchTerm) // or query.RunWithContext
for v, ok := iter.Next(); ok; v, ok = iter.Next() {
if err, ok := v.(error); ok {
return nil, err
}
pkg := &aur.Pkg{}
bValue, err := gojq.Marshal(v)
if err != nil {
log.Fatalln(err)
}
json.Unmarshal(bValue, pkg)
final = append(final, pkg)
}
return final, nil
}
func makeGoJQ() *gojq.Code {
pattern := ".[] | select((.Name == $x) or (.Provides[]? == ($x)))"
query, err := gojq.Parse(pattern)
if err != nil {
log.Fatalln(err)
}
compiled, err := gojq.Compile(query, gojq.WithVariables([]string{"$x"}))
if err != nil {
log.Fatalln(err)
}
return compiled
}
func toSearchBy(by aur.By) []string {
switch by {
case aur.Name:
return []string{"Name"}
case aur.NameDesc:
return []string{"Name", "Description"}
case aur.Maintainer:
return []string{"Maintainer"}
case aur.Depends:
return []string{"Depends[]?"}
case aur.MakeDepends:
return []string{"MakeDepends[]?"}
case aur.OptDepends:
return []string{"OptDepends[]?"}
case aur.CheckDepends:
return []string{"CheckDepends[]?"}
case aur.None:
return []string{"Name", "Provides[]?"}
default:
panic("invalid By")
}
}