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  }