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  }