github.com/hugh712/snapd@v0.0.0-20200910133618-1a99902bd583/daemon/api_snapshots.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2018 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 "fmt" 26 "net/http" 27 "strconv" 28 "strings" 29 30 "github.com/snapcore/snapd/client" 31 "github.com/snapcore/snapd/overlord/auth" 32 "github.com/snapcore/snapd/overlord/state" 33 "github.com/snapcore/snapd/strutil" 34 ) 35 36 var snapshotCmd = &Command{ 37 // TODO: also support /v2/snapshots/<id> 38 Path: "/v2/snapshots", 39 UserOK: true, 40 PolkitOK: "io.snapcraft.snapd.manage", 41 GET: listSnapshots, 42 POST: changeSnapshots, 43 } 44 45 var snapshotExportCmd = &Command{ 46 Path: "/v2/snapshots/{id}/export", 47 GET: getSnapshotExport, 48 } 49 50 func listSnapshots(c *Command, r *http.Request, user *auth.UserState) Response { 51 query := r.URL.Query() 52 var setID uint64 53 if sid := query.Get("set"); sid != "" { 54 var err error 55 setID, err = strconv.ParseUint(sid, 10, 64) 56 if err != nil { 57 return BadRequest("'set', if given, must be a positive base 10 number; got %q", sid) 58 } 59 } 60 61 sets, err := snapshotList(context.TODO(), setID, strutil.CommaSeparatedList(r.URL.Query().Get("snaps"))) 62 if err != nil { 63 return InternalError("%v", err) 64 } 65 return SyncResponse(sets, nil) 66 } 67 68 // A snapshotAction is used to request an operation on a snapshot 69 // keep this in sync with client/snapshotAction... 70 type snapshotAction struct { 71 SetID uint64 `json:"set"` 72 Action string `json:"action"` 73 Snaps []string `json:"snaps,omitempty"` 74 Users []string `json:"users,omitempty"` 75 } 76 77 func (action snapshotAction) String() string { 78 // verb of snapshot #N [for snaps %q] [for users %q] 79 var snaps string 80 var users string 81 if len(action.Snaps) > 0 { 82 snaps = " for snaps " + strutil.Quoted(action.Snaps) 83 } 84 if len(action.Users) > 0 { 85 users = " for users " + strutil.Quoted(action.Users) 86 } 87 return fmt.Sprintf("%s of snapshot set #%d%s%s", strings.Title(action.Action), action.SetID, snaps, users) 88 } 89 90 func changeSnapshots(c *Command, r *http.Request, user *auth.UserState) Response { 91 var action snapshotAction 92 decoder := json.NewDecoder(r.Body) 93 if err := decoder.Decode(&action); err != nil { 94 return BadRequest("cannot decode request body into snapshot operation: %v", err) 95 } 96 if decoder.More() { 97 return BadRequest("extra content found after snapshot operation") 98 } 99 100 if action.SetID == 0 { 101 return BadRequest("snapshot operation requires snapshot set ID") 102 } 103 104 if action.Action == "" { 105 return BadRequest("snapshot operation requires action") 106 } 107 108 var affected []string 109 var ts *state.TaskSet 110 var err error 111 112 st := c.d.overlord.State() 113 st.Lock() 114 defer st.Unlock() 115 116 switch action.Action { 117 case "check": 118 affected, ts, err = snapshotCheck(st, action.SetID, action.Snaps, action.Users) 119 case "restore": 120 affected, ts, err = snapshotRestore(st, action.SetID, action.Snaps, action.Users) 121 case "forget": 122 if len(action.Users) != 0 { 123 return BadRequest(`snapshot "forget" operation cannot specify users`) 124 } 125 affected, ts, err = snapshotForget(st, action.SetID, action.Snaps) 126 default: 127 return BadRequest("unknown snapshot operation %q", action.Action) 128 } 129 130 switch err { 131 case nil: 132 // woo 133 case client.ErrSnapshotSetNotFound, client.ErrSnapshotSnapsNotFound: 134 return NotFound("%v", err) 135 default: 136 return InternalError("%v", err) 137 } 138 139 chg := newChange(st, action.Action+"-snapshot", action.String(), []*state.TaskSet{ts}, affected) 140 chg.Set("api-data", map[string]interface{}{"snap-names": affected}) 141 ensureStateSoon(st) 142 143 return AsyncResponse(nil, &Meta{Change: chg.ID()}) 144 } 145 146 // getSnapshotExport streams an archive containing an export of existing snapshots. 147 // 148 // The snapshots are re-packaged into a single uncompressed tar archive and 149 // internally contain multiple zip files. 150 func getSnapshotExport(c *Command, r *http.Request, user *auth.UserState) Response { 151 st := c.d.overlord.State() 152 st.Lock() 153 defer st.Unlock() 154 155 vars := muxVars(r) 156 sid := vars["id"] 157 setID, err := strconv.ParseUint(sid, 10, 64) 158 if err != nil { 159 return BadRequest("'id' must be a positive base 10 number; got %q", sid) 160 } 161 162 export, err := snapshotExport(context.TODO(), setID) 163 if err != nil { 164 return BadRequest("cannot export %v: %v", setID, err) 165 } 166 // init (size calculation) can be slow so drop the lock 167 st.Unlock() 168 err = export.Init() 169 st.Lock() 170 if err != nil { 171 return BadRequest("cannot calculate size of exported snapshot %v: %v", setID, err) 172 } 173 174 return &snapshotExportResponse{SnapshotExport: export} 175 }