Add pkg/search

This commit is contained in:
Elara 2023-09-22 15:17:12 -07:00
parent be7709a5ed
commit 4774ec3343
8 changed files with 206 additions and 78 deletions

View File

@ -20,90 +20,46 @@ package main
import (
"context"
"os"
"path/filepath"
"strconv"
"strings"
"io"
"github.com/twitchtv/twirp"
"go.elara.ws/lure/internal/api"
"go.elara.ws/lure/cmd/lure-api-server/internal/api"
"go.elara.ws/lure/internal/log"
"go.elara.ws/lure/pkg/config"
"go.elara.ws/lure/pkg/db"
"go.elara.ws/lure/pkg/search"
"golang.org/x/text/language"
)
type lureWebAPI struct{}
func (l lureWebAPI) Search(ctx context.Context, req *api.SearchRequest) (*api.SearchResponse, error) {
query := "(name LIKE ? OR description LIKE ? OR json_array_contains(provides, ?))"
args := []any{"%" + req.Query + "%", "%" + req.Query + "%", req.Query}
if req.FilterValue != nil && req.FilterType != api.FILTER_TYPE_NO_FILTER {
switch req.FilterType {
case api.FILTER_TYPE_IN_REPOSITORY:
query += " AND repository = ?"
case api.FILTER_TYPE_SUPPORTS_ARCH:
query += " AND json_array_contains(architectures, ?)"
}
args = append(args, *req.FilterValue)
}
if req.SortBy != api.SORT_BY_UNSORTED {
switch req.SortBy {
case api.SORT_BY_NAME:
query += " ORDER BY name"
case api.SORT_BY_REPOSITORY:
query += " ORDER BY repository"
case api.SORT_BY_VERSION:
query += " ORDER BY version"
}
}
if req.Limit != 0 {
query += " LIMIT " + strconv.FormatInt(req.Limit, 10)
}
result, err := db.GetPkgs(query, args...)
if err != nil {
return nil, err
}
out := &api.SearchResponse{}
for result.Next() {
pkg := &db.Package{}
err = result.StructScan(pkg)
if err != nil {
return nil, err
}
out.Packages = append(out.Packages, dbPkgToAPI(ctx, pkg))
}
return out, err
pkgs, err := search.Search(search.Options{
Filter: search.Filter(req.FilterType),
SortBy: search.SortBy(req.SortBy),
Limit: req.Limit,
Query: req.Query,
})
return &api.SearchResponse{Packages: searchPkgsToAPI(ctx, pkgs)}, err
}
func (l lureWebAPI) GetPkg(ctx context.Context, req *api.GetPackageRequest) (*api.Package, error) {
pkg, err := db.GetPkg("name = ? AND repository = ?", req.Name, req.Repository)
pkg, err := search.GetPkg(req.Repository, req.Name)
if err != nil {
return nil, err
}
return dbPkgToAPI(ctx, pkg), nil
return searchPkgToAPI(ctx, pkg), nil
}
func (l lureWebAPI) GetBuildScript(ctx context.Context, req *api.GetBuildScriptRequest) (*api.GetBuildScriptResponse, error) {
if strings.ContainsAny(req.Name, "./") || strings.ContainsAny(req.Repository, "./") {
return nil, twirp.NewError(twirp.InvalidArgument, "name and repository must not contain . or /")
}
scriptPath := filepath.Join(config.GetPaths().RepoDir, req.Repository, req.Name, "lure.sh")
_, err := os.Stat(scriptPath)
if os.IsNotExist(err) {
return nil, twirp.NewError(twirp.NotFound, "requested package not found")
r, err := search.GetScript(req.Repository, req.Name)
if err == search.ErrScriptNotFound {
return nil, twirp.NewError(twirp.NotFound, err.Error())
} else if err == search.ErrInvalidArgument {
return nil, twirp.NewError(twirp.InvalidArgument, err.Error())
} else if err != nil {
return nil, err
}
data, err := os.ReadFile(scriptPath)
data, err := io.ReadAll(r)
if err != nil {
return nil, err
}
@ -111,23 +67,31 @@ func (l lureWebAPI) GetBuildScript(ctx context.Context, req *api.GetBuildScriptR
return &api.GetBuildScriptResponse{Script: string(data)}, nil
}
func dbPkgToAPI(ctx context.Context, pkg *db.Package) *api.Package {
func searchPkgsToAPI(ctx context.Context, pkgs []search.Package) []*api.Package {
out := make([]*api.Package, len(pkgs))
for i, pkg := range pkgs {
out[i] = searchPkgToAPI(ctx, pkg)
}
return out
}
func searchPkgToAPI(ctx context.Context, pkg search.Package) *api.Package {
return &api.Package{
Name: pkg.Name,
Repository: pkg.Repository,
Version: pkg.Version,
Release: int64(pkg.Release),
Epoch: ptr(int64(pkg.Epoch)),
Description: performTranslation(ctx, pkg.Description.Val),
Homepage: performTranslation(ctx, pkg.Homepage.Val),
Maintainer: performTranslation(ctx, pkg.Maintainer.Val),
Architectures: pkg.Architectures.Val,
Licenses: pkg.Licenses.Val,
Provides: pkg.Provides.Val,
Conflicts: pkg.Conflicts.Val,
Replaces: pkg.Replaces.Val,
Depends: dbMapToAPI(pkg.Depends.Val),
BuildDepends: dbMapToAPI(pkg.BuildDepends.Val),
Description: performTranslation(ctx, pkg.Description),
Homepage: performTranslation(ctx, pkg.Homepage),
Maintainer: performTranslation(ctx, pkg.Maintainer),
Architectures: pkg.Architectures,
Licenses: pkg.Licenses,
Provides: pkg.Provides,
Conflicts: pkg.Conflicts,
Replaces: pkg.Replaces,
Depends: mapToAPI(pkg.Depends),
BuildDepends: mapToAPI(pkg.BuildDepends),
}
}
@ -135,7 +99,7 @@ func ptr[T any](v T) *T {
return &v
}
func dbMapToAPI(m map[string][]string) map[string]*api.StringList {
func mapToAPI(m map[string][]string) map[string]*api.StringList {
out := make(map[string]*api.StringList, len(m))
for override, list := range m {
out[override] = &api.StringList{Entries: list}

View File

@ -8,7 +8,7 @@ import (
"strings"
"github.com/go-chi/chi/v5"
"go.elara.ws/lure/pkg/db"
"go.elara.ws/lure/pkg/search"
)
//go:embed badge-logo.txt
@ -21,7 +21,7 @@ func handleBadge() http.HandlerFunc {
repo := chi.URLParam(req, "repo")
name := chi.URLParam(req, "pkg")
pkg, err := db.GetPkg("name = ? AND repository = ?", name, repo)
pkg, err := search.GetPkg(repo, name)
if err != nil {
http.Error(res, err.Error(), http.StatusInternalServerError)
return
@ -31,7 +31,7 @@ func handleBadge() http.HandlerFunc {
}
}
func genVersion(pkg *db.Package) string {
func genVersion(pkg search.Package) string {
sb := strings.Builder{}
if pkg.Epoch != 0 {
sb.WriteString(strconv.Itoa(int(pkg.Epoch)))

View File

@ -28,7 +28,7 @@ import (
"github.com/go-chi/chi/v5"
"github.com/twitchtv/twirp"
"go.elara.ws/logger"
"go.elara.ws/lure/internal/api"
"go.elara.ws/lure/cmd/lure-api-server/internal/api"
"go.elara.ws/lure/internal/log"
"go.elara.ws/lure/pkg/config"
"go.elara.ws/lure/pkg/repos"

164
pkg/search/search.go Normal file
View File

@ -0,0 +1,164 @@
package search
import (
"errors"
"io"
"io/fs"
"os"
"path/filepath"
"strconv"
"strings"
"go.elara.ws/lure/pkg/config"
"go.elara.ws/lure/pkg/db"
)
// Filter represents search filters.
type Filter int
// Filters
const (
FilterNone Filter = iota
FilterInRepo
FilterSupportsArch
)
// SoryBy represents a value that packages can be sorted by.
type SortBy int
// Sort values
const (
SortByNone = iota
SortByName
SortByRepo
SortByVersion
)
// Package represents a package from LURE's database
type Package struct {
Name string
Version string
Release int
Epoch uint
Description map[string]string
Homepage map[string]string
Maintainer map[string]string
Architectures []string
Licenses []string
Provides []string
Conflicts []string
Replaces []string
Depends map[string][]string
BuildDepends map[string][]string
OptDepends map[string][]string
Repository string
}
func convertPkg(p db.Package) Package {
return Package{
Name: p.Name,
Version: p.Version,
Release: p.Release,
Epoch: p.Epoch,
Description: p.Description.Val,
Homepage: p.Homepage.Val,
Maintainer: p.Maintainer.Val,
Architectures: p.Architectures.Val,
Licenses: p.Licenses.Val,
Provides: p.Provides.Val,
Conflicts: p.Conflicts.Val,
Replaces: p.Replaces.Val,
Depends: p.Depends.Val,
OptDepends: p.OptDepends.Val,
Repository: p.Repository,
}
}
// Options contains the options for a search.
type Options struct {
Filter Filter
SortBy SortBy
Limit int64
Query string
}
// Search searches for packages in the database based on the given options.
func Search(opts Options) ([]Package, error) {
query := "(name LIKE ? OR description LIKE ? OR json_array_contains(provides, ?))"
args := []any{"%" + opts.Query + "%", "%" + opts.Query + "%", opts.Query}
if opts.Filter != FilterNone {
switch opts.Filter {
case FilterInRepo:
query += " AND repository = ?"
case FilterSupportsArch:
query += " AND json_array_contains(architectures, ?)"
}
args = append(args, opts.Filter)
}
if opts.SortBy != SortByNone {
switch opts.SortBy {
case SortByName:
query += " ORDER BY name"
case SortByRepo:
query += " ORDER BY repository"
case SortByVersion:
query += " ORDER BY version"
}
}
if opts.Limit != 0 {
query += " LIMIT " + strconv.FormatInt(opts.Limit, 10)
}
result, err := db.GetPkgs(query, args...)
if err != nil {
return nil, err
}
var out []Package
for result.Next() {
pkg := db.Package{}
err = result.StructScan(&pkg)
if err != nil {
return nil, err
}
out = append(out, convertPkg(pkg))
}
return out, err
}
// GetPkg gets a single package from the database and returns it.
func GetPkg(repo, name string) (Package, error) {
pkg, err := db.GetPkg("name = ? AND repository = ?", name, repo)
return convertPkg(*pkg), err
}
var (
// ErrInvalidArgument is an error returned by GetScript when one of its arguments
// contain invalid characters
ErrInvalidArgument = errors.New("name and repository must not contain . or /")
// ErrScriptNotFound is returned by GetScript if it can't find the script requested
// by the user.
ErrScriptNotFound = errors.New("requested script not found")
)
// GetScript returns a reader containing the build script for a given package.
func GetScript(repo, name string) (io.ReadCloser, error) {
if strings.Contains(name, "./") || strings.ContainsAny(repo, "./") {
return nil, ErrInvalidArgument
}
scriptPath := filepath.Join(config.GetPaths().RepoDir, repo, name, "lure.sh")
fl, err := os.Open(scriptPath)
if errors.Is(err, fs.ErrNotExist) {
return nil, ErrScriptNotFound
} else if err != nil {
return nil, err
}
return fl, nil
}