github.com/chipaca/snappy@v0.0.0-20210104084008-1f06296fe8ad/client/snapshot.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2018 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  	"errors"
    27  	"fmt"
    28  	"io"
    29  	"net/url"
    30  	"strconv"
    31  	"strings"
    32  	"time"
    33  
    34  	"github.com/snapcore/snapd/snap"
    35  )
    36  
    37  // SnapshotExportMediaType is the media type used to identify snapshot exports in the API.
    38  const SnapshotExportMediaType = "application/x.snapd.snapshot"
    39  
    40  var (
    41  	ErrSnapshotSetNotFound   = errors.New("no snapshot set with the given ID")
    42  	ErrSnapshotSnapsNotFound = errors.New("no snapshot for the requested snaps found in the set with the given ID")
    43  )
    44  
    45  // A snapshotAction is used to request an operation on a snapshot.
    46  type snapshotAction struct {
    47  	SetID  uint64   `json:"set"`
    48  	Action string   `json:"action"`
    49  	Snaps  []string `json:"snaps,omitempty"`
    50  	Users  []string `json:"users,omitempty"`
    51  }
    52  
    53  // A Snapshot is a collection of archives with a simple metadata json file
    54  // (and hashsums of everything).
    55  type Snapshot struct {
    56  	// SetID is the ID of the snapshot set (a snapshot set is the result of a "snap save" invocation)
    57  	SetID uint64 `json:"set"`
    58  	// the time this snapshot's data collection was started
    59  	Time time.Time `json:"time"`
    60  
    61  	// information about the snap this data is for
    62  	Snap     string        `json:"snap"`
    63  	Revision snap.Revision `json:"revision"`
    64  	SnapID   string        `json:"snap-id,omitempty"`
    65  	Epoch    snap.Epoch    `json:"epoch,omitempty"`
    66  	Summary  string        `json:"summary"`
    67  	Version  string        `json:"version"`
    68  
    69  	// the snap's configuration at snapshot time
    70  	Conf map[string]interface{} `json:"conf,omitempty"`
    71  
    72  	// the hash of the archives' data, keyed by archive path
    73  	// (either 'archive.tgz' for the system archive, or
    74  	// user/<username>.tgz for each user)
    75  	SHA3_384 map[string]string `json:"sha3-384"`
    76  	// the sum of the archive sizes
    77  	Size int64 `json:"size,omitempty"`
    78  	// if the snapshot failed to open this will be the reason why
    79  	Broken string `json:"broken,omitempty"`
    80  
    81  	// set if the snapshot was created automatically on snap removal;
    82  	// note, this is only set inside actual snapshot file for old snapshots;
    83  	// newer snapd just updates this flag on the fly for snapshots
    84  	// returned by List().
    85  	Auto bool `json:"auto,omitempty"`
    86  }
    87  
    88  // IsValid checks whether the snapshot is missing information that
    89  // should be there for a snapshot that's just been opened.
    90  func (sh *Snapshot) IsValid() bool {
    91  	return !(sh == nil || sh.SetID == 0 || sh.Snap == "" || sh.Revision.Unset() || len(sh.SHA3_384) == 0 || sh.Time.IsZero())
    92  }
    93  
    94  // A SnapshotSet is a set of snapshots created by a single "snap save".
    95  type SnapshotSet struct {
    96  	ID        uint64      `json:"id"`
    97  	Snapshots []*Snapshot `json:"snapshots"`
    98  }
    99  
   100  // Time returns the earliest time in the set.
   101  func (ss SnapshotSet) Time() time.Time {
   102  	if len(ss.Snapshots) == 0 {
   103  		return time.Time{}
   104  	}
   105  	mint := ss.Snapshots[0].Time
   106  	for _, sh := range ss.Snapshots {
   107  		if sh.Time.Before(mint) {
   108  			mint = sh.Time
   109  		}
   110  	}
   111  	return mint
   112  }
   113  
   114  // Size returns the sum of the set's sizes.
   115  func (ss SnapshotSet) Size() int64 {
   116  	var sum int64
   117  	for _, sh := range ss.Snapshots {
   118  		sum += sh.Size
   119  	}
   120  	return sum
   121  }
   122  
   123  // SnapshotSets lists the snapshot sets in the system that belong to the
   124  // given set (if non-zero) and are for the given snaps (if non-empty).
   125  func (client *Client) SnapshotSets(setID uint64, snapNames []string) ([]SnapshotSet, error) {
   126  	q := make(url.Values)
   127  	if setID > 0 {
   128  		q.Add("set", strconv.FormatUint(setID, 10))
   129  	}
   130  	if len(snapNames) > 0 {
   131  		q.Add("snaps", strings.Join(snapNames, ","))
   132  	}
   133  
   134  	var snapshotSets []SnapshotSet
   135  	_, err := client.doSync("GET", "/v2/snapshots", q, nil, nil, &snapshotSets)
   136  	return snapshotSets, err
   137  }
   138  
   139  // ForgetSnapshots permanently removes the snapshot set, limited to the
   140  // given snaps (if non-empty).
   141  func (client *Client) ForgetSnapshots(setID uint64, snaps []string) (changeID string, err error) {
   142  	return client.snapshotAction(&snapshotAction{
   143  		SetID:  setID,
   144  		Action: "forget",
   145  		Snaps:  snaps,
   146  	})
   147  }
   148  
   149  // CheckSnapshots verifies the archive checksums in the given snapshot set.
   150  //
   151  // If snaps or users are non-empty, limit to checking only those
   152  // archives of the snapshot.
   153  func (client *Client) CheckSnapshots(setID uint64, snaps []string, users []string) (changeID string, err error) {
   154  	return client.snapshotAction(&snapshotAction{
   155  		SetID:  setID,
   156  		Action: "check",
   157  		Snaps:  snaps,
   158  		Users:  users,
   159  	})
   160  }
   161  
   162  // RestoreSnapshots extracts the given snapshot set.
   163  //
   164  // If snaps or users are non-empty, limit to checking only those
   165  // archives of the snapshot.
   166  func (client *Client) RestoreSnapshots(setID uint64, snaps []string, users []string) (changeID string, err error) {
   167  	return client.snapshotAction(&snapshotAction{
   168  		SetID:  setID,
   169  		Action: "restore",
   170  		Snaps:  snaps,
   171  		Users:  users,
   172  	})
   173  }
   174  
   175  func (client *Client) snapshotAction(action *snapshotAction) (changeID string, err error) {
   176  	data, err := json.Marshal(action)
   177  	if err != nil {
   178  		return "", fmt.Errorf("cannot marshal snapshot action: %v", err)
   179  	}
   180  
   181  	headers := map[string]string{
   182  		"Content-Type": "application/json",
   183  	}
   184  
   185  	return client.doAsync("POST", "/v2/snapshots", nil, headers, bytes.NewBuffer(data))
   186  }
   187  
   188  // SnapshotExport streams the requested snapshot set.
   189  //
   190  // The return value includes the length of the returned stream.
   191  func (client *Client) SnapshotExport(setID uint64) (stream io.ReadCloser, contentLength int64, err error) {
   192  	rsp, err := client.raw(context.Background(), "GET", fmt.Sprintf("/v2/snapshots/%v/export", setID), nil, nil, nil)
   193  	if err != nil {
   194  		return nil, 0, err
   195  	}
   196  	if rsp.StatusCode != 200 {
   197  		defer rsp.Body.Close()
   198  
   199  		var r response
   200  		specificErr := r.err(client, rsp.StatusCode)
   201  		if err != nil {
   202  			return nil, 0, specificErr
   203  		}
   204  		return nil, 0, fmt.Errorf("unexpected status code: %v", rsp.Status)
   205  	}
   206  	contentType := rsp.Header.Get("Content-Type")
   207  	if contentType != SnapshotExportMediaType {
   208  		return nil, 0, fmt.Errorf("unexpected snapshot export content type %q", contentType)
   209  	}
   210  
   211  	return rsp.Body, rsp.ContentLength, nil
   212  }
   213  
   214  // SnapshotImportSet is a snapshot import created by a "snap import-snapshot".
   215  type SnapshotImportSet struct {
   216  	ID    uint64   `json:"set-id"`
   217  	Snaps []string `json:"snaps"`
   218  }
   219  
   220  // SnapshotImport imports an exported snapshot set.
   221  func (client *Client) SnapshotImport(exportStream io.Reader, size int64) (SnapshotImportSet, error) {
   222  	headers := map[string]string{
   223  		"Content-Type":   SnapshotExportMediaType,
   224  		"Content-Length": strconv.FormatInt(size, 10),
   225  	}
   226  
   227  	var importSet SnapshotImportSet
   228  	if _, err := client.doSync("POST", "/v2/snapshots", nil, headers, exportStream, &importSet); err != nil {
   229  		return importSet, err
   230  	}
   231  
   232  	return importSet, nil
   233  }