From 05e61978906a08132c4340a5f9ae518134dd0fa9 Mon Sep 17 00:00:00 2001
From: Alan Pearce
Date: Fri, 17 May 2024 14:06:06 +0200
Subject: feat: support searching packages
---
frontend/templates/blocks/packages.gotmpl | 26 ++++
frontend/templates/index.gotmpl | 6 +-
frontend/templates/opensearch.xml.gotmpl | 10 +-
internal/index/search.go | 8 +-
internal/server/mux.go | 208 +++++++++++++++---------------
internal/server/templates.go | 11 ++
6 files changed, 156 insertions(+), 113 deletions(-)
create mode 100644 frontend/templates/blocks/packages.gotmpl
diff --git a/frontend/templates/blocks/packages.gotmpl b/frontend/templates/blocks/packages.gotmpl
new file mode 100644
index 0000000..68f9246
--- /dev/null
+++ b/frontend/templates/blocks/packages.gotmpl
@@ -0,0 +1,26 @@
+{{- define "hits" }}
+ {{- range . }}
+ {{- with .Data }}
+
+
+
+ {{ .Name }}
+
+
+ {{ .Description }}
+
+ {{- with .Version }}
+ - Version
+ - {{ . }}
+ {{- end }}
+ {{- with .Definition }}
+ - Defined
+ -
+ Source
+
+ {{- end }}
+
+
+ {{- end }}
+ {{- end }}
+{{- end }}
diff --git a/frontend/templates/index.gotmpl b/frontend/templates/index.gotmpl
index 90f875b..0211a5b 100644
--- a/frontend/templates/index.gotmpl
+++ b/frontend/templates/index.gotmpl
@@ -13,8 +13,8 @@
{{- end }}
@@ -25,7 +25,7 @@
{{- range $key, $value := .Sources }}
{{- $value.Name -}}
diff --git a/frontend/templates/opensearch.xml.gotmpl b/frontend/templates/opensearch.xml.gotmpl
index 8d978ea..c761fd4 100644
--- a/frontend/templates/opensearch.xml.gotmpl
+++ b/frontend/templates/opensearch.xml.gotmpl
@@ -2,15 +2,15 @@
xmlns="http://a9.com/-/spec/opensearch/1.1/"
xmlns:moz="http://www.mozilla.org/2006/browser/search/"
>
- Searchix {{ .Source.Name }}
- Search {{ .Source.Name }} options with Searchix
- Search options {{ .Source.Name }}
+ Searchix {{ sourceNameAndType .Source }}
+ Search {{ sourceNameAndType .Source }} with Searchix
+ Search {{ sourceNameAndType .Source }}
{{ .BaseURL }}/options/{{ .Source.Key }}/search{{ .BaseURL }}/{{ .Source.ImporterType }}/{{ .Source.Key }}/search
diff --git a/internal/index/search.go b/internal/index/search.go
index a86cc02..5c18edb 100644
--- a/internal/index/search.go
+++ b/internal/index/search.go
@@ -4,6 +4,7 @@ import (
"bytes"
"context"
"encoding/gob"
+ "searchix/internal/config"
"searchix/internal/nix"
"github.com/blevesearch/bleve/v2"
@@ -15,7 +16,7 @@ const ResultsPerPage = 20
type DocumentMatch struct {
search.DocumentMatch
- Data nix.Option
+ Data nix.Importable
}
type Result struct {
@@ -53,11 +54,12 @@ func (index *ReadIndex) GetSource(ctx context.Context, name string) (*bleve.Sear
func (index *ReadIndex) Search(
ctx context.Context,
- source string,
+ source *config.Source,
keyword string,
from uint64,
) (*Result, error) {
- sourceQuery := bleve.NewTermQuery(source)
+ sourceQuery := bleve.NewTermQuery(source.Key)
+ sourceQuery.SetField("Source")
userQuery := bleve.NewMatchQuery(keyword)
userQuery.Analyzer = "option_name"
diff --git a/internal/server/mux.go b/internal/server/mux.go
index 78fee60..27cd4bf 100644
--- a/internal/server/mux.go
+++ b/internal/server/mux.go
@@ -17,7 +17,6 @@ import (
"searchix/frontend"
"searchix/internal/config"
search "searchix/internal/index"
- "searchix/internal/nix"
"github.com/blevesearch/bleve/v2"
sentryhttp "github.com/getsentry/sentry-go/http"
@@ -51,7 +50,7 @@ type TemplateData struct {
Message string
}
-type ResultData[T nix.Option] struct {
+type ResultData struct {
TemplateData
Query string
ResultsPerPage int
@@ -109,131 +108,133 @@ func NewMux(
})
const searchTimeout = 1 * time.Second
- mux.HandleFunc("/options/{source}/search", func(w http.ResponseWriter, r *http.Request) {
- sourceKey := r.PathValue("source")
+ createSearchHandler := func(importerType config.ImporterType) func(http.ResponseWriter, *http.Request) {
+ return func(w http.ResponseWriter, r *http.Request) {
+ var err error
+ source := cfg.Importer.Sources[r.PathValue("source")]
+ if source == nil || importerType != source.Importer {
+ errorHandler(w, r, http.StatusText(http.StatusNotFound), http.StatusNotFound)
- source := cfg.Importer.Sources[sourceKey]
- if source == nil {
- errorHandler(w, r, "Source not found", http.StatusNotFound)
-
- return
- }
+ return
+ }
- ctx, cancel := context.WithTimeout(r.Context(), searchTimeout)
- defer cancel()
-
- if r.URL.Query().Has("query") {
- qs := r.URL.Query().Get("query")
- pg := r.URL.Query().Get("page")
- var page uint64 = 1
- if pg != "" {
- page, err = strconv.ParseUint(pg, 10, 64)
- if err != nil || page == 0 {
- errorHandler(w, r, "Bad query string", http.StatusBadRequest)
+ ctx, cancel := context.WithTimeout(r.Context(), searchTimeout)
+ defer cancel()
+
+ if r.URL.Query().Has("query") {
+ qs := r.URL.Query().Get("query")
+ pg := r.URL.Query().Get("page")
+ var page uint64 = 1
+ if pg != "" {
+ page, err = strconv.ParseUint(pg, 10, 64)
+ if err != nil || page == 0 {
+ errorHandler(w, r, "Bad query string", http.StatusBadRequest)
+ }
}
- }
- results, err := index.Search(ctx, sourceKey, qs, (page-1)*search.ResultsPerPage)
- if err != nil {
- if err == context.DeadlineExceeded {
- errorHandler(w, r, "Search timed out", http.StatusInternalServerError)
+ results, err := index.Search(ctx, source, qs, (page-1)*search.ResultsPerPage)
+ if err != nil {
+ if err == context.DeadlineExceeded {
+ errorHandler(w, r, "Search timed out", http.StatusInternalServerError)
- return
+ return
+ }
+ slog.Error("search error", "error", err)
+ errorHandler(w, r, err.Error(), http.StatusInternalServerError)
}
- slog.Error("search error", "error", err)
- errorHandler(w, r, err.Error(), http.StatusInternalServerError)
- }
- tdata := ResultData[nix.Option]{
- TemplateData: TemplateData{
- ExtraHeadHTML: cfg.Web.ExtraHeadHTML,
- Source: *source,
- Sources: cfg.Importer.Sources,
- Version: *versionInfo,
- },
- ResultsPerPage: search.ResultsPerPage,
- Query: qs,
- Results: results,
- }
+ tdata := ResultData{
+ TemplateData: TemplateData{
+ ExtraHeadHTML: cfg.Web.ExtraHeadHTML,
+ Source: *source,
+ Sources: cfg.Importer.Sources,
+ Version: *versionInfo,
+ },
+ ResultsPerPage: search.ResultsPerPage,
+ Query: qs,
+ Results: results,
+ }
- hits := uint64(len(results.Hits))
- if results.Total > hits {
- q, err := url.ParseQuery(r.URL.RawQuery)
- if err != nil {
- errorHandler(w, r, "Query string error", http.StatusBadRequest)
+ hits := uint64(len(results.Hits))
+ if results.Total > hits {
+ q, err := url.ParseQuery(r.URL.RawQuery)
+ if err != nil {
+ errorHandler(w, r, "Query string error", http.StatusBadRequest)
- return
- }
+ return
+ }
- if page*search.ResultsPerPage > results.Total {
- errorHandler(w, r, "Not found", http.StatusNotFound)
+ if page*search.ResultsPerPage > results.Total {
+ errorHandler(w, r, "Not found", http.StatusNotFound)
- return
- }
+ return
+ }
- if page*search.ResultsPerPage < results.Total {
- q.Set("page", strconv.FormatUint(page+1, 10))
- tdata.Next = "search?" + q.Encode()
- }
+ if page*search.ResultsPerPage < results.Total {
+ q.Set("page", strconv.FormatUint(page+1, 10))
+ tdata.Next = "search?" + q.Encode()
+ }
- if page > 1 {
- p := page - 1
- if p == 1 {
- q.Del("page")
- } else {
- q.Set("page", strconv.FormatUint(p, 10))
+ if page > 1 {
+ p := page - 1
+ if p == 1 {
+ q.Del("page")
+ } else {
+ q.Set("page", strconv.FormatUint(p, 10))
+ }
+ tdata.Prev = "search?" + q.Encode()
}
- tdata.Prev = "search?" + q.Encode()
}
- }
- w.Header().Add("Cache-Control", "max-age=300")
- w.Header().Add("Vary", "Fetch")
- if r.Header.Get("Fetch") == "true" {
- w.Header().Add("Content-Type", "text/html; charset=utf-8")
- err = templates["options"].ExecuteTemplate(w, "results", tdata)
+ w.Header().Add("Cache-Control", "max-age=300")
+ w.Header().Add("Vary", "Fetch")
+ if r.Header.Get("Fetch") == "true" {
+ w.Header().Add("Content-Type", "text/html; charset=utf-8")
+ err = templates[importerType.String()].ExecuteTemplate(w, "results", tdata)
+ } else {
+ err = templates[importerType.String()].Execute(w, tdata)
+ }
+ if err != nil {
+ slog.Error("template error", "template", importerType, "error", err)
+ errorHandler(w, r, err.Error(), http.StatusInternalServerError)
+ }
} else {
- err = templates["options"].Execute(w, tdata)
- }
- if err != nil {
- slog.Error("template error", "template", "options", "error", err)
- errorHandler(w, r, err.Error(), http.StatusInternalServerError)
- }
- } else {
- sourceResult, err := index.GetSource(ctx, sourceKey)
- if err != nil {
- errorHandler(w, r, err.Error(), http.StatusInternalServerError)
+ sourceResult, err := index.GetSource(ctx, source.Key)
+ if err != nil {
+ errorHandler(w, r, err.Error(), http.StatusInternalServerError)
- return
- }
+ return
+ }
- w.Header().Add("Cache-Control", "max-age=14400")
- err = templates["search"].Execute(w, TemplateData{
- ExtraHeadHTML: cfg.Web.ExtraHeadHTML,
- Sources: cfg.Importer.Sources,
- Source: *source,
- SourceResult: sourceResult,
- Version: *versionInfo,
- })
- if err != nil {
- errorHandler(w, r, err.Error(), http.StatusInternalServerError)
+ w.Header().Add("Cache-Control", "max-age=14400")
+ err = templates["search"].Execute(w, TemplateData{
+ ExtraHeadHTML: cfg.Web.ExtraHeadHTML,
+ Sources: cfg.Importer.Sources,
+ Source: *source,
+ SourceResult: sourceResult,
+ Version: *versionInfo,
+ })
+ if err != nil {
+ errorHandler(w, r, err.Error(), http.StatusInternalServerError)
- return
+ return
+ }
}
}
- })
+ }
- mux.HandleFunc(
- "/options/{source}/opensearch.xml",
- func(w http.ResponseWriter, r *http.Request) {
+ mux.HandleFunc("/options/{source}/search", createSearchHandler(config.Options))
+ mux.HandleFunc("/packages/{source}/search", createSearchHandler(config.Packages))
+
+ createOpenSearchXMLHandler := func(importerType config.ImporterType) func(http.ResponseWriter, *http.Request) {
+ return func(w http.ResponseWriter, r *http.Request) {
type openSearchData struct {
BaseURL string
Source *config.Source
}
- sourceKey := r.PathValue("source")
- source := cfg.Importer.Sources[sourceKey]
- if source == nil {
- errorHandler(w, r, "Source not found", http.StatusNotFound)
+ source := cfg.Importer.Sources[r.PathValue("source")]
+ if source == nil || importerType != source.Importer {
+ errorHandler(w, r, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
}
@@ -256,8 +257,11 @@ func NewMux(
http.StatusInternalServerError,
)
}
- },
- )
+ }
+ }
+
+ mux.HandleFunc("/options/{source}/opensearch.xml", createOpenSearchXMLHandler(config.Options))
+ mux.HandleFunc("/packages/{source}/opensearch.xml", createOpenSearchXMLHandler(config.Packages))
fs := http.FileServer(http.FS(frontend.Files))
mux.HandleFunc("/static/", func(w http.ResponseWriter, r *http.Request) {
diff --git a/internal/server/templates.go b/internal/server/templates.go
index 3d45167..8967599 100644
--- a/internal/server/templates.go
+++ b/internal/server/templates.go
@@ -8,6 +8,7 @@ import (
"log/slog"
"path"
"searchix/frontend"
+ "searchix/internal/config"
"searchix/internal/nix"
"strings"
@@ -33,6 +34,16 @@ var templateFuncs = template.FuncMap{
return template.HTML(out.String()) // #nosec G203
},
+ "sourceNameAndType": func(source config.Source) (string, error) {
+ switch source.Importer {
+ case config.Options:
+ return source.Name + " " + source.Importer.String(), nil
+ case config.Packages:
+ return source.Name, nil
+ default:
+ return "", errors.Errorf("unknown source importer type %s", source.Importer.String())
+ }
+ },
"sourceName": func(input string) string {
switch input {
case "nixos":
--
cgit 1.4.1