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