github.com/bugraaydogar/snapd@v0.0.0-20210315170335-8c70bb858939/daemon/api_snaps.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 "context" 24 "encoding/json" 25 "errors" 26 "fmt" 27 "mime" 28 "net/http" 29 "strings" 30 31 "github.com/snapcore/snapd/client" 32 "github.com/snapcore/snapd/i18n" 33 "github.com/snapcore/snapd/logger" 34 "github.com/snapcore/snapd/overlord/auth" 35 "github.com/snapcore/snapd/overlord/servicestate" 36 "github.com/snapcore/snapd/overlord/snapstate" 37 "github.com/snapcore/snapd/overlord/state" 38 "github.com/snapcore/snapd/progress" 39 "github.com/snapcore/snapd/sandbox" 40 "github.com/snapcore/snapd/snap" 41 "github.com/snapcore/snapd/snap/channel" 42 "github.com/snapcore/snapd/strutil" 43 ) 44 45 var ( 46 // see daemon.go:canAccess for details how the access is controlled 47 snapCmd = &Command{ 48 Path: "/v2/snaps/{name}", 49 UserOK: true, 50 PolkitOK: "io.snapcraft.snapd.manage", 51 GET: getSnapInfo, 52 POST: postSnap, 53 } 54 55 snapsCmd = &Command{ 56 Path: "/v2/snaps", 57 UserOK: true, 58 PolkitOK: "io.snapcraft.snapd.manage", 59 GET: getSnapsInfo, 60 POST: postSnaps, 61 } 62 ) 63 64 func getSnapInfo(c *Command, r *http.Request, user *auth.UserState) Response { 65 vars := muxVars(r) 66 name := vars["name"] 67 68 about, err := localSnapInfo(c.d.overlord.State(), name) 69 if err != nil { 70 if err == errNoSnap { 71 return SnapNotFound(name, err) 72 } 73 74 return InternalError("%v", err) 75 } 76 77 route := c.d.router.Get(c.Path) 78 if route == nil { 79 return InternalError("cannot find route for %q snap", name) 80 } 81 82 url, err := route.URL("name", name) 83 if err != nil { 84 return InternalError("cannot build URL for %q snap: %v", name, err) 85 } 86 87 sd := servicestate.NewStatusDecorator(progress.Null) 88 89 result := webify(mapLocal(about, sd), url.String()) 90 91 return SyncResponse(result, nil) 92 } 93 94 func webify(result *client.Snap, resource string) *client.Snap { 95 if result.Icon == "" || strings.HasPrefix(result.Icon, "http") { 96 return result 97 } 98 result.Icon = "" 99 100 route := appIconCmd.d.router.Get(appIconCmd.Path) 101 if route != nil { 102 url, err := route.URL("name", result.Name) 103 if err == nil { 104 result.Icon = url.String() 105 } 106 } 107 108 return result 109 } 110 111 func postSnap(c *Command, r *http.Request, user *auth.UserState) Response { 112 route := c.d.router.Get(stateChangeCmd.Path) 113 if route == nil { 114 return InternalError("cannot find route for change") 115 } 116 117 decoder := json.NewDecoder(r.Body) 118 var inst snapInstruction 119 if err := decoder.Decode(&inst); err != nil { 120 return BadRequest("cannot decode request body into snap instruction: %v", err) 121 } 122 inst.ctx = r.Context() 123 124 state := c.d.overlord.State() 125 state.Lock() 126 defer state.Unlock() 127 128 if user != nil { 129 inst.userID = user.ID 130 } 131 132 vars := muxVars(r) 133 inst.Snaps = []string{vars["name"]} 134 135 if err := inst.validate(); err != nil { 136 return BadRequest("%s", err) 137 } 138 139 impl := inst.dispatch() 140 if impl == nil { 141 return BadRequest("unknown action %s", inst.Action) 142 } 143 144 msg, tsets, err := impl(&inst, state) 145 if err != nil { 146 return inst.errToResponse(err) 147 } 148 149 chg := newChange(state, inst.Action+"-snap", msg, tsets, inst.Snaps) 150 151 ensureStateSoon(state) 152 153 return AsyncResponse(nil, &Meta{Change: chg.ID()}) 154 } 155 156 type snapRevisionOptions struct { 157 Channel string `json:"channel"` 158 Revision snap.Revision `json:"revision"` 159 160 CohortKey string `json:"cohort-key"` 161 LeaveCohort bool `json:"leave-cohort"` 162 } 163 164 func (ropt *snapRevisionOptions) validate() error { 165 if ropt.CohortKey != "" { 166 if ropt.LeaveCohort { 167 return fmt.Errorf("cannot specify both cohort-key and leave-cohort") 168 } 169 if !ropt.Revision.Unset() { 170 return fmt.Errorf("cannot specify both cohort-key and revision") 171 } 172 } 173 174 if ropt.Channel != "" { 175 _, err := channel.Parse(ropt.Channel, "-") 176 if err != nil { 177 return err 178 } 179 } 180 return nil 181 } 182 183 type snapInstruction struct { 184 progress.NullMeter 185 186 Action string `json:"action"` 187 Amend bool `json:"amend"` 188 snapRevisionOptions 189 DevMode bool `json:"devmode"` 190 JailMode bool `json:"jailmode"` 191 Classic bool `json:"classic"` 192 IgnoreValidation bool `json:"ignore-validation"` 193 IgnoreRunning bool `json:"ignore-running"` 194 Unaliased bool `json:"unaliased"` 195 Purge bool `json:"purge,omitempty"` 196 Snaps []string `json:"snaps"` 197 Users []string `json:"users"` 198 199 // The fields below should not be unmarshalled into. Do not export them. 200 userID int 201 ctx context.Context 202 } 203 204 func (inst *snapInstruction) revnoOpts() *snapstate.RevisionOptions { 205 return &snapstate.RevisionOptions{ 206 Channel: inst.Channel, 207 Revision: inst.Revision, 208 CohortKey: inst.CohortKey, 209 LeaveCohort: inst.LeaveCohort, 210 } 211 } 212 213 func (inst *snapInstruction) modeFlags() (snapstate.Flags, error) { 214 return modeFlags(inst.DevMode, inst.JailMode, inst.Classic) 215 } 216 217 func (inst *snapInstruction) installFlags() (snapstate.Flags, error) { 218 flags, err := inst.modeFlags() 219 if err != nil { 220 return snapstate.Flags{}, err 221 } 222 if inst.Unaliased { 223 flags.Unaliased = true 224 } 225 if inst.IgnoreRunning { 226 flags.IgnoreRunning = true 227 } 228 229 return flags, nil 230 } 231 232 func (inst *snapInstruction) validate() error { 233 if inst.CohortKey != "" { 234 if inst.Action != "install" && inst.Action != "refresh" && inst.Action != "switch" { 235 return fmt.Errorf("cohort-key can only be specified for install, refresh, or switch") 236 } 237 } 238 if inst.LeaveCohort { 239 if inst.Action != "refresh" && inst.Action != "switch" { 240 return fmt.Errorf("leave-cohort can only be specified for refresh or switch") 241 } 242 } 243 if inst.Action == "install" { 244 for _, snapName := range inst.Snaps { 245 // FIXME: alternatively we could simply mutate *inst 246 // and s/ubuntu-core/core/ ? 247 if snapName == "ubuntu-core" { 248 return fmt.Errorf(`cannot install "ubuntu-core", please use "core" instead`) 249 } 250 } 251 } 252 253 return inst.snapRevisionOptions.validate() 254 } 255 256 type snapInstructionResult struct { 257 Summary string 258 Affected []string 259 Tasksets []*state.TaskSet 260 Result map[string]interface{} 261 } 262 263 var errDevJailModeConflict = errors.New("cannot use devmode and jailmode flags together") 264 var errClassicDevmodeConflict = errors.New("cannot use classic and devmode flags together") 265 var errNoJailMode = errors.New("this system cannot honour the jailmode flag") 266 267 func modeFlags(devMode, jailMode, classic bool) (snapstate.Flags, error) { 268 flags := snapstate.Flags{} 269 devModeOS := sandbox.ForceDevMode() 270 switch { 271 case jailMode && devModeOS: 272 return flags, errNoJailMode 273 case jailMode && devMode: 274 return flags, errDevJailModeConflict 275 case devMode && classic: 276 return flags, errClassicDevmodeConflict 277 } 278 // NOTE: jailmode and classic are allowed together. In that setting, 279 // jailmode overrides classic and the app gets regular (non-classic) 280 // confinement. 281 flags.JailMode = jailMode 282 flags.Classic = classic 283 flags.DevMode = devMode 284 return flags, nil 285 } 286 287 func snapInstall(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) { 288 if len(inst.Snaps[0]) == 0 { 289 return "", nil, fmt.Errorf(i18n.G("cannot install snap with empty name")) 290 } 291 292 flags, err := inst.installFlags() 293 if err != nil { 294 return "", nil, err 295 } 296 297 var ckey string 298 if inst.CohortKey == "" { 299 logger.Noticef("Installing snap %q revision %s", inst.Snaps[0], inst.Revision) 300 } else { 301 ckey = strutil.ElliptLeft(inst.CohortKey, 10) 302 logger.Noticef("Installing snap %q from cohort %q", inst.Snaps[0], ckey) 303 } 304 tset, err := snapstateInstall(inst.ctx, st, inst.Snaps[0], inst.revnoOpts(), inst.userID, flags) 305 if err != nil { 306 return "", nil, err 307 } 308 309 msg := fmt.Sprintf(i18n.G("Install %q snap"), inst.Snaps[0]) 310 if inst.Channel != "stable" && inst.Channel != "" { 311 msg += fmt.Sprintf(" from %q channel", inst.Channel) 312 } 313 if inst.CohortKey != "" { 314 msg += fmt.Sprintf(" from %q cohort", ckey) 315 } 316 return msg, []*state.TaskSet{tset}, nil 317 } 318 319 func snapUpdate(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) { 320 // TODO: bail if revision is given (and != current?), *or* behave as with install --revision? 321 flags, err := inst.modeFlags() 322 if err != nil { 323 return "", nil, err 324 } 325 if inst.IgnoreValidation { 326 flags.IgnoreValidation = true 327 } 328 if inst.IgnoreRunning { 329 flags.IgnoreRunning = true 330 } 331 if inst.Amend { 332 flags.Amend = true 333 } 334 335 // we need refreshed snap-declarations to enforce refresh-control as best as we can 336 if err = assertstateRefreshSnapDeclarations(st, inst.userID); err != nil { 337 return "", nil, err 338 } 339 340 ts, err := snapstateUpdate(st, inst.Snaps[0], inst.revnoOpts(), inst.userID, flags) 341 if err != nil { 342 return "", nil, err 343 } 344 345 msg := fmt.Sprintf(i18n.G("Refresh %q snap"), inst.Snaps[0]) 346 if inst.Channel != "stable" && inst.Channel != "" { 347 msg = fmt.Sprintf(i18n.G("Refresh %q snap from %q channel"), inst.Snaps[0], inst.Channel) 348 } 349 350 return msg, []*state.TaskSet{ts}, nil 351 } 352 353 func snapRemove(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) { 354 ts, err := snapstate.Remove(st, inst.Snaps[0], inst.Revision, &snapstate.RemoveFlags{Purge: inst.Purge}) 355 if err != nil { 356 return "", nil, err 357 } 358 359 msg := fmt.Sprintf(i18n.G("Remove %q snap"), inst.Snaps[0]) 360 return msg, []*state.TaskSet{ts}, nil 361 } 362 363 func snapRevert(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) { 364 var ts *state.TaskSet 365 366 flags, err := inst.modeFlags() 367 if err != nil { 368 return "", nil, err 369 } 370 371 if inst.Revision.Unset() { 372 ts, err = snapstateRevert(st, inst.Snaps[0], flags) 373 } else { 374 ts, err = snapstateRevertToRevision(st, inst.Snaps[0], inst.Revision, flags) 375 } 376 if err != nil { 377 return "", nil, err 378 } 379 380 msg := fmt.Sprintf(i18n.G("Revert %q snap"), inst.Snaps[0]) 381 return msg, []*state.TaskSet{ts}, nil 382 } 383 384 func snapEnable(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) { 385 if !inst.Revision.Unset() { 386 return "", nil, errors.New("enable takes no revision") 387 } 388 ts, err := snapstate.Enable(st, inst.Snaps[0]) 389 if err != nil { 390 return "", nil, err 391 } 392 393 msg := fmt.Sprintf(i18n.G("Enable %q snap"), inst.Snaps[0]) 394 return msg, []*state.TaskSet{ts}, nil 395 } 396 397 func snapDisable(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) { 398 if !inst.Revision.Unset() { 399 return "", nil, errors.New("disable takes no revision") 400 } 401 ts, err := snapstate.Disable(st, inst.Snaps[0]) 402 if err != nil { 403 return "", nil, err 404 } 405 406 msg := fmt.Sprintf(i18n.G("Disable %q snap"), inst.Snaps[0]) 407 return msg, []*state.TaskSet{ts}, nil 408 } 409 410 func snapSwitch(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) { 411 if !inst.Revision.Unset() { 412 return "", nil, errors.New("switch takes no revision") 413 } 414 ts, err := snapstateSwitch(st, inst.Snaps[0], inst.revnoOpts()) 415 if err != nil { 416 return "", nil, err 417 } 418 419 var msg string 420 switch { 421 case inst.LeaveCohort && inst.Channel != "": 422 msg = fmt.Sprintf(i18n.G("Switch %q snap to channel %q and away from cohort"), inst.Snaps[0], inst.Channel) 423 case inst.LeaveCohort: 424 msg = fmt.Sprintf(i18n.G("Switch %q snap away from cohort"), inst.Snaps[0]) 425 case inst.CohortKey == "" && inst.Channel != "": 426 msg = fmt.Sprintf(i18n.G("Switch %q snap to channel %q"), inst.Snaps[0], inst.Channel) 427 case inst.CohortKey != "" && inst.Channel == "": 428 msg = fmt.Sprintf(i18n.G("Switch %q snap to cohort %q"), inst.Snaps[0], strutil.ElliptLeft(inst.CohortKey, 10)) 429 default: 430 msg = fmt.Sprintf(i18n.G("Switch %q snap to channel %q and cohort %q"), inst.Snaps[0], inst.Channel, strutil.ElliptLeft(inst.CohortKey, 10)) 431 } 432 return msg, []*state.TaskSet{ts}, nil 433 } 434 435 type snapActionFunc func(*snapInstruction, *state.State) (string, []*state.TaskSet, error) 436 437 var snapInstructionDispTable = map[string]snapActionFunc{ 438 "install": snapInstall, 439 "refresh": snapUpdate, 440 "remove": snapRemove, 441 "revert": snapRevert, 442 "enable": snapEnable, 443 "disable": snapDisable, 444 "switch": snapSwitch, 445 } 446 447 func (inst *snapInstruction) dispatch() snapActionFunc { 448 if len(inst.Snaps) != 1 { 449 logger.Panicf("dispatch only handles single-snap ops; got %d", len(inst.Snaps)) 450 } 451 return snapInstructionDispTable[inst.Action] 452 } 453 454 func (inst *snapInstruction) errToResponse(err error) Response { 455 if len(inst.Snaps) == 0 { 456 return errToResponse(err, nil, BadRequest, "cannot %s: %v", inst.Action) 457 } 458 459 return errToResponse(err, inst.Snaps, BadRequest, "cannot %s %s: %v", inst.Action, strutil.Quoted(inst.Snaps)) 460 } 461 462 func postSnaps(c *Command, r *http.Request, user *auth.UserState) Response { 463 contentType := r.Header.Get("Content-Type") 464 465 mediaType, params, err := mime.ParseMediaType(contentType) 466 if err != nil { 467 return BadRequest("cannot parse content type: %v", err) 468 } 469 470 if mediaType == "application/json" { 471 charset := strings.ToUpper(params["charset"]) 472 if charset != "" && charset != "UTF-8" { 473 return BadRequest("unknown charset in content type: %s", contentType) 474 } 475 return snapOpMany(c, r, user) 476 } 477 478 if !strings.HasPrefix(contentType, "multipart/") { 479 return BadRequest("unknown content type: %s", contentType) 480 } 481 482 return sideloadOrTrySnap(c, r.Body, params["boundary"], user) 483 } 484 485 func snapOpMany(c *Command, r *http.Request, user *auth.UserState) Response { 486 route := c.d.router.Get(stateChangeCmd.Path) 487 if route == nil { 488 return InternalError("cannot find route for change") 489 } 490 491 decoder := json.NewDecoder(r.Body) 492 var inst snapInstruction 493 if err := decoder.Decode(&inst); err != nil { 494 return BadRequest("cannot decode request body into snap instruction: %v", err) 495 } 496 497 // TODO: inst.Amend, etc? 498 if inst.Channel != "" || !inst.Revision.Unset() || inst.DevMode || inst.JailMode || inst.CohortKey != "" || inst.LeaveCohort || inst.Purge { 499 return BadRequest("unsupported option provided for multi-snap operation") 500 } 501 if err := inst.validate(); err != nil { 502 return BadRequest("%v", err) 503 } 504 505 st := c.d.overlord.State() 506 st.Lock() 507 defer st.Unlock() 508 509 if user != nil { 510 inst.userID = user.ID 511 } 512 513 op := inst.dispatchForMany() 514 if op == nil { 515 return BadRequest("unsupported multi-snap operation %q", inst.Action) 516 } 517 res, err := op(&inst, st) 518 if err != nil { 519 return inst.errToResponse(err) 520 } 521 522 var chg *state.Change 523 if len(res.Tasksets) == 0 { 524 chg = st.NewChange(inst.Action+"-snap", res.Summary) 525 chg.SetStatus(state.DoneStatus) 526 } else { 527 chg = newChange(st, inst.Action+"-snap", res.Summary, res.Tasksets, res.Affected) 528 ensureStateSoon(st) 529 } 530 531 chg.Set("api-data", map[string]interface{}{"snap-names": res.Affected}) 532 533 return AsyncResponse(res.Result, &Meta{Change: chg.ID()}) 534 } 535 536 type snapManyActionFunc func(*snapInstruction, *state.State) (*snapInstructionResult, error) 537 538 func (inst *snapInstruction) dispatchForMany() (op snapManyActionFunc) { 539 switch inst.Action { 540 case "refresh": 541 op = snapUpdateMany 542 case "install": 543 op = snapInstallMany 544 case "remove": 545 op = snapRemoveMany 546 case "snapshot": 547 // see api_snapshots.go 548 op = snapshotMany 549 } 550 return op 551 } 552 553 func snapInstallMany(inst *snapInstruction, st *state.State) (*snapInstructionResult, error) { 554 for _, name := range inst.Snaps { 555 if len(name) == 0 { 556 return nil, fmt.Errorf(i18n.G("cannot install snap with empty name")) 557 } 558 } 559 installed, tasksets, err := snapstateInstallMany(st, inst.Snaps, inst.userID) 560 if err != nil { 561 return nil, err 562 } 563 564 var msg string 565 switch len(inst.Snaps) { 566 case 0: 567 return nil, fmt.Errorf("cannot install zero snaps") 568 case 1: 569 msg = fmt.Sprintf(i18n.G("Install snap %q"), inst.Snaps[0]) 570 default: 571 quoted := strutil.Quoted(inst.Snaps) 572 // TRANSLATORS: the %s is a comma-separated list of quoted snap names 573 msg = fmt.Sprintf(i18n.G("Install snaps %s"), quoted) 574 } 575 576 return &snapInstructionResult{ 577 Summary: msg, 578 Affected: installed, 579 Tasksets: tasksets, 580 }, nil 581 } 582 583 func snapUpdateMany(inst *snapInstruction, st *state.State) (*snapInstructionResult, error) { 584 // we need refreshed snap-declarations to enforce refresh-control as best as we can, this also ensures that snap-declarations and their prerequisite assertions are updated regularly 585 if err := assertstateRefreshSnapDeclarations(st, inst.userID); err != nil { 586 return nil, err 587 } 588 589 // TODO: use a per-request context 590 updated, tasksets, err := snapstateUpdateMany(context.TODO(), st, inst.Snaps, inst.userID, nil) 591 if err != nil { 592 return nil, err 593 } 594 595 var msg string 596 switch len(updated) { 597 case 0: 598 if len(inst.Snaps) != 0 { 599 // TRANSLATORS: the %s is a comma-separated list of quoted snap names 600 msg = fmt.Sprintf(i18n.G("Refresh snaps %s: no updates"), strutil.Quoted(inst.Snaps)) 601 } else { 602 msg = i18n.G("Refresh all snaps: no updates") 603 } 604 case 1: 605 msg = fmt.Sprintf(i18n.G("Refresh snap %q"), updated[0]) 606 default: 607 quoted := strutil.Quoted(updated) 608 // TRANSLATORS: the %s is a comma-separated list of quoted snap names 609 msg = fmt.Sprintf(i18n.G("Refresh snaps %s"), quoted) 610 } 611 612 return &snapInstructionResult{ 613 Summary: msg, 614 Affected: updated, 615 Tasksets: tasksets, 616 }, nil 617 } 618 619 func snapRemoveMany(inst *snapInstruction, st *state.State) (*snapInstructionResult, error) { 620 removed, tasksets, err := snapstateRemoveMany(st, inst.Snaps) 621 if err != nil { 622 return nil, err 623 } 624 625 var msg string 626 switch len(inst.Snaps) { 627 case 0: 628 return nil, fmt.Errorf("cannot remove zero snaps") 629 case 1: 630 msg = fmt.Sprintf(i18n.G("Remove snap %q"), inst.Snaps[0]) 631 default: 632 quoted := strutil.Quoted(inst.Snaps) 633 // TRANSLATORS: the %s is a comma-separated list of quoted snap names 634 msg = fmt.Sprintf(i18n.G("Remove snaps %s"), quoted) 635 } 636 637 return &snapInstructionResult{ 638 Summary: msg, 639 Affected: removed, 640 Tasksets: tasksets, 641 }, nil 642 } 643 644 // query many snaps 645 func getSnapsInfo(c *Command, r *http.Request, user *auth.UserState) Response { 646 647 if shouldSearchStore(r) { 648 logger.Noticef("Jumping to \"find\" to better support legacy request %q", r.URL) 649 return searchStore(c, r, user) 650 } 651 652 route := c.d.router.Get(snapCmd.Path) 653 if route == nil { 654 return InternalError("cannot find route for snaps") 655 } 656 657 query := r.URL.Query() 658 var all bool 659 sel := query.Get("select") 660 switch sel { 661 case "all": 662 all = true 663 case "enabled", "": 664 all = false 665 default: 666 return BadRequest("invalid select parameter: %q", sel) 667 } 668 var wanted map[string]bool 669 if ns := query.Get("snaps"); len(ns) > 0 { 670 nsl := strutil.CommaSeparatedList(ns) 671 wanted = make(map[string]bool, len(nsl)) 672 for _, name := range nsl { 673 wanted[name] = true 674 } 675 } 676 677 found, err := allLocalSnapInfos(c.d.overlord.State(), all, wanted) 678 if err != nil { 679 return InternalError("cannot list local snaps! %v", err) 680 } 681 682 results := make([]*json.RawMessage, len(found)) 683 684 sd := servicestate.NewStatusDecorator(progress.Null) 685 for i, x := range found { 686 name := x.info.InstanceName() 687 rev := x.info.Revision 688 689 url, err := route.URL("name", name) 690 if err != nil { 691 logger.Noticef("Cannot build URL for snap %q revision %s: %v", name, rev, err) 692 continue 693 } 694 695 data, err := json.Marshal(webify(mapLocal(x, sd), url.String())) 696 if err != nil { 697 return InternalError("cannot serialize snap %q revision %s: %v", name, rev, err) 698 } 699 raw := json.RawMessage(data) 700 results[i] = &raw 701 } 702 703 return SyncResponse(results, &Meta{Sources: []string{"local"}}) 704 } 705 706 func shouldSearchStore(r *http.Request) bool { 707 // we should jump to the old behaviour iff q is given, or if 708 // sources is given and either empty or contains the word 709 // 'store'. Otherwise, local results only. 710 711 query := r.URL.Query() 712 713 if _, ok := query["q"]; ok { 714 logger.Debugf("use of obsolete \"q\" parameter: %q", r.URL) 715 return true 716 } 717 718 if src, ok := query["sources"]; ok { 719 logger.Debugf("use of obsolete \"sources\" parameter: %q", r.URL) 720 if len(src) == 0 || strings.Contains(src[0], "store") { 721 return true 722 } 723 } 724 725 return false 726 }