github.com/kubiko/snapd@v0.0.0-20201013125620-d4f3094d9ddf/client/snap_op.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2016-2017 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 client 21 22 import ( 23 "bytes" 24 "context" 25 "encoding/json" 26 "fmt" 27 "io" 28 "mime/multipart" 29 "os" 30 "path/filepath" 31 ) 32 33 type SnapOptions struct { 34 Channel string `json:"channel,omitempty"` 35 Revision string `json:"revision,omitempty"` 36 CohortKey string `json:"cohort-key,omitempty"` 37 LeaveCohort bool `json:"leave-cohort,omitempty"` 38 DevMode bool `json:"devmode,omitempty"` 39 JailMode bool `json:"jailmode,omitempty"` 40 Classic bool `json:"classic,omitempty"` 41 Dangerous bool `json:"dangerous,omitempty"` 42 IgnoreValidation bool `json:"ignore-validation,omitempty"` 43 Unaliased bool `json:"unaliased,omitempty"` 44 Purge bool `json:"purge,omitempty"` 45 Amend bool `json:"amend,omitempty"` 46 47 Users []string `json:"users,omitempty"` 48 } 49 50 func writeFieldBool(mw *multipart.Writer, key string, val bool) error { 51 if !val { 52 return nil 53 } 54 return mw.WriteField(key, "true") 55 } 56 57 func (opts *SnapOptions) writeModeFields(mw *multipart.Writer) error { 58 fields := []struct { 59 f string 60 b bool 61 }{ 62 {"devmode", opts.DevMode}, 63 {"classic", opts.Classic}, 64 {"jailmode", opts.JailMode}, 65 {"dangerous", opts.Dangerous}, 66 } 67 for _, o := range fields { 68 if err := writeFieldBool(mw, o.f, o.b); err != nil { 69 return err 70 } 71 } 72 73 return nil 74 } 75 76 func (opts *SnapOptions) writeOptionFields(mw *multipart.Writer) error { 77 return writeFieldBool(mw, "unaliased", opts.Unaliased) 78 } 79 80 type actionData struct { 81 Action string `json:"action"` 82 Name string `json:"name,omitempty"` 83 SnapPath string `json:"snap-path,omitempty"` 84 *SnapOptions 85 } 86 87 type multiActionData struct { 88 Action string `json:"action"` 89 Snaps []string `json:"snaps,omitempty"` 90 Users []string `json:"users,omitempty"` 91 } 92 93 // Install adds the snap with the given name from the given channel (or 94 // the system default channel if not). 95 func (client *Client) Install(name string, options *SnapOptions) (changeID string, err error) { 96 return client.doSnapAction("install", name, options) 97 } 98 99 func (client *Client) InstallMany(names []string, options *SnapOptions) (changeID string, err error) { 100 return client.doMultiSnapAction("install", names, options) 101 } 102 103 // Remove removes the snap with the given name. 104 func (client *Client) Remove(name string, options *SnapOptions) (changeID string, err error) { 105 return client.doSnapAction("remove", name, options) 106 } 107 108 func (client *Client) RemoveMany(names []string, options *SnapOptions) (changeID string, err error) { 109 return client.doMultiSnapAction("remove", names, options) 110 } 111 112 // Refresh refreshes the snap with the given name (switching it to track 113 // the given channel if given). 114 func (client *Client) Refresh(name string, options *SnapOptions) (changeID string, err error) { 115 return client.doSnapAction("refresh", name, options) 116 } 117 118 func (client *Client) RefreshMany(names []string, options *SnapOptions) (changeID string, err error) { 119 return client.doMultiSnapAction("refresh", names, options) 120 } 121 122 func (client *Client) Enable(name string, options *SnapOptions) (changeID string, err error) { 123 return client.doSnapAction("enable", name, options) 124 } 125 126 func (client *Client) Disable(name string, options *SnapOptions) (changeID string, err error) { 127 return client.doSnapAction("disable", name, options) 128 } 129 130 // Revert rolls the snap back to the previous on-disk state 131 func (client *Client) Revert(name string, options *SnapOptions) (changeID string, err error) { 132 return client.doSnapAction("revert", name, options) 133 } 134 135 // Switch moves the snap to a different channel without a refresh 136 func (client *Client) Switch(name string, options *SnapOptions) (changeID string, err error) { 137 return client.doSnapAction("switch", name, options) 138 } 139 140 // SnapshotMany snapshots many snaps (all, if names empty) for many users (all, if users is empty). 141 func (client *Client) SnapshotMany(names []string, users []string) (setID uint64, changeID string, err error) { 142 result, changeID, err := client.doMultiSnapActionFull("snapshot", names, &SnapOptions{Users: users}) 143 if err != nil { 144 return 0, "", err 145 } 146 if len(result) == 0 { 147 return 0, "", fmt.Errorf("server result does not contain snapshot set identifier") 148 } 149 var x struct { 150 SetID uint64 `json:"set-id"` 151 } 152 if err := json.Unmarshal(result, &x); err != nil { 153 return 0, "", err 154 } 155 return x.SetID, changeID, nil 156 } 157 158 var ErrDangerousNotApplicable = fmt.Errorf("dangerous option only meaningful when installing from a local file") 159 160 func (client *Client) doSnapAction(actionName string, snapName string, options *SnapOptions) (changeID string, err error) { 161 if options != nil && options.Dangerous { 162 return "", ErrDangerousNotApplicable 163 } 164 action := actionData{ 165 Action: actionName, 166 SnapOptions: options, 167 } 168 data, err := json.Marshal(&action) 169 if err != nil { 170 return "", fmt.Errorf("cannot marshal snap action: %s", err) 171 } 172 path := fmt.Sprintf("/v2/snaps/%s", snapName) 173 174 headers := map[string]string{ 175 "Content-Type": "application/json", 176 } 177 178 return client.doAsync("POST", path, nil, headers, bytes.NewBuffer(data)) 179 } 180 181 func (client *Client) doMultiSnapAction(actionName string, snaps []string, options *SnapOptions) (changeID string, err error) { 182 if options != nil { 183 return "", fmt.Errorf("cannot use options for multi-action") // (yet) 184 } 185 _, changeID, err = client.doMultiSnapActionFull(actionName, snaps, options) 186 187 return changeID, err 188 } 189 190 func (client *Client) doMultiSnapActionFull(actionName string, snaps []string, options *SnapOptions) (result json.RawMessage, changeID string, err error) { 191 action := multiActionData{ 192 Action: actionName, 193 Snaps: snaps, 194 } 195 if options != nil { 196 action.Users = options.Users 197 } 198 data, err := json.Marshal(&action) 199 if err != nil { 200 return nil, "", fmt.Errorf("cannot marshal multi-snap action: %s", err) 201 } 202 203 headers := map[string]string{ 204 "Content-Type": "application/json", 205 } 206 207 return client.doAsyncFull("POST", "/v2/snaps", nil, headers, bytes.NewBuffer(data), nil) 208 } 209 210 // InstallPath sideloads the snap with the given path under optional provided name, 211 // returning the UUID of the background operation upon success. 212 func (client *Client) InstallPath(path, name string, options *SnapOptions) (changeID string, err error) { 213 f, err := os.Open(path) 214 if err != nil { 215 return "", fmt.Errorf("cannot open: %q", path) 216 } 217 218 action := actionData{ 219 Action: "install", 220 Name: name, 221 SnapPath: path, 222 SnapOptions: options, 223 } 224 225 pr, pw := io.Pipe() 226 mw := multipart.NewWriter(pw) 227 go sendSnapFile(path, f, pw, mw, &action) 228 229 headers := map[string]string{ 230 "Content-Type": mw.FormDataContentType(), 231 } 232 233 _, changeID, err = client.doAsyncFull("POST", "/v2/snaps", nil, headers, pr, doNoTimeoutAndRetry) 234 return changeID, err 235 } 236 237 // Try 238 func (client *Client) Try(path string, options *SnapOptions) (changeID string, err error) { 239 if options == nil { 240 options = &SnapOptions{} 241 } 242 if options.Dangerous { 243 return "", ErrDangerousNotApplicable 244 } 245 246 buf := bytes.NewBuffer(nil) 247 mw := multipart.NewWriter(buf) 248 mw.WriteField("action", "try") 249 mw.WriteField("snap-path", path) 250 options.writeModeFields(mw) 251 mw.Close() 252 253 headers := map[string]string{ 254 "Content-Type": mw.FormDataContentType(), 255 } 256 257 return client.doAsync("POST", "/v2/snaps", nil, headers, buf) 258 } 259 260 func sendSnapFile(snapPath string, snapFile *os.File, pw *io.PipeWriter, mw *multipart.Writer, action *actionData) { 261 defer snapFile.Close() 262 263 if action.SnapOptions == nil { 264 action.SnapOptions = &SnapOptions{} 265 } 266 fields := []struct { 267 name string 268 value string 269 }{ 270 {"action", action.Action}, 271 {"name", action.Name}, 272 {"snap-path", action.SnapPath}, 273 {"channel", action.Channel}, 274 } 275 for _, s := range fields { 276 if s.value == "" { 277 continue 278 } 279 if err := mw.WriteField(s.name, s.value); err != nil { 280 pw.CloseWithError(err) 281 return 282 } 283 } 284 285 if err := action.writeModeFields(mw); err != nil { 286 pw.CloseWithError(err) 287 return 288 } 289 290 if err := action.writeOptionFields(mw); err != nil { 291 pw.CloseWithError(err) 292 return 293 } 294 295 fw, err := mw.CreateFormFile("snap", filepath.Base(snapPath)) 296 if err != nil { 297 pw.CloseWithError(err) 298 return 299 } 300 301 _, err = io.Copy(fw, snapFile) 302 if err != nil { 303 pw.CloseWithError(err) 304 return 305 } 306 307 mw.Close() 308 pw.Close() 309 } 310 311 type snapRevisionOptions struct { 312 Channel string `json:"channel,omitempty"` 313 Revision string `json:"revision,omitempty"` 314 CohortKey string `json:"cohort-key,omitempty"` 315 } 316 317 type downloadAction struct { 318 SnapName string `json:"snap-name"` 319 320 snapRevisionOptions 321 322 HeaderPeek bool `json:"header-peek,omitempty"` 323 ResumeToken string `json:"resume-token,omitempty"` 324 } 325 326 type DownloadInfo struct { 327 SuggestedFileName string 328 Size int64 329 Sha3_384 string 330 ResumeToken string 331 } 332 333 type DownloadOptions struct { 334 SnapOptions 335 336 HeaderPeek bool 337 ResumeToken string 338 Resume int64 339 } 340 341 // Download will stream the given snap to the client 342 func (client *Client) Download(name string, options *DownloadOptions) (dlInfo *DownloadInfo, r io.ReadCloser, err error) { 343 if options == nil { 344 options = &DownloadOptions{} 345 } 346 action := downloadAction{ 347 SnapName: name, 348 snapRevisionOptions: snapRevisionOptions{ 349 Channel: options.Channel, 350 CohortKey: options.CohortKey, 351 Revision: options.Revision, 352 }, 353 HeaderPeek: options.HeaderPeek, 354 ResumeToken: options.ResumeToken, 355 } 356 data, err := json.Marshal(&action) 357 if err != nil { 358 return nil, nil, fmt.Errorf("cannot marshal snap action: %s", err) 359 } 360 headers := map[string]string{ 361 "Content-Type": "application/json", 362 } 363 if options.Resume > 0 { 364 headers["range"] = fmt.Sprintf("bytes: %d-", options.Resume) 365 } 366 367 // no deadline for downloads 368 ctx := context.Background() 369 rsp, err := client.raw(ctx, "POST", "/v2/download", nil, headers, bytes.NewBuffer(data)) 370 if err != nil { 371 return nil, nil, err 372 } 373 374 if rsp.StatusCode != 200 { 375 var r response 376 defer rsp.Body.Close() 377 if err := decodeInto(rsp.Body, &r); err != nil { 378 return nil, nil, err 379 } 380 return nil, nil, r.err(client, rsp.StatusCode) 381 } 382 matches := contentDispositionMatcher(rsp.Header.Get("Content-Disposition")) 383 if matches == nil || matches[1] == "" { 384 return nil, nil, fmt.Errorf("cannot determine filename") 385 } 386 387 dlInfo = &DownloadInfo{ 388 SuggestedFileName: matches[1], 389 Size: rsp.ContentLength, 390 Sha3_384: rsp.Header.Get("Snap-Sha3-384"), 391 ResumeToken: rsp.Header.Get("Snap-Download-Token"), 392 } 393 394 return dlInfo, rsp.Body, nil 395 }