gitee.com/mysnapcore/mysnapd@v0.1.0/client/snap_op.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2016-2022 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 // TransactionType says whether we want to treat each snap separately 34 // (the transaction is per snap) or whether to consider the call a 35 // single transaction so everything is reverted if it fails for just 36 // one snap. This applies to installs and updates, which can be done 37 // for multiple snaps in the same API call. 38 type TransactionType string 39 40 const ( 41 TransactionAllSnaps TransactionType = "all-snaps" 42 TransactionPerSnap TransactionType = "per-snap" 43 ) 44 45 type SnapOptions struct { 46 Channel string `json:"channel,omitempty"` 47 Revision string `json:"revision,omitempty"` 48 CohortKey string `json:"cohort-key,omitempty"` 49 LeaveCohort bool `json:"leave-cohort,omitempty"` 50 DevMode bool `json:"devmode,omitempty"` 51 JailMode bool `json:"jailmode,omitempty"` 52 Classic bool `json:"classic,omitempty"` 53 Dangerous bool `json:"dangerous,omitempty"` 54 IgnoreValidation bool `json:"ignore-validation,omitempty"` 55 IgnoreRunning bool `json:"ignore-running,omitempty"` 56 Unaliased bool `json:"unaliased,omitempty"` 57 Purge bool `json:"purge,omitempty"` 58 Amend bool `json:"amend,omitempty"` 59 Transaction TransactionType `json:"transaction,omitempty"` 60 QuotaGroupName string `json:"quota-group,omitempty"` 61 ValidationSets []string `json:"validation-sets,omitempty"` 62 Time string `json:"time,omitempty"` 63 HoldLevel string `json:"hold-level,omitempty"` 64 65 Users []string `json:"users,omitempty"` 66 } 67 68 func writeFieldBool(mw *multipart.Writer, key string, val bool) error { 69 if !val { 70 return nil 71 } 72 return mw.WriteField(key, "true") 73 } 74 75 type field struct { 76 field string 77 value bool 78 } 79 80 func writeFields(mw *multipart.Writer, fields []field) error { 81 for _, fd := range fields { 82 if err := writeFieldBool(mw, fd.field, fd.value); err != nil { 83 return err 84 } 85 } 86 87 return nil 88 } 89 90 func (opts *SnapOptions) writeModeFields(mw *multipart.Writer) error { 91 fields := []field{ 92 {"devmode", opts.DevMode}, 93 {"classic", opts.Classic}, 94 {"jailmode", opts.JailMode}, 95 {"dangerous", opts.Dangerous}, 96 } 97 return writeFields(mw, fields) 98 } 99 100 func (opts *SnapOptions) writeOptionFields(mw *multipart.Writer) error { 101 fields := []field{ 102 {"ignore-running", opts.IgnoreRunning}, 103 {"unaliased", opts.Unaliased}, 104 } 105 if opts.Transaction != "" { 106 if err := mw.WriteField("transaction", string(opts.Transaction)); err != nil { 107 return err 108 } 109 } 110 if opts.QuotaGroupName != "" { 111 if err := mw.WriteField("quota-group", opts.QuotaGroupName); err != nil { 112 return err 113 } 114 } 115 return writeFields(mw, fields) 116 } 117 118 type actionData struct { 119 Action string `json:"action"` 120 Name string `json:"name,omitempty"` 121 SnapPath string `json:"snap-path,omitempty"` 122 *SnapOptions 123 } 124 125 type multiActionData struct { 126 Action string `json:"action"` 127 Snaps []string `json:"snaps,omitempty"` 128 Users []string `json:"users,omitempty"` 129 Transaction TransactionType `json:"transaction,omitempty"` 130 IgnoreRunning bool `json:"ignore-running,omitempty"` 131 Purge bool `json:"purge,omitempty"` 132 ValidationSets []string `json:"validation-sets,omitempty"` 133 Time string `json:"time,omitempty"` 134 HoldLevel string `json:"hold-level,omitempty"` 135 } 136 137 // Install adds the snap with the given name from the given channel (or 138 // the system default channel if not). 139 func (client *Client) Install(name string, options *SnapOptions) (changeID string, err error) { 140 return client.doSnapAction("install", name, options) 141 } 142 143 func (client *Client) InstallMany(names []string, options *SnapOptions) (changeID string, err error) { 144 return client.doMultiSnapAction("install", names, options) 145 } 146 147 // Remove removes the snap with the given name. 148 func (client *Client) Remove(name string, options *SnapOptions) (changeID string, err error) { 149 return client.doSnapAction("remove", name, options) 150 } 151 152 func (client *Client) RemoveMany(names []string, options *SnapOptions) (changeID string, err error) { 153 return client.doMultiSnapAction("remove", names, options) 154 } 155 156 // Refresh refreshes the snap with the given name (switching it to track 157 // the given channel if given). 158 func (client *Client) Refresh(name string, options *SnapOptions) (changeID string, err error) { 159 return client.doSnapAction("refresh", name, options) 160 } 161 162 func (client *Client) RefreshMany(names []string, options *SnapOptions) (changeID string, err error) { 163 return client.doMultiSnapAction("refresh", names, options) 164 } 165 166 func (client *Client) HoldRefreshes(name string, options *SnapOptions) (changeID string, err error) { 167 return client.doSnapAction("hold", name, options) 168 } 169 170 func (client *Client) HoldRefreshesMany(names []string, options *SnapOptions) (changeID string, err error) { 171 return client.doMultiSnapAction("hold", names, options) 172 } 173 174 func (client *Client) UnholdRefreshes(name string, options *SnapOptions) (changeID string, err error) { 175 return client.doSnapAction("unhold", name, options) 176 } 177 178 func (client *Client) UnholdRefreshesMany(names []string, options *SnapOptions) (changeID string, err error) { 179 return client.doMultiSnapAction("unhold", names, options) 180 } 181 182 func (client *Client) Enable(name string, options *SnapOptions) (changeID string, err error) { 183 return client.doSnapAction("enable", name, options) 184 } 185 186 func (client *Client) Disable(name string, options *SnapOptions) (changeID string, err error) { 187 return client.doSnapAction("disable", name, options) 188 } 189 190 // Revert rolls the snap back to the previous on-disk state 191 func (client *Client) Revert(name string, options *SnapOptions) (changeID string, err error) { 192 return client.doSnapAction("revert", name, options) 193 } 194 195 // Switch moves the snap to a different channel without a refresh 196 func (client *Client) Switch(name string, options *SnapOptions) (changeID string, err error) { 197 return client.doSnapAction("switch", name, options) 198 } 199 200 // SnapshotMany snapshots many snaps (all, if names empty) for many users (all, if users is empty). 201 func (client *Client) SnapshotMany(names []string, users []string) (setID uint64, changeID string, err error) { 202 result, changeID, err := client.doMultiSnapActionFull("snapshot", names, &SnapOptions{Users: users}) 203 if err != nil { 204 return 0, "", err 205 } 206 if len(result) == 0 { 207 return 0, "", fmt.Errorf("server result does not contain snapshot set identifier") 208 } 209 var x struct { 210 SetID uint64 `json:"set-id"` 211 } 212 if err := json.Unmarshal(result, &x); err != nil { 213 return 0, "", err 214 } 215 return x.SetID, changeID, nil 216 } 217 218 var ErrDangerousNotApplicable = fmt.Errorf("dangerous option only meaningful when installing from a local file") 219 220 func (client *Client) doSnapAction(actionName string, snapName string, options *SnapOptions) (changeID string, err error) { 221 if options != nil && options.Dangerous { 222 return "", ErrDangerousNotApplicable 223 } 224 action := actionData{ 225 Action: actionName, 226 SnapOptions: options, 227 } 228 data, err := json.Marshal(&action) 229 if err != nil { 230 return "", fmt.Errorf("cannot marshal snap action: %s", err) 231 } 232 path := fmt.Sprintf("/v2/snaps/%s", snapName) 233 234 headers := map[string]string{ 235 "Content-Type": "application/json", 236 } 237 238 return client.doAsync("POST", path, nil, headers, bytes.NewBuffer(data)) 239 } 240 241 func (client *Client) doMultiSnapAction(actionName string, snaps []string, options *SnapOptions) (changeID string, err error) { 242 _, changeID, err = client.doMultiSnapActionFull(actionName, snaps, options) 243 244 return changeID, err 245 } 246 247 func (client *Client) doMultiSnapActionFull(actionName string, snaps []string, options *SnapOptions) (result json.RawMessage, changeID string, err error) { 248 action := multiActionData{ 249 Action: actionName, 250 Snaps: snaps, 251 } 252 if options != nil { 253 // TODO: consider returning error when options.Dangerous is set 254 action.Users = options.Users 255 action.Transaction = options.Transaction 256 action.IgnoreRunning = options.IgnoreRunning 257 action.Purge = options.Purge 258 action.ValidationSets = options.ValidationSets 259 action.Time = options.Time 260 action.HoldLevel = options.HoldLevel 261 } 262 263 data, err := json.Marshal(&action) 264 if err != nil { 265 return nil, "", fmt.Errorf("cannot marshal multi-snap action: %s", err) 266 } 267 268 headers := map[string]string{ 269 "Content-Type": "application/json", 270 } 271 272 return client.doAsyncFull("POST", "/v2/snaps", nil, headers, bytes.NewBuffer(data), nil) 273 } 274 275 // InstallPath sideloads the snap with the given path under optional provided name, 276 // returning the UUID of the background operation upon success. 277 func (client *Client) InstallPath(path, name string, options *SnapOptions) (changeID string, err error) { 278 f, err := os.Open(path) 279 if err != nil { 280 return "", fmt.Errorf("cannot open %q: %w", path, err) 281 } 282 283 action := actionData{ 284 Action: "install", 285 Name: name, 286 SnapPath: path, 287 SnapOptions: options, 288 } 289 290 return client.sendLocalSnaps([]string{path}, []*os.File{f}, action) 291 } 292 293 // InstallPathMany sideloads the snaps with the given paths, 294 // returning the UUID of the background operation upon success. 295 func (client *Client) InstallPathMany(paths []string, options *SnapOptions) (changeID string, err error) { 296 action := actionData{ 297 Action: "install", 298 SnapOptions: options, 299 } 300 301 var files []*os.File 302 for _, path := range paths { 303 f, err := os.Open(path) 304 if err != nil { 305 for _, openFile := range files { 306 openFile.Close() 307 } 308 return "", fmt.Errorf("cannot open %q: %w", path, err) 309 } 310 311 files = append(files, f) 312 } 313 314 return client.sendLocalSnaps(paths, files, action) 315 } 316 317 func (client *Client) sendLocalSnaps(paths []string, files []*os.File, action actionData) (string, error) { 318 pr, pw := io.Pipe() 319 mw := multipart.NewWriter(pw) 320 go sendSnapFiles(paths, files, pw, mw, &action) 321 322 headers := map[string]string{ 323 "Content-Type": mw.FormDataContentType(), 324 } 325 326 _, changeID, err := client.doAsyncFull("POST", "/v2/snaps", nil, headers, pr, doNoTimeoutAndRetry) 327 return changeID, err 328 } 329 330 // Try 331 func (client *Client) Try(path string, options *SnapOptions) (changeID string, err error) { 332 if options == nil { 333 options = &SnapOptions{} 334 } 335 if options.Dangerous { 336 return "", ErrDangerousNotApplicable 337 } 338 339 buf := bytes.NewBuffer(nil) 340 mw := multipart.NewWriter(buf) 341 mw.WriteField("action", "try") 342 mw.WriteField("snap-path", path) 343 options.writeModeFields(mw) 344 mw.Close() 345 346 headers := map[string]string{ 347 "Content-Type": mw.FormDataContentType(), 348 } 349 350 return client.doAsync("POST", "/v2/snaps", nil, headers, buf) 351 } 352 353 func sendSnapFiles(paths []string, files []*os.File, pw *io.PipeWriter, mw *multipart.Writer, action *actionData) { 354 defer func() { 355 for _, f := range files { 356 f.Close() 357 } 358 }() 359 360 if action.SnapOptions == nil { 361 action.SnapOptions = &SnapOptions{} 362 } 363 364 type field struct { 365 name string 366 value string 367 } 368 369 fields := []field{{"action", action.Action}} 370 if len(paths) == 1 { 371 fields = append(fields, []field{ 372 {"name", action.Name}, 373 {"snap-path", action.SnapPath}, 374 {"channel", action.Channel}}...) 375 } 376 377 for _, s := range fields { 378 if s.value == "" { 379 continue 380 } 381 if err := mw.WriteField(s.name, s.value); err != nil { 382 pw.CloseWithError(err) 383 return 384 } 385 } 386 387 if err := action.writeModeFields(mw); err != nil { 388 pw.CloseWithError(err) 389 return 390 } 391 392 if err := action.writeOptionFields(mw); err != nil { 393 pw.CloseWithError(err) 394 return 395 } 396 397 for i, file := range files { 398 path := paths[i] 399 fw, err := mw.CreateFormFile("snap", filepath.Base(path)) 400 if err != nil { 401 pw.CloseWithError(err) 402 return 403 } 404 405 _, err = io.Copy(fw, file) 406 if err != nil { 407 pw.CloseWithError(err) 408 return 409 } 410 } 411 412 mw.Close() 413 pw.Close() 414 } 415 416 type snapRevisionOptions struct { 417 Channel string `json:"channel,omitempty"` 418 Revision string `json:"revision,omitempty"` 419 CohortKey string `json:"cohort-key,omitempty"` 420 } 421 422 type downloadAction struct { 423 SnapName string `json:"snap-name"` 424 425 snapRevisionOptions 426 427 HeaderPeek bool `json:"header-peek,omitempty"` 428 ResumeToken string `json:"resume-token,omitempty"` 429 } 430 431 type DownloadInfo struct { 432 SuggestedFileName string 433 Size int64 434 Sha3_384 string 435 ResumeToken string 436 } 437 438 type DownloadOptions struct { 439 SnapOptions 440 441 HeaderPeek bool 442 ResumeToken string 443 Resume int64 444 } 445 446 // Download will stream the given snap to the client 447 func (client *Client) Download(name string, options *DownloadOptions) (dlInfo *DownloadInfo, r io.ReadCloser, err error) { 448 if options == nil { 449 options = &DownloadOptions{} 450 } 451 action := downloadAction{ 452 SnapName: name, 453 snapRevisionOptions: snapRevisionOptions{ 454 Channel: options.Channel, 455 CohortKey: options.CohortKey, 456 Revision: options.Revision, 457 }, 458 HeaderPeek: options.HeaderPeek, 459 ResumeToken: options.ResumeToken, 460 } 461 data, err := json.Marshal(&action) 462 if err != nil { 463 return nil, nil, fmt.Errorf("cannot marshal snap action: %s", err) 464 } 465 headers := map[string]string{ 466 "Content-Type": "application/json", 467 } 468 if options.Resume > 0 { 469 headers["range"] = fmt.Sprintf("bytes: %d-", options.Resume) 470 } 471 472 // no deadline for downloads 473 ctx := context.Background() 474 rsp, err := client.raw(ctx, "POST", "/v2/download", nil, headers, bytes.NewBuffer(data)) 475 if err != nil { 476 return nil, nil, err 477 } 478 479 if rsp.StatusCode != 200 { 480 var r response 481 defer rsp.Body.Close() 482 if err := decodeInto(rsp.Body, &r); err != nil { 483 return nil, nil, err 484 } 485 return nil, nil, r.err(client, rsp.StatusCode) 486 } 487 matches := contentDispositionMatcher(rsp.Header.Get("Content-Disposition")) 488 if matches == nil || matches[1] == "" { 489 return nil, nil, fmt.Errorf("cannot determine filename") 490 } 491 492 dlInfo = &DownloadInfo{ 493 SuggestedFileName: matches[1], 494 Size: rsp.ContentLength, 495 Sha3_384: rsp.Header.Get("Snap-Sha3-384"), 496 ResumeToken: rsp.Header.Get("Snap-Download-Token"), 497 } 498 499 return dlInfo, rsp.Body, nil 500 }