github.com/chipaca/snappy@v0.0.0-20210104084008-1f06296fe8ad/overlord/snapshotstate/backend/backend.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2018-2020 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 backend
    21  
    22  import (
    23  	"archive/tar"
    24  	"archive/zip"
    25  	"bytes"
    26  	"context"
    27  	"crypto"
    28  	"encoding/json"
    29  	"errors"
    30  	"fmt"
    31  	"io"
    32  	"io/ioutil"
    33  	"os"
    34  	"path"
    35  	"path/filepath"
    36  	"regexp"
    37  	"runtime"
    38  	"sort"
    39  	"strconv"
    40  	"strings"
    41  	"syscall"
    42  	"time"
    43  
    44  	"github.com/snapcore/snapd/client"
    45  	"github.com/snapcore/snapd/dirs"
    46  	"github.com/snapcore/snapd/logger"
    47  	"github.com/snapcore/snapd/osutil"
    48  	"github.com/snapcore/snapd/snap"
    49  	"github.com/snapcore/snapd/snapdenv"
    50  	"github.com/snapcore/snapd/strutil"
    51  )
    52  
    53  const (
    54  	archiveName  = "archive.tgz"
    55  	metadataName = "meta.json"
    56  	metaHashName = "meta.sha3_384"
    57  
    58  	userArchivePrefix = "user/"
    59  	userArchiveSuffix = ".tgz"
    60  )
    61  
    62  var (
    63  	// Stop is used to ask Iter to stop iteration, without it being an error.
    64  	Stop = errors.New("stop iteration")
    65  
    66  	osOpen      = os.Open
    67  	dirNames    = (*os.File).Readdirnames
    68  	backendOpen = Open
    69  	timeNow     = time.Now
    70  
    71  	usersForUsernames = usersForUsernamesImpl
    72  )
    73  
    74  // LastSnapshotSetID returns the highest set id number for the snapshots stored
    75  // in snapshots directory; set ids are inferred from the filenames.
    76  func LastSnapshotSetID() (uint64, error) {
    77  	dir, err := osOpen(dirs.SnapshotsDir)
    78  	if err != nil {
    79  		if osutil.IsDirNotExist(err) {
    80  			// no snapshots
    81  			return 0, nil
    82  		}
    83  		return 0, fmt.Errorf("cannot open snapshots directory: %v", err)
    84  	}
    85  	defer dir.Close()
    86  
    87  	var maxSetID uint64
    88  
    89  	var readErr error
    90  	for readErr == nil {
    91  		var names []string
    92  		// note os.Readdirnames can return a non-empty names and a non-nil err
    93  		names, readErr = dirNames(dir, 100)
    94  		for _, name := range names {
    95  			if ok, setID := isSnapshotFilename(name); ok {
    96  				if setID > maxSetID {
    97  					maxSetID = setID
    98  				}
    99  			}
   100  		}
   101  	}
   102  	if readErr != nil && readErr != io.EOF {
   103  		return 0, readErr
   104  	}
   105  	return maxSetID, nil
   106  }
   107  
   108  // Iter loops over all snapshots in the snapshots directory, applying the given
   109  // function to each. The snapshot will be closed after the function returns. If
   110  // the function returns an error, iteration is stopped (and if the error isn't
   111  // Stop, it's returned as the error of the iterator).
   112  func Iter(ctx context.Context, f func(*Reader) error) error {
   113  	if err := ctx.Err(); err != nil {
   114  		return err
   115  	}
   116  
   117  	dir, err := osOpen(dirs.SnapshotsDir)
   118  	if err != nil {
   119  		if osutil.IsDirNotExist(err) {
   120  			// no dir -> no snapshots
   121  			return nil
   122  		}
   123  		return fmt.Errorf("cannot open snapshots directory: %v", err)
   124  	}
   125  	defer dir.Close()
   126  
   127  	importsInProgress := map[uint64]bool{}
   128  	var names []string
   129  	var readErr error
   130  	for readErr == nil && err == nil {
   131  		names, readErr = dirNames(dir, 100)
   132  		// note os.Readdirnames can return a non-empty names and a non-nil err
   133  		for _, name := range names {
   134  			if err = ctx.Err(); err != nil {
   135  				break
   136  			}
   137  
   138  			// filter out non-snapshot directory entries
   139  			ok, setID := isSnapshotFilename(name)
   140  			if !ok {
   141  				continue
   142  			}
   143  			// keep track of in-progress in a map as well
   144  			// to avoid races. E.g.:
   145  			// 1. The dirNnames() are read
   146  			// 2. 99_some-snap_1.0_x1.zip is returned
   147  			// 3. the code checks if 99_importing is there,
   148  			//    it is so 99_some-snap is skipped
   149  			// 4. other snapshots are examined
   150  			// 5. in-parallel 99_importing finishes
   151  			// 7. 99_other-snap_1.0_x1.zip is now examined
   152  			// 8. code checks if 99_importing is there, but it
   153  			//    is no longer there because import
   154  			//    finished in the meantime. We still
   155  			//    want to not call the callback with
   156  			//    99_other-snap or the callback would get
   157  			//    an incomplete view about 99_snapshot.
   158  			if importsInProgress[setID] {
   159  				continue
   160  			}
   161  			if importInProgressFor(setID) {
   162  				importsInProgress[setID] = true
   163  				continue
   164  			}
   165  
   166  			filename := filepath.Join(dirs.SnapshotsDir, name)
   167  			reader, openError := backendOpen(filename, setID)
   168  			// reader can be non-nil even when openError is not nil (in
   169  			// which case reader.Broken will have a reason). f can
   170  			// check and either ignore or return an error when
   171  			// finding a broken snapshot.
   172  			if reader != nil {
   173  				err = f(reader)
   174  			} else {
   175  				// TODO: use warnings instead
   176  				logger.Noticef("Cannot open snapshot %q: %v.", name, openError)
   177  			}
   178  			if openError == nil {
   179  				// if openError was nil the snapshot was opened and needs closing
   180  				if closeError := reader.Close(); err == nil {
   181  					err = closeError
   182  				}
   183  			}
   184  			if err != nil {
   185  				break
   186  			}
   187  		}
   188  	}
   189  
   190  	if readErr != nil && readErr != io.EOF {
   191  		return readErr
   192  	}
   193  
   194  	if err == Stop {
   195  		err = nil
   196  	}
   197  
   198  	return err
   199  }
   200  
   201  // List valid snapshots sets.
   202  func List(ctx context.Context, setID uint64, snapNames []string) ([]client.SnapshotSet, error) {
   203  	setshots := map[uint64][]*client.Snapshot{}
   204  	err := Iter(ctx, func(reader *Reader) error {
   205  		if setID == 0 || reader.SetID == setID {
   206  			if len(snapNames) == 0 || strutil.ListContains(snapNames, reader.Snap) {
   207  				setshots[reader.SetID] = append(setshots[reader.SetID], &reader.Snapshot)
   208  			}
   209  		}
   210  		return nil
   211  	})
   212  
   213  	sets := make([]client.SnapshotSet, 0, len(setshots))
   214  	for id, shots := range setshots {
   215  		sort.Sort(bySnap(shots))
   216  		sets = append(sets, client.SnapshotSet{ID: id, Snapshots: shots})
   217  	}
   218  
   219  	sort.Sort(byID(sets))
   220  
   221  	return sets, err
   222  }
   223  
   224  // Filename of the given client.Snapshot in this backend.
   225  func Filename(snapshot *client.Snapshot) string {
   226  	// this _needs_ the snap name and version to be valid
   227  	return filepath.Join(dirs.SnapshotsDir, fmt.Sprintf("%d_%s_%s_%s.zip", snapshot.SetID, snapshot.Snap, snapshot.Version, snapshot.Revision))
   228  }
   229  
   230  // isSnapshotFilename checks if the given filePath is a snapshot file name, i.e.
   231  // if it starts with a numeric set id and ends with .zip extension;
   232  // filePath can be just a file name, or a full path.
   233  func isSnapshotFilename(filePath string) (ok bool, setID uint64) {
   234  	fname := filepath.Base(filePath)
   235  	// XXX: we could use a regexp here to match very precisely all the elements
   236  	// of the filename following Filename() above, but perhaps it's better no to
   237  	// go overboard with it in case the format evolves in the future. Only check
   238  	// if the name starts with a set-id and ends with .zip.
   239  	//
   240  	// Filename is "<sid>_<snapName>_version_revision.zip", e.g. "16_snapcraft_4.2_5407.zip"
   241  	ext := filepath.Ext(fname)
   242  	if ext != ".zip" {
   243  		return false, 0
   244  	}
   245  	parts := strings.SplitN(fname, "_", 2)
   246  	if len(parts) != 2 {
   247  		return false, 0
   248  	}
   249  	// invalid: no parts following <sid>_
   250  	if parts[1] == ext {
   251  		return false, 0
   252  	}
   253  	id, err := strconv.Atoi(parts[0])
   254  	if err != nil {
   255  		return false, 0
   256  	}
   257  	return true, uint64(id)
   258  }
   259  
   260  // EstimateSnapshotSize calculates estimated size of the snapshot.
   261  func EstimateSnapshotSize(si *snap.Info, usernames []string) (uint64, error) {
   262  	var total uint64
   263  	calculateSize := func(path string, finfo os.FileInfo, err error) error {
   264  		if finfo.Mode().IsRegular() {
   265  			total += uint64(finfo.Size())
   266  		}
   267  		return err
   268  	}
   269  
   270  	visitDir := func(dir string) error {
   271  		exists, isDir, err := osutil.DirExists(dir)
   272  		if err != nil {
   273  			return err
   274  		}
   275  		if !(exists && isDir) {
   276  			return nil
   277  		}
   278  		return filepath.Walk(dir, calculateSize)
   279  	}
   280  
   281  	for _, dir := range []string{si.DataDir(), si.CommonDataDir()} {
   282  		if err := visitDir(dir); err != nil {
   283  			return 0, err
   284  		}
   285  	}
   286  
   287  	users, err := usersForUsernames(usernames)
   288  	if err != nil {
   289  		return 0, err
   290  	}
   291  	for _, usr := range users {
   292  		if err := visitDir(si.UserDataDir(usr.HomeDir)); err != nil {
   293  			return 0, err
   294  		}
   295  		if err := visitDir(si.UserCommonDataDir(usr.HomeDir)); err != nil {
   296  			return 0, err
   297  		}
   298  	}
   299  
   300  	// XXX: we could use a typical compression factor here
   301  	return total, nil
   302  }
   303  
   304  // Save a snapshot
   305  func Save(ctx context.Context, id uint64, si *snap.Info, cfg map[string]interface{}, usernames []string) (*client.Snapshot, error) {
   306  	if err := os.MkdirAll(dirs.SnapshotsDir, 0700); err != nil {
   307  		return nil, err
   308  	}
   309  
   310  	snapshot := &client.Snapshot{
   311  		SetID:    id,
   312  		Snap:     si.InstanceName(),
   313  		SnapID:   si.SnapID,
   314  		Revision: si.Revision,
   315  		Version:  si.Version,
   316  		Epoch:    si.Epoch,
   317  		Time:     timeNow(),
   318  		SHA3_384: make(map[string]string),
   319  		Size:     0,
   320  		Conf:     cfg,
   321  		// Note: Auto is no longer set in the Snapshot.
   322  	}
   323  
   324  	aw, err := osutil.NewAtomicFile(Filename(snapshot), 0600, 0, osutil.NoChown, osutil.NoChown)
   325  	if err != nil {
   326  		return nil, err
   327  	}
   328  	// if things worked, we'll commit (and Cancel becomes a NOP)
   329  	defer aw.Cancel()
   330  
   331  	w := zip.NewWriter(aw)
   332  	defer w.Close() // note this does not close the file descriptor (that's done by hand on the atomic writer, above)
   333  	if err := addDirToZip(ctx, snapshot, w, "root", archiveName, si.DataDir()); err != nil {
   334  		return nil, err
   335  	}
   336  
   337  	users, err := usersForUsernames(usernames)
   338  	if err != nil {
   339  		return nil, err
   340  	}
   341  
   342  	for _, usr := range users {
   343  		if err := addDirToZip(ctx, snapshot, w, usr.Username, userArchiveName(usr), si.UserDataDir(usr.HomeDir)); err != nil {
   344  			return nil, err
   345  		}
   346  	}
   347  
   348  	metaWriter, err := w.Create(metadataName)
   349  	if err != nil {
   350  		return nil, err
   351  	}
   352  
   353  	hasher := crypto.SHA3_384.New()
   354  	enc := json.NewEncoder(io.MultiWriter(metaWriter, hasher))
   355  	if err := enc.Encode(snapshot); err != nil {
   356  		return nil, err
   357  	}
   358  
   359  	hashWriter, err := w.Create(metaHashName)
   360  	if err != nil {
   361  		return nil, err
   362  	}
   363  	fmt.Fprintf(hashWriter, "%x\n", hasher.Sum(nil))
   364  	if err := w.Close(); err != nil {
   365  		return nil, err
   366  	}
   367  
   368  	if err := ctx.Err(); err != nil {
   369  		return nil, err
   370  	}
   371  
   372  	if err := aw.Commit(); err != nil {
   373  		return nil, err
   374  	}
   375  
   376  	return snapshot, nil
   377  }
   378  
   379  var isTesting = snapdenv.Testing()
   380  
   381  func addDirToZip(ctx context.Context, snapshot *client.Snapshot, w *zip.Writer, username string, entry, dir string) error {
   382  	parent, revdir := filepath.Split(dir)
   383  	exists, isDir, err := osutil.DirExists(parent)
   384  	if err != nil {
   385  		return err
   386  	}
   387  	if exists && !isDir {
   388  		logger.Noticef("Not saving directories under %q in snapshot #%d of %q as it is not a directory.", parent, snapshot.SetID, snapshot.Snap)
   389  		return nil
   390  	}
   391  	if !exists {
   392  		logger.Debugf("Not saving directories under %q in snapshot #%d of %q as it is does not exist.", parent, snapshot.SetID, snapshot.Snap)
   393  		return nil
   394  	}
   395  	tarArgs := []string{
   396  		"--create",
   397  		"--sparse", "--gzip",
   398  		"--format", "gnu",
   399  		"--directory", parent,
   400  	}
   401  
   402  	noRev, noCommon := true, true
   403  
   404  	exists, isDir, err = osutil.DirExists(dir)
   405  	if err != nil {
   406  		return err
   407  	}
   408  	switch {
   409  	case exists && isDir:
   410  		tarArgs = append(tarArgs, revdir)
   411  		noRev = false
   412  	case exists && !isDir:
   413  		logger.Noticef("Not saving %q in snapshot #%d of %q as it is not a directory.", dir, snapshot.SetID, snapshot.Snap)
   414  	case !exists:
   415  		logger.Debugf("Not saving %q in snapshot #%d of %q as it is does not exist.", dir, snapshot.SetID, snapshot.Snap)
   416  	}
   417  
   418  	common := filepath.Join(parent, "common")
   419  	exists, isDir, err = osutil.DirExists(common)
   420  	if err != nil {
   421  		return err
   422  	}
   423  	switch {
   424  	case exists && isDir:
   425  		tarArgs = append(tarArgs, "common")
   426  		noCommon = false
   427  	case exists && !isDir:
   428  		logger.Noticef("Not saving %q in snapshot #%d of %q as it is not a directory.", common, snapshot.SetID, snapshot.Snap)
   429  	case !exists:
   430  		logger.Debugf("Not saving %q in snapshot #%d of %q as it is does not exist.", common, snapshot.SetID, snapshot.Snap)
   431  	}
   432  
   433  	if noCommon && noRev {
   434  		return nil
   435  	}
   436  
   437  	archiveWriter, err := w.CreateHeader(&zip.FileHeader{Name: entry})
   438  	if err != nil {
   439  		return err
   440  	}
   441  
   442  	var sz osutil.Sizer
   443  	hasher := crypto.SHA3_384.New()
   444  
   445  	cmd := tarAsUser(username, tarArgs...)
   446  	cmd.Stdout = io.MultiWriter(archiveWriter, hasher, &sz)
   447  	matchCounter := &strutil.MatchCounter{N: 1}
   448  	cmd.Stderr = matchCounter
   449  	if isTesting {
   450  		matchCounter.N = -1
   451  		cmd.Stderr = io.MultiWriter(os.Stderr, matchCounter)
   452  	}
   453  	if err := osutil.RunWithContext(ctx, cmd); err != nil {
   454  		matches, count := matchCounter.Matches()
   455  		if count > 0 {
   456  			return fmt.Errorf("cannot create archive: %s (and %d more)", matches[0], count-1)
   457  		}
   458  		return fmt.Errorf("tar failed: %v", err)
   459  	}
   460  
   461  	snapshot.SHA3_384[entry] = fmt.Sprintf("%x", hasher.Sum(nil))
   462  	snapshot.Size += sz.Size()
   463  
   464  	return nil
   465  }
   466  
   467  var ErrCannotCancel = errors.New("cannot cancel: import already finished")
   468  
   469  // multiError collects multiple errors that affected an operation.
   470  type multiError struct {
   471  	header string
   472  	errs   []error
   473  }
   474  
   475  // newMultiError returns a new multiError struct initialized with
   476  // the given format string that explains what operation potentially
   477  // went wrong. multiError can be nested and will render correctly
   478  // in these cases.
   479  func newMultiError(header string, errs []error) error {
   480  	return &multiError{header: header, errs: errs}
   481  }
   482  
   483  // Error formats the error string.
   484  func (me *multiError) Error() string {
   485  	return me.nestedError(0)
   486  }
   487  
   488  // helper to ensure formating of nested multiErrors works.
   489  func (me *multiError) nestedError(level int) string {
   490  	indent := strings.Repeat(" ", level)
   491  	buf := bytes.NewBufferString(fmt.Sprintf("%s:\n", me.header))
   492  	if level > 8 {
   493  		return "circular or too deep error nesting (max 8)?!"
   494  	}
   495  	for i, err := range me.errs {
   496  		switch v := err.(type) {
   497  		case *multiError:
   498  			fmt.Fprintf(buf, "%s- %v", indent, v.nestedError(level+1))
   499  		default:
   500  			fmt.Fprintf(buf, "%s- %v", indent, err)
   501  		}
   502  		if i < len(me.errs)-1 {
   503  			fmt.Fprintf(buf, "\n")
   504  		}
   505  	}
   506  	return buf.String()
   507  }
   508  
   509  var (
   510  	importingFnRegexp = regexp.MustCompile("^([0-9]+)_importing$")
   511  	importingFnGlob   = "[0-9]*_importing"
   512  	importingFnFmt    = "%d_importing"
   513  	importingForIDFmt = "%d_*.zip"
   514  )
   515  
   516  // importInProgressFor return true if the given snapshot id has an import
   517  // that is in progress.
   518  func importInProgressFor(setID uint64) bool {
   519  	return newImportTransaction(setID).InProgress()
   520  }
   521  
   522  // importTransaction keeps track of the given snapshot ID import and
   523  // ensures it can be committed/cancelled in an atomic way.
   524  //
   525  // Start() must be called before the first data is imported. When the
   526  // import is successful Commit() should be called.
   527  //
   528  // Cancel() will cancel the given import and cleanup. It's always safe
   529  // to defer a Cancel() it will just return a "ErrCannotCancel" after
   530  // a commit.
   531  type importTransaction struct {
   532  	id        uint64
   533  	lockPath  string
   534  	committed bool
   535  }
   536  
   537  // newImportTransaction creates a new importTransaction for the given
   538  // snapshot id.
   539  func newImportTransaction(setID uint64) *importTransaction {
   540  	return &importTransaction{
   541  		id:       setID,
   542  		lockPath: filepath.Join(dirs.SnapshotsDir, fmt.Sprintf(importingFnFmt, setID)),
   543  	}
   544  }
   545  
   546  // newImportTransactionFromImportFile creates a new importTransaction
   547  // for the given import file path. It may return an error if an
   548  // invalid file was specified.
   549  func newImportTransactionFromImportFile(p string) (*importTransaction, error) {
   550  	parts := importingFnRegexp.FindStringSubmatch(path.Base(p))
   551  	if len(parts) != 2 {
   552  		return nil, fmt.Errorf("cannot determine snapshot id from %q", p)
   553  	}
   554  	setID, err := strconv.ParseUint(parts[1], 10, 64)
   555  	if err != nil {
   556  		return nil, err
   557  	}
   558  	return newImportTransaction(setID), nil
   559  }
   560  
   561  // Start marks the start of a snapshot import
   562  func (t *importTransaction) Start() error {
   563  	return t.lock()
   564  }
   565  
   566  // InProgress returns true if there is an import for this transactions
   567  // snapshot ID already.
   568  func (t *importTransaction) InProgress() bool {
   569  	return osutil.FileExists(t.lockPath)
   570  }
   571  
   572  // Cancel cancels a snapshot import and cleanups any files on disk belonging
   573  // to this snapshot ID.
   574  func (t *importTransaction) Cancel() error {
   575  	if t.committed {
   576  		return ErrCannotCancel
   577  	}
   578  	inProgressImports, err := filepath.Glob(filepath.Join(dirs.SnapshotsDir, fmt.Sprintf(importingForIDFmt, t.id)))
   579  	if err != nil {
   580  		return err
   581  	}
   582  	var errs []error
   583  	for _, p := range inProgressImports {
   584  		if err := os.Remove(p); err != nil {
   585  			errs = append(errs, err)
   586  		}
   587  	}
   588  	if err := t.unlock(); err != nil {
   589  		errs = append(errs, err)
   590  	}
   591  	if len(errs) > 0 {
   592  		return newMultiError(fmt.Sprintf("cannot cancel import for set id %d", t.id), errs)
   593  	}
   594  	return nil
   595  }
   596  
   597  // Commit will commit a given transaction
   598  func (t *importTransaction) Commit() error {
   599  	if err := t.unlock(); err != nil {
   600  		return err
   601  	}
   602  	t.committed = true
   603  	return nil
   604  }
   605  
   606  func (t *importTransaction) lock() error {
   607  	return ioutil.WriteFile(t.lockPath, nil, 0644)
   608  }
   609  
   610  func (t *importTransaction) unlock() error {
   611  	return os.Remove(t.lockPath)
   612  }
   613  
   614  var filepathGlob = filepath.Glob
   615  
   616  // CleanupAbandondedImports will clean any import that is in progress.
   617  // This is meant to be called at startup of snapd before any real imports
   618  // happen. It is not safe to run this concurrently with any other snapshot
   619  // operation.
   620  //
   621  // The amount of snapshots cleaned is returned and an error if one or
   622  // more cleanups did not succeed.
   623  func CleanupAbandondedImports() (cleaned int, err error) {
   624  	inProgressSnapshots, err := filepathGlob(filepath.Join(dirs.SnapshotsDir, importingFnGlob))
   625  	if err != nil {
   626  		return 0, err
   627  	}
   628  
   629  	var errs []error
   630  	for _, p := range inProgressSnapshots {
   631  		tr, err := newImportTransactionFromImportFile(p)
   632  		if err != nil {
   633  			errs = append(errs, err)
   634  			continue
   635  		}
   636  		if err := tr.Cancel(); err != nil {
   637  			errs = append(errs, err)
   638  		} else {
   639  			cleaned++
   640  		}
   641  	}
   642  	if len(errs) > 0 {
   643  		return cleaned, newMultiError("cannot cleanup imports", errs)
   644  	}
   645  	return cleaned, nil
   646  }
   647  
   648  // Import a snapshot from the export file format
   649  func Import(ctx context.Context, id uint64, r io.Reader) (snapNames []string, err error) {
   650  	errPrefix := fmt.Sprintf("cannot import snapshot %d", id)
   651  
   652  	tr := newImportTransaction(id)
   653  	if tr.InProgress() {
   654  		return nil, fmt.Errorf("%s: already in progress for this set id", errPrefix)
   655  	}
   656  	if err := tr.Start(); err != nil {
   657  		return nil, err
   658  	}
   659  	// Cancel once Committed is a NOP
   660  	defer tr.Cancel()
   661  
   662  	// Unpack and validate the streamed data
   663  	snapNames, err = unpackVerifySnapshotImport(r, id)
   664  	if err != nil {
   665  		return nil, fmt.Errorf("%s: %v", errPrefix, err)
   666  	}
   667  	if err := tr.Commit(); err != nil {
   668  		return nil, err
   669  	}
   670  
   671  	return snapNames, nil
   672  }
   673  
   674  func writeOneSnapshotFile(targetPath string, tr io.Reader) error {
   675  	t, err := os.OpenFile(targetPath, os.O_CREATE|os.O_RDWR, 0600)
   676  	if err != nil {
   677  		return fmt.Errorf("cannot create snapshot file %q: %v", targetPath, err)
   678  	}
   679  	defer t.Close()
   680  
   681  	if _, err := io.Copy(t, tr); err != nil {
   682  		return fmt.Errorf("cannot write snapshot file %q: %v", targetPath, err)
   683  	}
   684  	return nil
   685  }
   686  
   687  func unpackVerifySnapshotImport(r io.Reader, realSetID uint64) (snapNames []string, err error) {
   688  	var exportFound bool
   689  
   690  	tr := tar.NewReader(r)
   691  	var tarErr error
   692  	var header *tar.Header
   693  
   694  	for tarErr == nil {
   695  		header, tarErr = tr.Next()
   696  		if tarErr == io.EOF {
   697  			break
   698  		}
   699  		switch {
   700  		case tarErr != nil:
   701  			return nil, fmt.Errorf("cannot read snapshot import: %v", tarErr)
   702  		case header == nil:
   703  			// should not happen
   704  			return nil, fmt.Errorf("tar header not found")
   705  		case header.Typeflag == tar.TypeDir:
   706  			return nil, errors.New("unexpected directory in import file")
   707  		}
   708  
   709  		if header.Name == "export.json" {
   710  			// XXX: read into memory and validate once we
   711  			// hashes in export.json
   712  			exportFound = true
   713  			continue
   714  		}
   715  
   716  		// Format of the snapshot import is:
   717  		//     $setID_.....
   718  		// But because the setID is local this will not be correct
   719  		// for our system and we need to discard this setID.
   720  		//
   721  		// So chop off the incorrect (old) setID and just use
   722  		// the rest that is still valid.
   723  		l := strings.SplitN(header.Name, "_", 2)
   724  		if len(l) != 2 {
   725  			return nil, fmt.Errorf("unexpected filename in import stream: %v", header.Name)
   726  		}
   727  		targetPath := path.Join(dirs.SnapshotsDir, fmt.Sprintf("%d_%s", realSetID, l[1]))
   728  		if err := writeOneSnapshotFile(targetPath, tr); err != nil {
   729  			return snapNames, err
   730  		}
   731  
   732  		r, err := backendOpen(targetPath, realSetID)
   733  		if err != nil {
   734  			return snapNames, fmt.Errorf("cannot open snapshot: %v", err)
   735  		}
   736  		err = r.Check(context.TODO(), nil)
   737  		r.Close()
   738  		snapNames = append(snapNames, r.Snap)
   739  		if err != nil {
   740  			return snapNames, fmt.Errorf("validation failed for %q: %v", targetPath, err)
   741  		}
   742  	}
   743  
   744  	if !exportFound {
   745  		return nil, fmt.Errorf("no export.json file in uploaded data")
   746  	}
   747  	// XXX: validate using the unmarshalled export.json hashes here
   748  
   749  	return snapNames, nil
   750  }
   751  
   752  type exportMetadata struct {
   753  	Format int       `json:"format"`
   754  	Date   time.Time `json:"date"`
   755  	Files  []string  `json:"files"`
   756  }
   757  
   758  type SnapshotExport struct {
   759  	// open snapshot files
   760  	snapshotFiles []*os.File
   761  
   762  	// remember setID mostly for nicer errors
   763  	setID uint64
   764  
   765  	// cached size, needs to be calculated with CalculateSize
   766  	size int64
   767  }
   768  
   769  // NewSnapshotExport will return a SnapshotExport structure. It must be
   770  // Close()ed after use to avoid leaking file descriptors.
   771  func NewSnapshotExport(ctx context.Context, setID uint64) (se *SnapshotExport, err error) {
   772  	var snapshotFiles []*os.File
   773  
   774  	defer func() {
   775  		// cleanup any open FDs if anything goes wrong
   776  		if err != nil {
   777  			for _, f := range snapshotFiles {
   778  				f.Close()
   779  			}
   780  		}
   781  	}()
   782  
   783  	// Open all files first and keep the file descriptors
   784  	// open. The caller should have locked the state so that no
   785  	// delete/change snapshot operations can happen while the
   786  	// files are getting opened.
   787  	err = Iter(ctx, func(reader *Reader) error {
   788  		if reader.SetID == setID {
   789  			// Duplicate the file descriptor of the reader we were handed as
   790  			// Iter() closes those as soon as this unnamed returns. We
   791  			// re-package the file descriptor into snapshotFiles below.
   792  			fd, err := syscall.Dup(int(reader.Fd()))
   793  			if err != nil {
   794  				return fmt.Errorf("cannot duplicate descriptor: %v", err)
   795  			}
   796  			f := os.NewFile(uintptr(fd), reader.Name())
   797  			if f == nil {
   798  				return fmt.Errorf("cannot open file from descriptor %d", fd)
   799  			}
   800  			snapshotFiles = append(snapshotFiles, f)
   801  		}
   802  		return nil
   803  	})
   804  	if err != nil {
   805  		return nil, fmt.Errorf("cannot export snapshot %v: %v", setID, err)
   806  	}
   807  	if len(snapshotFiles) == 0 {
   808  		return nil, fmt.Errorf("no snapshot data found for %v", setID)
   809  	}
   810  
   811  	se = &SnapshotExport{snapshotFiles: snapshotFiles, setID: setID}
   812  
   813  	// ensure we never leak FDs even if the user does not call close
   814  	runtime.SetFinalizer(se, (*SnapshotExport).Close)
   815  
   816  	return se, nil
   817  }
   818  
   819  // Init will calculate the snapshot size. This can take some time
   820  // so it should be called without any locks. The SnapshotExport
   821  // keeps the FDs open so even files moved/deleted will be found.
   822  func (se *SnapshotExport) Init() error {
   823  	// Export once into a dummy writer so that we can set the size
   824  	// of the export. This is then used to set the Content-Length
   825  	// in the response correctly.
   826  	//
   827  	// Note that the size of the generated tar could change if the
   828  	// time switches between this export and the export we stream
   829  	// to the client to a time after the year 2242. This is unlikely
   830  	// but a known issue with this approach here.
   831  	var sz osutil.Sizer
   832  	if err := se.StreamTo(&sz); err != nil {
   833  		return fmt.Errorf("cannot calculcate the size for %v: %s", se.setID, err)
   834  	}
   835  	se.size = sz.Size()
   836  	return nil
   837  }
   838  
   839  func (se *SnapshotExport) Size() int64 {
   840  	return se.size
   841  }
   842  
   843  func (se *SnapshotExport) Close() {
   844  	for _, f := range se.snapshotFiles {
   845  		f.Close()
   846  	}
   847  	se.snapshotFiles = nil
   848  }
   849  
   850  func (se *SnapshotExport) StreamTo(w io.Writer) error {
   851  	// write out a tar
   852  	var files []string
   853  	tw := tar.NewWriter(w)
   854  	defer tw.Close()
   855  	for _, snapshotFile := range se.snapshotFiles {
   856  		stat, err := snapshotFile.Stat()
   857  		if err != nil {
   858  			return err
   859  		}
   860  		if !stat.Mode().IsRegular() {
   861  			// should never happen
   862  			return fmt.Errorf("unexported special file %q in snapshot: %s", stat.Name(), stat.Mode())
   863  		}
   864  		if _, err := snapshotFile.Seek(0, 0); err != nil {
   865  			return fmt.Errorf("cannot seek on %v: %v", stat.Name(), err)
   866  		}
   867  		hdr, err := tar.FileInfoHeader(stat, "")
   868  		if err != nil {
   869  			return fmt.Errorf("symlink: %v", stat.Name())
   870  		}
   871  		if err = tw.WriteHeader(hdr); err != nil {
   872  			return fmt.Errorf("cannot write header for %v: %v", stat.Name(), err)
   873  		}
   874  		if _, err := io.Copy(tw, snapshotFile); err != nil {
   875  			return fmt.Errorf("cannot write data for %v: %v", stat.Name(), err)
   876  		}
   877  
   878  		files = append(files, path.Base(snapshotFile.Name()))
   879  	}
   880  
   881  	// write the metadata last, then the client can use that to
   882  	// validate the archive is complete
   883  	meta := exportMetadata{
   884  		Format: 1,
   885  		Date:   timeNow(),
   886  		Files:  files,
   887  	}
   888  	metaDataBuf, err := json.Marshal(&meta)
   889  	if err != nil {
   890  		return fmt.Errorf("cannot marshal meta-data: %v", err)
   891  	}
   892  	hdr := &tar.Header{
   893  		Typeflag: tar.TypeReg,
   894  		Name:     "export.json",
   895  		Size:     int64(len(metaDataBuf)),
   896  		Mode:     0640,
   897  		ModTime:  timeNow(),
   898  	}
   899  	if err := tw.WriteHeader(hdr); err != nil {
   900  		return err
   901  	}
   902  	if _, err := tw.Write(metaDataBuf); err != nil {
   903  		return err
   904  	}
   905  
   906  	return nil
   907  }