diff --git a/cmd/lure-api-server/api.go b/cmd/lure-api-server/api.go index bcceec4..c00671f 100644 --- a/cmd/lure-api-server/api.go +++ b/cmd/lure-api-server/api.go @@ -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} diff --git a/cmd/lure-api-server/badge.go b/cmd/lure-api-server/badge.go index 8af61e9..b926cb7 100644 --- a/cmd/lure-api-server/badge.go +++ b/cmd/lure-api-server/badge.go @@ -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))) diff --git a/internal/api/gen.go b/cmd/lure-api-server/internal/api/gen.go similarity index 100% rename from internal/api/gen.go rename to cmd/lure-api-server/internal/api/gen.go diff --git a/internal/api/lure.pb.go b/cmd/lure-api-server/internal/api/lure.pb.go similarity index 100% rename from internal/api/lure.pb.go rename to cmd/lure-api-server/internal/api/lure.pb.go diff --git a/internal/api/lure.proto b/cmd/lure-api-server/internal/api/lure.proto similarity index 100% rename from internal/api/lure.proto rename to cmd/lure-api-server/internal/api/lure.proto diff --git a/internal/api/lure.twirp.go b/cmd/lure-api-server/internal/api/lure.twirp.go similarity index 100% rename from internal/api/lure.twirp.go rename to cmd/lure-api-server/internal/api/lure.twirp.go diff --git a/cmd/lure-api-server/main.go b/cmd/lure-api-server/main.go index 75c26f1..38828b7 100644 --- a/cmd/lure-api-server/main.go +++ b/cmd/lure-api-server/main.go @@ -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" diff --git a/pkg/search/search.go b/pkg/search/search.go new file mode 100644 index 0000000..d9d48d3 --- /dev/null +++ b/pkg/search/search.go @@ -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 +}