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 }