mirror of
https://github.com/token2/snapd.git
synced 2026-03-13 11:15:47 -07:00
* store: Return categories in find results This allows clients to show the categories as snapcraft.io does. Fixes https://bugs.launchpad.net/snapd/+bug/1838786 * Remove unnecessary CategoryInfos type * Only show categories when using --verbose * Add tests for snap info printing categories * Update cmd/snap/cmd_info.go Co-authored-by: Miguel Pires <miguelpires94@gmail.com> * Update cmd/snap/cmd_info.go Co-authored-by: Miguel Pires <miguelpires94@gmail.com> * v1 store API doesn't return categories * Drop category information from snap info Other snap commands don't support categories yet, this change should be part of that. * Add /v2/categories and support /v2/find?category=foo * Add note that section is deprecated * Update client/packages.go Co-authored-by: Miguel Pires <miguelpires94@gmail.com> * Update daemon/api_categories.go Co-authored-by: Miguel Pires <miguelpires94@gmail.com> * Update daemon/api_categories.go Co-authored-by: Miguel Pires <miguelpires94@gmail.com> * Update daemon/api_categories.go Co-authored-by: Miguel Pires <miguelpires94@gmail.com> * Update store/store.go Co-authored-by: Miguel Pires <miguelpires94@gmail.com> * Update store/details_v2.go Co-authored-by: Miguel Pires <miguelpires94@gmail.com> * Update client/packages.go Co-authored-by: Miguel Pires <miguelpires94@gmail.com> * Improve test error message * Drop copy/pasted comments that doesn't seem relevant * Remove unused import * Reorder CategoryInfo struct * Fix accepted content type for store v2/snaps/categories request * Set APILevel for v2/snaps/categories request * Update accept string used to get data for store test * Make /v2/categories return objects not just strings --------- Co-authored-by: Miguel Pires <miguelpires94@gmail.com>
275 lines
6.6 KiB
Go
275 lines
6.6 KiB
Go
// -*- Mode: Go; indent-tabs-mode: t -*-
|
|
|
|
/*
|
|
* Copyright (C) 2015-2020 Canonical Ltd
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License version 3 as
|
|
* published by the Free Software Foundation.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
|
|
package daemon
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
|
|
"github.com/gorilla/mux"
|
|
|
|
"github.com/snapcore/snapd/client"
|
|
"github.com/snapcore/snapd/client/clientutil"
|
|
"github.com/snapcore/snapd/httputil"
|
|
"github.com/snapcore/snapd/logger"
|
|
"github.com/snapcore/snapd/overlord/auth"
|
|
"github.com/snapcore/snapd/snap"
|
|
"github.com/snapcore/snapd/store"
|
|
)
|
|
|
|
var (
|
|
findCmd = &Command{
|
|
Path: "/v2/find",
|
|
GET: searchStore,
|
|
ReadAccess: openAccess{},
|
|
}
|
|
)
|
|
|
|
func searchStore(c *Command, r *http.Request, user *auth.UserState) Response {
|
|
route := c.d.router.Get(snapCmd.Path)
|
|
if route == nil {
|
|
return InternalError("cannot find route for snaps")
|
|
}
|
|
query := r.URL.Query()
|
|
q := query.Get("q")
|
|
commonID := query.Get("common-id")
|
|
section := query.Get("section")
|
|
category := query.Get("category")
|
|
name := query.Get("name")
|
|
scope := query.Get("scope")
|
|
private := false
|
|
prefix := false
|
|
|
|
if sel := query.Get("select"); sel != "" {
|
|
switch sel {
|
|
case "refresh":
|
|
if commonID != "" {
|
|
return BadRequest("cannot use 'common-id' with 'select=refresh'")
|
|
}
|
|
if name != "" {
|
|
return BadRequest("cannot use 'name' with 'select=refresh'")
|
|
}
|
|
if q != "" {
|
|
return BadRequest("cannot use 'q' with 'select=refresh'")
|
|
}
|
|
return storeUpdates(c, r, user)
|
|
case "private":
|
|
private = true
|
|
}
|
|
}
|
|
|
|
if name != "" {
|
|
if q != "" {
|
|
return BadRequest("cannot use 'q' and 'name' together")
|
|
}
|
|
if commonID != "" {
|
|
return BadRequest("cannot use 'common-id' and 'name' together")
|
|
}
|
|
|
|
if name[len(name)-1] != '*' {
|
|
return findOne(c, r, user, name)
|
|
}
|
|
|
|
prefix = true
|
|
q = name[:len(name)-1]
|
|
}
|
|
|
|
if commonID != "" && q != "" {
|
|
return BadRequest("cannot use 'common-id' and 'q' together")
|
|
}
|
|
|
|
if section != "" && category != "" {
|
|
return BadRequest("cannot use 'section' and 'category' together")
|
|
}
|
|
if section != "" {
|
|
category = section
|
|
}
|
|
|
|
theStore := storeFrom(c.d)
|
|
ctx := store.WithClientUserAgent(r.Context(), r)
|
|
found, err := theStore.Find(ctx, &store.Search{
|
|
Query: q,
|
|
Prefix: prefix,
|
|
CommonID: commonID,
|
|
Category: category,
|
|
Private: private,
|
|
Scope: scope,
|
|
}, user)
|
|
switch err {
|
|
case nil:
|
|
// pass
|
|
case store.ErrBadQuery:
|
|
return BadQuery()
|
|
case store.ErrUnauthenticated, store.ErrInvalidCredentials:
|
|
return Unauthorized(err.Error())
|
|
default:
|
|
// XXX should these return 503 actually?
|
|
if e, ok := err.(*url.Error); ok {
|
|
if neterr, ok := e.Err.(*net.OpError); ok {
|
|
if dnserr, ok := neterr.Err.(*net.DNSError); ok {
|
|
return &apiError{
|
|
Status: 400,
|
|
Message: dnserr.Error(),
|
|
Kind: client.ErrorKindDNSFailure,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if e, ok := err.(net.Error); ok && e.Timeout() {
|
|
return &apiError{
|
|
Status: 400,
|
|
Message: err.Error(),
|
|
Kind: client.ErrorKindNetworkTimeout,
|
|
}
|
|
}
|
|
if e, ok := err.(*httputil.PersistentNetworkError); ok {
|
|
return &apiError{
|
|
Status: 400,
|
|
Message: e.Error(),
|
|
Kind: client.ErrorKindDNSFailure,
|
|
}
|
|
}
|
|
|
|
return InternalError("%v", err)
|
|
}
|
|
|
|
fresp := &findResponse{
|
|
Sources: []string{"store"},
|
|
SuggestedCurrency: theStore.SuggestedCurrency(),
|
|
}
|
|
|
|
return sendStorePackages(route, found, fresp)
|
|
}
|
|
|
|
func findOne(c *Command, r *http.Request, user *auth.UserState, name string) Response {
|
|
if err := snap.ValidateName(name); err != nil {
|
|
return BadRequest(err.Error())
|
|
}
|
|
|
|
theStore := storeFrom(c.d)
|
|
spec := store.SnapSpec{
|
|
Name: name,
|
|
}
|
|
ctx := store.WithClientUserAgent(r.Context(), r)
|
|
snapInfo, err := theStore.SnapInfo(ctx, spec, user)
|
|
switch err {
|
|
case nil:
|
|
// pass
|
|
case store.ErrInvalidCredentials:
|
|
return Unauthorized("%v", err)
|
|
case store.ErrSnapNotFound:
|
|
return SnapNotFound(name, err)
|
|
default:
|
|
return InternalError("%v", err)
|
|
}
|
|
|
|
results := make([]*json.RawMessage, 1)
|
|
data, err := json.Marshal(webify(mapRemote(snapInfo), r.URL.String()))
|
|
if err != nil {
|
|
return InternalError(err.Error())
|
|
}
|
|
results[0] = (*json.RawMessage)(&data)
|
|
return &findResponse{
|
|
Results: results,
|
|
Sources: []string{"store"},
|
|
SuggestedCurrency: theStore.SuggestedCurrency(),
|
|
}
|
|
}
|
|
|
|
func storeUpdates(c *Command, r *http.Request, user *auth.UserState) Response {
|
|
route := c.d.router.Get(snapCmd.Path)
|
|
if route == nil {
|
|
return InternalError("cannot find route for snaps")
|
|
}
|
|
|
|
state := c.d.overlord.State()
|
|
state.Lock()
|
|
updates, err := snapstateRefreshCandidates(state, user)
|
|
state.Unlock()
|
|
if err != nil {
|
|
return InternalError("cannot list updates: %v", err)
|
|
}
|
|
|
|
return sendStorePackages(route, updates, nil)
|
|
}
|
|
|
|
func sendStorePackages(route *mux.Route, found []*snap.Info, resp *findResponse) StructuredResponse {
|
|
results := make([]*json.RawMessage, 0, len(found))
|
|
for _, x := range found {
|
|
url, err := route.URL("name", x.InstanceName())
|
|
if err != nil {
|
|
logger.Noticef("Cannot build URL for snap %q revision %s: %v", x.InstanceName(), x.Revision, err)
|
|
continue
|
|
}
|
|
|
|
data, err := json.Marshal(webify(mapRemote(x), url.String()))
|
|
if err != nil {
|
|
return InternalError("%v", err)
|
|
}
|
|
raw := json.RawMessage(data)
|
|
results = append(results, &raw)
|
|
}
|
|
|
|
if resp == nil {
|
|
resp = &findResponse{}
|
|
}
|
|
|
|
resp.Results = results
|
|
|
|
return resp
|
|
}
|
|
|
|
func mapRemote(remoteSnap *snap.Info) *client.Snap {
|
|
result, err := clientutil.ClientSnapFromSnapInfo(remoteSnap, nil)
|
|
if err != nil {
|
|
logger.Noticef("cannot get full app info for snap %q: %v", remoteSnap.SnapName(), err)
|
|
}
|
|
result.DownloadSize = remoteSnap.Size
|
|
if remoteSnap.MustBuy {
|
|
result.Status = "priced"
|
|
} else {
|
|
result.Status = "available"
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
type findResponse struct {
|
|
Results interface{}
|
|
Sources []string
|
|
SuggestedCurrency string
|
|
}
|
|
|
|
func (r *findResponse) JSON() *respJSON {
|
|
return &respJSON{
|
|
Status: 200,
|
|
Type: ResponseTypeSync,
|
|
Result: r.Results,
|
|
Sources: r.Sources,
|
|
SuggestedCurrency: r.SuggestedCurrency,
|
|
}
|
|
}
|
|
|
|
func (r *findResponse) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|
r.JSON().ServeHTTP(w, req)
|
|
}
|