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  }