github.com/chipaca/snappy@v0.0.0-20210104084008-1f06296fe8ad/daemon/api_find.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2015-2020 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package daemon
    21  
    22  import (
    23  	"encoding/json"
    24  	"net"
    25  	"net/http"
    26  	"net/url"
    27  
    28  	"github.com/gorilla/mux"
    29  
    30  	"github.com/snapcore/snapd/client"
    31  	"github.com/snapcore/snapd/client/clientutil"
    32  	"github.com/snapcore/snapd/httputil"
    33  	"github.com/snapcore/snapd/logger"
    34  	"github.com/snapcore/snapd/overlord/auth"
    35  	"github.com/snapcore/snapd/snap"
    36  	"github.com/snapcore/snapd/store"
    37  )
    38  
    39  var (
    40  	findCmd = &Command{
    41  		Path:   "/v2/find",
    42  		UserOK: true,
    43  		GET:    searchStore,
    44  	}
    45  )
    46  
    47  func searchStore(c *Command, r *http.Request, user *auth.UserState) Response {
    48  	route := c.d.router.Get(snapCmd.Path)
    49  	if route == nil {
    50  		return InternalError("cannot find route for snaps")
    51  	}
    52  	query := r.URL.Query()
    53  	q := query.Get("q")
    54  	commonID := query.Get("common-id")
    55  	// TODO: support both "category" (search v2) and "section"
    56  	section := query.Get("section")
    57  	name := query.Get("name")
    58  	scope := query.Get("scope")
    59  	private := false
    60  	prefix := false
    61  
    62  	if sel := query.Get("select"); sel != "" {
    63  		switch sel {
    64  		case "refresh":
    65  			if commonID != "" {
    66  				return BadRequest("cannot use 'common-id' with 'select=refresh'")
    67  			}
    68  			if name != "" {
    69  				return BadRequest("cannot use 'name' with 'select=refresh'")
    70  			}
    71  			if q != "" {
    72  				return BadRequest("cannot use 'q' with 'select=refresh'")
    73  			}
    74  			return storeUpdates(c, r, user)
    75  		case "private":
    76  			private = true
    77  		}
    78  	}
    79  
    80  	if name != "" {
    81  		if q != "" {
    82  			return BadRequest("cannot use 'q' and 'name' together")
    83  		}
    84  		if commonID != "" {
    85  			return BadRequest("cannot use 'common-id' and 'name' together")
    86  		}
    87  
    88  		if name[len(name)-1] != '*' {
    89  			return findOne(c, r, user, name)
    90  		}
    91  
    92  		prefix = true
    93  		q = name[:len(name)-1]
    94  	}
    95  
    96  	if commonID != "" && q != "" {
    97  		return BadRequest("cannot use 'common-id' and 'q' together")
    98  	}
    99  
   100  	theStore := getStore(c)
   101  	ctx := store.WithClientUserAgent(r.Context(), r)
   102  	found, err := theStore.Find(ctx, &store.Search{
   103  		Query:    q,
   104  		Prefix:   prefix,
   105  		CommonID: commonID,
   106  		Category: section,
   107  		Private:  private,
   108  		Scope:    scope,
   109  	}, user)
   110  	switch err {
   111  	case nil:
   112  		// pass
   113  	case store.ErrBadQuery:
   114  		return SyncResponse(&resp{
   115  			Type:   ResponseTypeError,
   116  			Result: &errorResult{Message: err.Error(), Kind: client.ErrorKindBadQuery},
   117  			Status: 400,
   118  		}, nil)
   119  	case store.ErrUnauthenticated, store.ErrInvalidCredentials:
   120  		return Unauthorized(err.Error())
   121  	default:
   122  		if e, ok := err.(*url.Error); ok {
   123  			if neterr, ok := e.Err.(*net.OpError); ok {
   124  				if dnserr, ok := neterr.Err.(*net.DNSError); ok {
   125  					return SyncResponse(&resp{
   126  						Type:   ResponseTypeError,
   127  						Result: &errorResult{Message: dnserr.Error(), Kind: client.ErrorKindDNSFailure},
   128  						Status: 400,
   129  					}, nil)
   130  				}
   131  			}
   132  		}
   133  		if e, ok := err.(net.Error); ok && e.Timeout() {
   134  			return SyncResponse(&resp{
   135  				Type:   ResponseTypeError,
   136  				Result: &errorResult{Message: err.Error(), Kind: client.ErrorKindNetworkTimeout},
   137  				Status: 400,
   138  			}, nil)
   139  		}
   140  		if e, ok := err.(*httputil.PersistentNetworkError); ok {
   141  			return SyncResponse(&resp{
   142  				Type:   ResponseTypeError,
   143  				Result: &errorResult{Message: e.Error(), Kind: client.ErrorKindDNSFailure},
   144  				Status: 400,
   145  			}, nil)
   146  		}
   147  
   148  		return InternalError("%v", err)
   149  	}
   150  
   151  	meta := &Meta{
   152  		SuggestedCurrency: theStore.SuggestedCurrency(),
   153  		Sources:           []string{"store"},
   154  	}
   155  
   156  	return sendStorePackages(route, meta, found)
   157  }
   158  
   159  func findOne(c *Command, r *http.Request, user *auth.UserState, name string) Response {
   160  	if err := snap.ValidateName(name); err != nil {
   161  		return BadRequest(err.Error())
   162  	}
   163  
   164  	theStore := getStore(c)
   165  	spec := store.SnapSpec{
   166  		Name: name,
   167  	}
   168  	ctx := store.WithClientUserAgent(r.Context(), r)
   169  	snapInfo, err := theStore.SnapInfo(ctx, spec, user)
   170  	switch err {
   171  	case nil:
   172  		// pass
   173  	case store.ErrInvalidCredentials:
   174  		return Unauthorized("%v", err)
   175  	case store.ErrSnapNotFound:
   176  		return SnapNotFound(name, err)
   177  	default:
   178  		return InternalError("%v", err)
   179  	}
   180  
   181  	meta := &Meta{
   182  		SuggestedCurrency: theStore.SuggestedCurrency(),
   183  		Sources:           []string{"store"},
   184  	}
   185  
   186  	results := make([]*json.RawMessage, 1)
   187  	data, err := json.Marshal(webify(mapRemote(snapInfo), r.URL.String()))
   188  	if err != nil {
   189  		return InternalError(err.Error())
   190  	}
   191  	results[0] = (*json.RawMessage)(&data)
   192  	return SyncResponse(results, meta)
   193  }
   194  
   195  func storeUpdates(c *Command, r *http.Request, user *auth.UserState) Response {
   196  	route := c.d.router.Get(snapCmd.Path)
   197  	if route == nil {
   198  		return InternalError("cannot find route for snaps")
   199  	}
   200  
   201  	state := c.d.overlord.State()
   202  	state.Lock()
   203  	updates, err := snapstateRefreshCandidates(state, user)
   204  	state.Unlock()
   205  	if err != nil {
   206  		return InternalError("cannot list updates: %v", err)
   207  	}
   208  
   209  	return sendStorePackages(route, nil, updates)
   210  }
   211  
   212  func sendStorePackages(route *mux.Route, meta *Meta, found []*snap.Info) Response {
   213  	results := make([]*json.RawMessage, 0, len(found))
   214  	for _, x := range found {
   215  		url, err := route.URL("name", x.InstanceName())
   216  		if err != nil {
   217  			logger.Noticef("Cannot build URL for snap %q revision %s: %v", x.InstanceName(), x.Revision, err)
   218  			continue
   219  		}
   220  
   221  		data, err := json.Marshal(webify(mapRemote(x), url.String()))
   222  		if err != nil {
   223  			return InternalError("%v", err)
   224  		}
   225  		raw := json.RawMessage(data)
   226  		results = append(results, &raw)
   227  	}
   228  
   229  	return SyncResponse(results, meta)
   230  }
   231  
   232  func mapRemote(remoteSnap *snap.Info) *client.Snap {
   233  	result, err := clientutil.ClientSnapFromSnapInfo(remoteSnap, nil)
   234  	if err != nil {
   235  		logger.Noticef("cannot get full app info for snap %q: %v", remoteSnap.SnapName(), err)
   236  	}
   237  	result.DownloadSize = remoteSnap.Size
   238  	if remoteSnap.MustBuy {
   239  		result.Status = "priced"
   240  	} else {
   241  		result.Status = "available"
   242  	}
   243  
   244  	return result
   245  }