2019-04-17 18:12:27 +03:00
|
|
|
// Copyright 2019 The Go Authors. All rights reserved.
|
|
|
|
// Use of this source code is governed by a BSD-style
|
|
|
|
// license that can be found in the LICENSE file.
|
|
|
|
|
|
|
|
package frontend
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2019-12-19 16:17:14 +03:00
|
|
|
"errors"
|
2019-04-17 18:12:27 +03:00
|
|
|
"fmt"
|
2019-09-26 20:18:09 +03:00
|
|
|
"math"
|
2019-04-17 18:12:27 +03:00
|
|
|
"net/http"
|
2019-05-07 17:25:28 +03:00
|
|
|
"path"
|
2019-04-17 18:12:27 +03:00
|
|
|
"strings"
|
|
|
|
|
2020-04-21 23:51:29 +03:00
|
|
|
"golang.org/x/pkgsite/internal"
|
|
|
|
"golang.org/x/pkgsite/internal/derrors"
|
2020-07-07 05:10:24 +03:00
|
|
|
"golang.org/x/pkgsite/internal/experiment"
|
2020-04-21 23:51:29 +03:00
|
|
|
"golang.org/x/pkgsite/internal/log"
|
2020-06-11 23:10:04 +03:00
|
|
|
"golang.org/x/pkgsite/internal/postgres"
|
2019-04-17 18:12:27 +03:00
|
|
|
)
|
|
|
|
|
2019-05-02 22:49:45 +03:00
|
|
|
const defaultSearchLimit = 10
|
2019-04-17 20:47:58 +03:00
|
|
|
|
2019-04-17 18:12:27 +03:00
|
|
|
// SearchPage contains all of the data that the search template needs to
|
|
|
|
// populate.
|
|
|
|
type SearchPage struct {
|
2019-07-02 20:36:59 +03:00
|
|
|
basePage
|
|
|
|
Pagination pagination
|
|
|
|
Results []*SearchResult
|
2019-04-17 18:12:27 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// SearchResult contains data needed to display a single search result.
|
|
|
|
type SearchResult struct {
|
2019-11-21 15:04:51 +03:00
|
|
|
Name string
|
|
|
|
PackagePath string
|
|
|
|
ModulePath string
|
|
|
|
Synopsis string
|
|
|
|
DisplayVersion string
|
|
|
|
Licenses []string
|
|
|
|
CommitTime string
|
|
|
|
NumImportedBy uint64
|
|
|
|
Approximate bool
|
2019-04-17 18:12:27 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// fetchSearchPage fetches data matching the search query from the database and
|
|
|
|
// returns a SearchPage.
|
2020-06-11 23:10:04 +03:00
|
|
|
func fetchSearchPage(ctx context.Context, db *postgres.DB, query string, pageParams paginationParams) (*SearchPage, error) {
|
|
|
|
dbresults, err := db.Search(ctx, query, pageParams.limit, pageParams.offset())
|
2019-04-17 18:12:27 +03:00
|
|
|
if err != nil {
|
2019-08-14 22:08:42 +03:00
|
|
|
return nil, err
|
2019-04-17 18:12:27 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
var results []*SearchResult
|
|
|
|
for _, r := range dbresults {
|
|
|
|
results = append(results, &SearchResult{
|
2019-11-21 15:04:51 +03:00
|
|
|
Name: r.Name,
|
|
|
|
PackagePath: r.PackagePath,
|
|
|
|
ModulePath: r.ModulePath,
|
|
|
|
Synopsis: r.Synopsis,
|
|
|
|
DisplayVersion: displayVersion(r.Version, r.ModulePath),
|
|
|
|
Licenses: r.Licenses,
|
|
|
|
CommitTime: elapsedTime(r.CommitTime),
|
|
|
|
NumImportedBy: r.NumImportedBy,
|
2019-04-17 18:12:27 +03:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2019-09-26 20:18:09 +03:00
|
|
|
var (
|
|
|
|
numResults int
|
|
|
|
approximate bool
|
|
|
|
)
|
2019-04-17 20:47:58 +03:00
|
|
|
if len(dbresults) > 0 {
|
2019-04-27 17:46:59 +03:00
|
|
|
numResults = int(dbresults[0].NumResults)
|
2019-09-26 20:18:09 +03:00
|
|
|
if dbresults[0].Approximate {
|
|
|
|
// 128 buckets corresponds to a standard error of 10%.
|
|
|
|
// http://algo.inria.fr/flajolet/Publications/FlFuGaMe07.pdf
|
|
|
|
numResults = approximateNumber(numResults, 0.1)
|
2019-10-02 21:25:16 +03:00
|
|
|
approximate = true
|
2019-09-26 20:18:09 +03:00
|
|
|
}
|
2019-04-17 20:47:58 +03:00
|
|
|
}
|
2019-04-27 17:46:59 +03:00
|
|
|
|
2019-09-26 20:18:09 +03:00
|
|
|
pgs := newPagination(pageParams, len(results), numResults)
|
|
|
|
pgs.Approximate = approximate
|
2019-04-17 18:12:27 +03:00
|
|
|
return &SearchPage{
|
2019-04-27 17:46:59 +03:00
|
|
|
Results: results,
|
2019-09-26 20:18:09 +03:00
|
|
|
Pagination: pgs,
|
2019-04-17 18:12:27 +03:00
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2019-09-26 20:18:09 +03:00
|
|
|
// approximateNumber returns an approximation of the estimate, calibrated by
|
|
|
|
// the statistical estimate of standard error.
|
|
|
|
// i.e., a number that isn't misleading when we say '1-10 of approximately N
|
|
|
|
// results', but that is still close to our estimate.
|
|
|
|
func approximateNumber(estimate int, sigma float64) int {
|
|
|
|
expectedErr := sigma * float64(estimate)
|
|
|
|
// Compute the unit by rounding the error the logarithmically closest power
|
|
|
|
// of 10, so that 300->100, but 400->1000.
|
|
|
|
unit := math.Pow(10, math.Round(math.Log10(expectedErr)))
|
|
|
|
// Now round the estimate to the nearest unit.
|
|
|
|
return int(unit * math.Round(float64(estimate)/unit))
|
|
|
|
}
|
|
|
|
|
internal/frontend: handlers return error
Instead of serving an error page themselves, top-level handlers return
an error with the information for serving the error page.
This is a largely mechanical rewrite that removes a lot of clumsiness
from the handling code. For example, instead of
if err != nil {
log.Errorf(ctx, "frobbing: %v", err)
s.serveErrorPage(w, r, http.StatusInternalServerError, nil)
return
}
the code is both simpler and more idiomatic:
if err != nil {
return fmt.Errorf("frobbing: %v", err)
}
It is still possible to include an errorPage with the error, like so:
if err != nil {
return &serverError{
status: http.StatusBadRequest,
epage: &errorPage{...},
err: err,
}
}
Change-Id: Id076894b1cb912fe0731fdf6fbdb7b0e5b972bcb
Reviewed-on: https://team-review.git.corp.google.com/c/golang/discovery/+/686039
CI-Result: Cloud Build <devtools-proctor-result-processor@system.gserviceaccount.com>
Reviewed-by: Julie Qiu <julieqiu@google.com>
2020-03-08 18:05:56 +03:00
|
|
|
// serveSearch applies database data to the search template. Handles endpoint
|
2019-05-07 17:25:28 +03:00
|
|
|
// /search?q=<query>. If <query> is an exact match for a package path, the user
|
|
|
|
// will be redirected to the details page.
|
2020-07-22 16:12:04 +03:00
|
|
|
func (s *Server) serveSearch(w http.ResponseWriter, r *http.Request, ds internal.DataSource) error {
|
2020-07-08 04:51:35 +03:00
|
|
|
if r.Method != http.MethodGet {
|
|
|
|
return &serverError{status: http.StatusMethodNotAllowed}
|
|
|
|
}
|
2020-07-22 16:12:04 +03:00
|
|
|
db, ok := ds.(*postgres.DB)
|
2020-06-11 23:10:04 +03:00
|
|
|
if !ok {
|
|
|
|
// The proxydatasource does not support the imported by page.
|
2020-06-19 17:27:56 +03:00
|
|
|
return proxydatasourceNotSupportedErr()
|
2020-06-11 23:10:04 +03:00
|
|
|
}
|
|
|
|
|
2019-04-17 18:12:27 +03:00
|
|
|
ctx := r.Context()
|
2019-07-02 20:36:59 +03:00
|
|
|
query := searchQuery(r)
|
2019-04-17 18:12:27 +03:00
|
|
|
if query == "" {
|
|
|
|
http.Redirect(w, r, "/", http.StatusFound)
|
internal/frontend: handlers return error
Instead of serving an error page themselves, top-level handlers return
an error with the information for serving the error page.
This is a largely mechanical rewrite that removes a lot of clumsiness
from the handling code. For example, instead of
if err != nil {
log.Errorf(ctx, "frobbing: %v", err)
s.serveErrorPage(w, r, http.StatusInternalServerError, nil)
return
}
the code is both simpler and more idiomatic:
if err != nil {
return fmt.Errorf("frobbing: %v", err)
}
It is still possible to include an errorPage with the error, like so:
if err != nil {
return &serverError{
status: http.StatusBadRequest,
epage: &errorPage{...},
err: err,
}
}
Change-Id: Id076894b1cb912fe0731fdf6fbdb7b0e5b972bcb
Reviewed-on: https://team-review.git.corp.google.com/c/golang/discovery/+/686039
CI-Result: Cloud Build <devtools-proctor-result-processor@system.gserviceaccount.com>
Reviewed-by: Julie Qiu <julieqiu@google.com>
2020-03-08 18:05:56 +03:00
|
|
|
return nil
|
2019-04-17 18:12:27 +03:00
|
|
|
}
|
2019-04-17 20:47:58 +03:00
|
|
|
|
2020-07-22 16:12:04 +03:00
|
|
|
if path := searchRequestRedirectPath(ctx, ds, query); path != "" {
|
2020-01-29 10:01:34 +03:00
|
|
|
http.Redirect(w, r, path, http.StatusFound)
|
internal/frontend: handlers return error
Instead of serving an error page themselves, top-level handlers return
an error with the information for serving the error page.
This is a largely mechanical rewrite that removes a lot of clumsiness
from the handling code. For example, instead of
if err != nil {
log.Errorf(ctx, "frobbing: %v", err)
s.serveErrorPage(w, r, http.StatusInternalServerError, nil)
return
}
the code is both simpler and more idiomatic:
if err != nil {
return fmt.Errorf("frobbing: %v", err)
}
It is still possible to include an errorPage with the error, like so:
if err != nil {
return &serverError{
status: http.StatusBadRequest,
epage: &errorPage{...},
err: err,
}
}
Change-Id: Id076894b1cb912fe0731fdf6fbdb7b0e5b972bcb
Reviewed-on: https://team-review.git.corp.google.com/c/golang/discovery/+/686039
CI-Result: Cloud Build <devtools-proctor-result-processor@system.gserviceaccount.com>
Reviewed-by: Julie Qiu <julieqiu@google.com>
2020-03-08 18:05:56 +03:00
|
|
|
return nil
|
2019-05-07 17:25:28 +03:00
|
|
|
}
|
2020-06-11 23:10:04 +03:00
|
|
|
page, err := fetchSearchPage(ctx, db, query, newPaginationParams(r, defaultSearchLimit))
|
2019-04-17 18:12:27 +03:00
|
|
|
if err != nil {
|
internal/frontend: handlers return error
Instead of serving an error page themselves, top-level handlers return
an error with the information for serving the error page.
This is a largely mechanical rewrite that removes a lot of clumsiness
from the handling code. For example, instead of
if err != nil {
log.Errorf(ctx, "frobbing: %v", err)
s.serveErrorPage(w, r, http.StatusInternalServerError, nil)
return
}
the code is both simpler and more idiomatic:
if err != nil {
return fmt.Errorf("frobbing: %v", err)
}
It is still possible to include an errorPage with the error, like so:
if err != nil {
return &serverError{
status: http.StatusBadRequest,
epage: &errorPage{...},
err: err,
}
}
Change-Id: Id076894b1cb912fe0731fdf6fbdb7b0e5b972bcb
Reviewed-on: https://team-review.git.corp.google.com/c/golang/discovery/+/686039
CI-Result: Cloud Build <devtools-proctor-result-processor@system.gserviceaccount.com>
Reviewed-by: Julie Qiu <julieqiu@google.com>
2020-03-08 18:05:56 +03:00
|
|
|
return fmt.Errorf("fetchSearchPage(ctx, db, %q): %v", query, err)
|
2019-04-17 18:12:27 +03:00
|
|
|
}
|
2020-05-21 00:49:09 +03:00
|
|
|
page.basePage = s.newBasePage(r, query)
|
2019-12-18 16:38:16 +03:00
|
|
|
s.servePage(ctx, w, "search.tmpl", page)
|
internal/frontend: handlers return error
Instead of serving an error page themselves, top-level handlers return
an error with the information for serving the error page.
This is a largely mechanical rewrite that removes a lot of clumsiness
from the handling code. For example, instead of
if err != nil {
log.Errorf(ctx, "frobbing: %v", err)
s.serveErrorPage(w, r, http.StatusInternalServerError, nil)
return
}
the code is both simpler and more idiomatic:
if err != nil {
return fmt.Errorf("frobbing: %v", err)
}
It is still possible to include an errorPage with the error, like so:
if err != nil {
return &serverError{
status: http.StatusBadRequest,
epage: &errorPage{...},
err: err,
}
}
Change-Id: Id076894b1cb912fe0731fdf6fbdb7b0e5b972bcb
Reviewed-on: https://team-review.git.corp.google.com/c/golang/discovery/+/686039
CI-Result: Cloud Build <devtools-proctor-result-processor@system.gserviceaccount.com>
Reviewed-by: Julie Qiu <julieqiu@google.com>
2020-03-08 18:05:56 +03:00
|
|
|
return nil
|
2019-04-17 18:12:27 +03:00
|
|
|
}
|
2019-07-02 20:36:59 +03:00
|
|
|
|
2020-01-29 10:01:34 +03:00
|
|
|
// searchRequestRedirectPath returns the path that a search request should be
|
2020-06-04 17:24:10 +03:00
|
|
|
// redirected to, or the empty string if there is no such path. If the user
|
|
|
|
// types an existing package path into the search bar, we will redirect the
|
|
|
|
// user to the details page. Standard library packages that only contain one
|
|
|
|
// element (such as fmt, errors, etc.) will not redirect, to allow users to
|
|
|
|
// search by those terms.
|
2020-01-29 10:01:34 +03:00
|
|
|
func searchRequestRedirectPath(ctx context.Context, ds internal.DataSource, query string) string {
|
|
|
|
requestedPath := path.Clean(query)
|
|
|
|
if !strings.Contains(requestedPath, "/") {
|
|
|
|
return ""
|
|
|
|
}
|
2020-07-07 05:10:24 +03:00
|
|
|
if experiment.IsActive(ctx, internal.ExperimentUsePathInfo) {
|
2020-06-04 17:24:10 +03:00
|
|
|
modulePath, _, isPackage, err := ds.GetPathInfo(ctx, requestedPath, internal.UnknownModulePath, internal.LatestVersion)
|
|
|
|
if err != nil {
|
|
|
|
if !errors.Is(err, derrors.NotFound) {
|
|
|
|
log.Errorf(ctx, "searchRequestRedirectPath(%q): %v", requestedPath, err)
|
|
|
|
}
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
if isPackage || modulePath != requestedPath {
|
|
|
|
return fmt.Sprintf("/%s", requestedPath)
|
|
|
|
}
|
|
|
|
return fmt.Sprintf("/mod/%s", requestedPath)
|
|
|
|
}
|
|
|
|
|
2020-06-16 03:53:53 +03:00
|
|
|
pkg, err := ds.LegacyGetPackage(ctx, requestedPath, internal.UnknownModulePath, internal.LatestVersion)
|
2020-01-29 10:01:34 +03:00
|
|
|
if err == nil {
|
|
|
|
return fmt.Sprintf("/%s", pkg.Path)
|
|
|
|
} else if !errors.Is(err, derrors.NotFound) {
|
|
|
|
log.Errorf(ctx, "error getting package for %s: %v", requestedPath, err)
|
|
|
|
return ""
|
|
|
|
}
|
2020-06-16 04:14:28 +03:00
|
|
|
mi, err := ds.LegacyGetModuleInfo(ctx, requestedPath, internal.LatestVersion)
|
2020-01-29 10:01:34 +03:00
|
|
|
if err == nil {
|
2020-02-25 00:28:22 +03:00
|
|
|
return fmt.Sprintf("/mod/%s", mi.ModulePath)
|
2020-01-29 10:01:34 +03:00
|
|
|
} else if !errors.Is(err, derrors.NotFound) {
|
|
|
|
log.Errorf(ctx, "error getting module for %s: %v", requestedPath, err)
|
|
|
|
return ""
|
|
|
|
}
|
2020-06-16 03:50:02 +03:00
|
|
|
dir, err := ds.LegacyGetDirectory(ctx, requestedPath, internal.UnknownModulePath, internal.LatestVersion, internal.AllFields)
|
2020-01-29 10:01:34 +03:00
|
|
|
if err == nil {
|
|
|
|
return fmt.Sprintf("/%s", dir.Path)
|
|
|
|
} else if !errors.Is(err, derrors.NotFound) {
|
|
|
|
log.Errorf(ctx, "error getting directory for %s: %v", requestedPath, err)
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
2019-07-02 20:36:59 +03:00
|
|
|
// searchQuery extracts a search query from the request.
|
|
|
|
func searchQuery(r *http.Request) string {
|
|
|
|
return strings.TrimSpace(r.FormValue("q"))
|
|
|
|
}
|