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