github.com/lalkh/containerd@v1.4.3/content/local/store.go (about)

     1  /*
     2     Copyright The containerd Authors.
     3  
     4     Licensed under the Apache License, Version 2.0 (the "License");
     5     you may not use this file except in compliance with the License.
     6     You may obtain a copy of the License at
     7  
     8         http://www.apache.org/licenses/LICENSE-2.0
     9  
    10     Unless required by applicable law or agreed to in writing, software
    11     distributed under the License is distributed on an "AS IS" BASIS,
    12     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13     See the License for the specific language governing permissions and
    14     limitations under the License.
    15  */
    16  
    17  package local
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"io"
    23  	"io/ioutil"
    24  	"math/rand"
    25  	"os"
    26  	"path/filepath"
    27  	"strconv"
    28  	"strings"
    29  	"sync"
    30  	"time"
    31  
    32  	"github.com/containerd/containerd/content"
    33  	"github.com/containerd/containerd/errdefs"
    34  	"github.com/containerd/containerd/filters"
    35  	"github.com/containerd/containerd/log"
    36  	"github.com/sirupsen/logrus"
    37  
    38  	digest "github.com/opencontainers/go-digest"
    39  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    40  	"github.com/pkg/errors"
    41  )
    42  
    43  var bufPool = sync.Pool{
    44  	New: func() interface{} {
    45  		buffer := make([]byte, 1<<20)
    46  		return &buffer
    47  	},
    48  }
    49  
    50  // LabelStore is used to store mutable labels for digests
    51  type LabelStore interface {
    52  	// Get returns all the labels for the given digest
    53  	Get(digest.Digest) (map[string]string, error)
    54  
    55  	// Set sets all the labels for a given digest
    56  	Set(digest.Digest, map[string]string) error
    57  
    58  	// Update replaces the given labels for a digest,
    59  	// a key with an empty value removes a label.
    60  	Update(digest.Digest, map[string]string) (map[string]string, error)
    61  }
    62  
    63  // Store is digest-keyed store for content. All data written into the store is
    64  // stored under a verifiable digest.
    65  //
    66  // Store can generally support multi-reader, single-writer ingest of data,
    67  // including resumable ingest.
    68  type store struct {
    69  	root string
    70  	ls   LabelStore
    71  }
    72  
    73  // NewStore returns a local content store
    74  func NewStore(root string) (content.Store, error) {
    75  	return NewLabeledStore(root, nil)
    76  }
    77  
    78  // NewLabeledStore returns a new content store using the provided label store
    79  //
    80  // Note: content stores which are used underneath a metadata store may not
    81  // require labels and should use `NewStore`. `NewLabeledStore` is primarily
    82  // useful for tests or standalone implementations.
    83  func NewLabeledStore(root string, ls LabelStore) (content.Store, error) {
    84  	if err := os.MkdirAll(filepath.Join(root, "ingest"), 0777); err != nil {
    85  		return nil, err
    86  	}
    87  
    88  	return &store{
    89  		root: root,
    90  		ls:   ls,
    91  	}, nil
    92  }
    93  
    94  func (s *store) Info(ctx context.Context, dgst digest.Digest) (content.Info, error) {
    95  	p, err := s.blobPath(dgst)
    96  	if err != nil {
    97  		return content.Info{}, errors.Wrapf(err, "calculating blob info path")
    98  	}
    99  
   100  	fi, err := os.Stat(p)
   101  	if err != nil {
   102  		if os.IsNotExist(err) {
   103  			err = errors.Wrapf(errdefs.ErrNotFound, "content %v", dgst)
   104  		}
   105  
   106  		return content.Info{}, err
   107  	}
   108  	var labels map[string]string
   109  	if s.ls != nil {
   110  		labels, err = s.ls.Get(dgst)
   111  		if err != nil {
   112  			return content.Info{}, err
   113  		}
   114  	}
   115  	return s.info(dgst, fi, labels), nil
   116  }
   117  
   118  func (s *store) info(dgst digest.Digest, fi os.FileInfo, labels map[string]string) content.Info {
   119  	return content.Info{
   120  		Digest:    dgst,
   121  		Size:      fi.Size(),
   122  		CreatedAt: fi.ModTime(),
   123  		UpdatedAt: getATime(fi),
   124  		Labels:    labels,
   125  	}
   126  }
   127  
   128  // ReaderAt returns an io.ReaderAt for the blob.
   129  func (s *store) ReaderAt(ctx context.Context, desc ocispec.Descriptor) (content.ReaderAt, error) {
   130  	p, err := s.blobPath(desc.Digest)
   131  	if err != nil {
   132  		return nil, errors.Wrapf(err, "calculating blob path for ReaderAt")
   133  	}
   134  	fi, err := os.Stat(p)
   135  	if err != nil {
   136  		if !os.IsNotExist(err) {
   137  			return nil, err
   138  		}
   139  
   140  		return nil, errors.Wrapf(errdefs.ErrNotFound, "blob %s expected at %s", desc.Digest, p)
   141  	}
   142  
   143  	fp, err := os.Open(p)
   144  	if err != nil {
   145  		if !os.IsNotExist(err) {
   146  			return nil, err
   147  		}
   148  
   149  		return nil, errors.Wrapf(errdefs.ErrNotFound, "blob %s expected at %s", desc.Digest, p)
   150  	}
   151  
   152  	return sizeReaderAt{size: fi.Size(), fp: fp}, nil
   153  }
   154  
   155  // Delete removes a blob by its digest.
   156  //
   157  // While this is safe to do concurrently, safe exist-removal logic must hold
   158  // some global lock on the store.
   159  func (s *store) Delete(ctx context.Context, dgst digest.Digest) error {
   160  	bp, err := s.blobPath(dgst)
   161  	if err != nil {
   162  		return errors.Wrapf(err, "calculating blob path for delete")
   163  	}
   164  
   165  	if err := os.RemoveAll(bp); err != nil {
   166  		if !os.IsNotExist(err) {
   167  			return err
   168  		}
   169  
   170  		return errors.Wrapf(errdefs.ErrNotFound, "content %v", dgst)
   171  	}
   172  
   173  	return nil
   174  }
   175  
   176  func (s *store) Update(ctx context.Context, info content.Info, fieldpaths ...string) (content.Info, error) {
   177  	if s.ls == nil {
   178  		return content.Info{}, errors.Wrapf(errdefs.ErrFailedPrecondition, "update not supported on immutable content store")
   179  	}
   180  
   181  	p, err := s.blobPath(info.Digest)
   182  	if err != nil {
   183  		return content.Info{}, errors.Wrapf(err, "calculating blob path for update")
   184  	}
   185  
   186  	fi, err := os.Stat(p)
   187  	if err != nil {
   188  		if os.IsNotExist(err) {
   189  			err = errors.Wrapf(errdefs.ErrNotFound, "content %v", info.Digest)
   190  		}
   191  
   192  		return content.Info{}, err
   193  	}
   194  
   195  	var (
   196  		all    bool
   197  		labels map[string]string
   198  	)
   199  	if len(fieldpaths) > 0 {
   200  		for _, path := range fieldpaths {
   201  			if strings.HasPrefix(path, "labels.") {
   202  				if labels == nil {
   203  					labels = map[string]string{}
   204  				}
   205  
   206  				key := strings.TrimPrefix(path, "labels.")
   207  				labels[key] = info.Labels[key]
   208  				continue
   209  			}
   210  
   211  			switch path {
   212  			case "labels":
   213  				all = true
   214  				labels = info.Labels
   215  			default:
   216  				return content.Info{}, errors.Wrapf(errdefs.ErrInvalidArgument, "cannot update %q field on content info %q", path, info.Digest)
   217  			}
   218  		}
   219  	} else {
   220  		all = true
   221  		labels = info.Labels
   222  	}
   223  
   224  	if all {
   225  		err = s.ls.Set(info.Digest, labels)
   226  	} else {
   227  		labels, err = s.ls.Update(info.Digest, labels)
   228  	}
   229  	if err != nil {
   230  		return content.Info{}, err
   231  	}
   232  
   233  	info = s.info(info.Digest, fi, labels)
   234  	info.UpdatedAt = time.Now()
   235  
   236  	if err := os.Chtimes(p, info.UpdatedAt, info.CreatedAt); err != nil {
   237  		log.G(ctx).WithError(err).Warnf("could not change access time for %s", info.Digest)
   238  	}
   239  
   240  	return info, nil
   241  }
   242  
   243  func (s *store) Walk(ctx context.Context, fn content.WalkFunc, filters ...string) error {
   244  	// TODO: Support filters
   245  	root := filepath.Join(s.root, "blobs")
   246  	var alg digest.Algorithm
   247  	return filepath.Walk(root, func(path string, fi os.FileInfo, err error) error {
   248  		if err != nil {
   249  			return err
   250  		}
   251  		if !fi.IsDir() && !alg.Available() {
   252  			return nil
   253  		}
   254  
   255  		// TODO(stevvooe): There are few more cases with subdirs that should be
   256  		// handled in case the layout gets corrupted. This isn't strict enough
   257  		// and may spew bad data.
   258  
   259  		if path == root {
   260  			return nil
   261  		}
   262  		if filepath.Dir(path) == root {
   263  			alg = digest.Algorithm(filepath.Base(path))
   264  
   265  			if !alg.Available() {
   266  				alg = ""
   267  				return filepath.SkipDir
   268  			}
   269  
   270  			// descending into a hash directory
   271  			return nil
   272  		}
   273  
   274  		dgst := digest.NewDigestFromHex(alg.String(), filepath.Base(path))
   275  		if err := dgst.Validate(); err != nil {
   276  			// log error but don't report
   277  			log.L.WithError(err).WithField("path", path).Error("invalid digest for blob path")
   278  			// if we see this, it could mean some sort of corruption of the
   279  			// store or extra paths not expected previously.
   280  		}
   281  
   282  		var labels map[string]string
   283  		if s.ls != nil {
   284  			labels, err = s.ls.Get(dgst)
   285  			if err != nil {
   286  				return err
   287  			}
   288  		}
   289  		return fn(s.info(dgst, fi, labels))
   290  	})
   291  }
   292  
   293  func (s *store) Status(ctx context.Context, ref string) (content.Status, error) {
   294  	return s.status(s.ingestRoot(ref))
   295  }
   296  
   297  func (s *store) ListStatuses(ctx context.Context, fs ...string) ([]content.Status, error) {
   298  	fp, err := os.Open(filepath.Join(s.root, "ingest"))
   299  	if err != nil {
   300  		return nil, err
   301  	}
   302  
   303  	defer fp.Close()
   304  
   305  	fis, err := fp.Readdir(-1)
   306  	if err != nil {
   307  		return nil, err
   308  	}
   309  
   310  	filter, err := filters.ParseAll(fs...)
   311  	if err != nil {
   312  		return nil, err
   313  	}
   314  
   315  	var active []content.Status
   316  	for _, fi := range fis {
   317  		p := filepath.Join(s.root, "ingest", fi.Name())
   318  		stat, err := s.status(p)
   319  		if err != nil {
   320  			if !os.IsNotExist(err) {
   321  				return nil, err
   322  			}
   323  
   324  			// TODO(stevvooe): This is a common error if uploads are being
   325  			// completed while making this listing. Need to consider taking a
   326  			// lock on the whole store to coordinate this aspect.
   327  			//
   328  			// Another option is to cleanup downloads asynchronously and
   329  			// coordinate this method with the cleanup process.
   330  			//
   331  			// For now, we just skip them, as they really don't exist.
   332  			continue
   333  		}
   334  
   335  		if filter.Match(adaptStatus(stat)) {
   336  			active = append(active, stat)
   337  		}
   338  	}
   339  
   340  	return active, nil
   341  }
   342  
   343  // WalkStatusRefs is used to walk all status references
   344  // Failed status reads will be logged and ignored, if
   345  // this function is called while references are being altered,
   346  // these error messages may be produced.
   347  func (s *store) WalkStatusRefs(ctx context.Context, fn func(string) error) error {
   348  	fp, err := os.Open(filepath.Join(s.root, "ingest"))
   349  	if err != nil {
   350  		return err
   351  	}
   352  
   353  	defer fp.Close()
   354  
   355  	fis, err := fp.Readdir(-1)
   356  	if err != nil {
   357  		return err
   358  	}
   359  
   360  	for _, fi := range fis {
   361  		rf := filepath.Join(s.root, "ingest", fi.Name(), "ref")
   362  
   363  		ref, err := readFileString(rf)
   364  		if err != nil {
   365  			log.G(ctx).WithError(err).WithField("path", rf).Error("failed to read ingest ref")
   366  			continue
   367  		}
   368  
   369  		if err := fn(ref); err != nil {
   370  			return err
   371  		}
   372  	}
   373  
   374  	return nil
   375  }
   376  
   377  // status works like stat above except uses the path to the ingest.
   378  func (s *store) status(ingestPath string) (content.Status, error) {
   379  	dp := filepath.Join(ingestPath, "data")
   380  	fi, err := os.Stat(dp)
   381  	if err != nil {
   382  		if os.IsNotExist(err) {
   383  			err = errors.Wrap(errdefs.ErrNotFound, err.Error())
   384  		}
   385  		return content.Status{}, err
   386  	}
   387  
   388  	ref, err := readFileString(filepath.Join(ingestPath, "ref"))
   389  	if err != nil {
   390  		if os.IsNotExist(err) {
   391  			err = errors.Wrap(errdefs.ErrNotFound, err.Error())
   392  		}
   393  		return content.Status{}, err
   394  	}
   395  
   396  	startedAt, err := readFileTimestamp(filepath.Join(ingestPath, "startedat"))
   397  	if err != nil {
   398  		return content.Status{}, errors.Wrapf(err, "could not read startedat")
   399  	}
   400  
   401  	updatedAt, err := readFileTimestamp(filepath.Join(ingestPath, "updatedat"))
   402  	if err != nil {
   403  		return content.Status{}, errors.Wrapf(err, "could not read updatedat")
   404  	}
   405  
   406  	// because we don't write updatedat on every write, the mod time may
   407  	// actually be more up to date.
   408  	if fi.ModTime().After(updatedAt) {
   409  		updatedAt = fi.ModTime()
   410  	}
   411  
   412  	return content.Status{
   413  		Ref:       ref,
   414  		Offset:    fi.Size(),
   415  		Total:     s.total(ingestPath),
   416  		UpdatedAt: updatedAt,
   417  		StartedAt: startedAt,
   418  	}, nil
   419  }
   420  
   421  func adaptStatus(status content.Status) filters.Adaptor {
   422  	return filters.AdapterFunc(func(fieldpath []string) (string, bool) {
   423  		if len(fieldpath) == 0 {
   424  			return "", false
   425  		}
   426  		switch fieldpath[0] {
   427  		case "ref":
   428  			return status.Ref, true
   429  		}
   430  
   431  		return "", false
   432  	})
   433  }
   434  
   435  // total attempts to resolve the total expected size for the write.
   436  func (s *store) total(ingestPath string) int64 {
   437  	totalS, err := readFileString(filepath.Join(ingestPath, "total"))
   438  	if err != nil {
   439  		return 0
   440  	}
   441  
   442  	total, err := strconv.ParseInt(totalS, 10, 64)
   443  	if err != nil {
   444  		// represents a corrupted file, should probably remove.
   445  		return 0
   446  	}
   447  
   448  	return total
   449  }
   450  
   451  // Writer begins or resumes the active writer identified by ref. If the writer
   452  // is already in use, an error is returned. Only one writer may be in use per
   453  // ref at a time.
   454  //
   455  // The argument `ref` is used to uniquely identify a long-lived writer transaction.
   456  func (s *store) Writer(ctx context.Context, opts ...content.WriterOpt) (content.Writer, error) {
   457  	var wOpts content.WriterOpts
   458  	for _, opt := range opts {
   459  		if err := opt(&wOpts); err != nil {
   460  			return nil, err
   461  		}
   462  	}
   463  	// TODO(AkihiroSuda): we could create a random string or one calculated based on the context
   464  	// https://github.com/containerd/containerd/issues/2129#issuecomment-380255019
   465  	if wOpts.Ref == "" {
   466  		return nil, errors.Wrap(errdefs.ErrInvalidArgument, "ref must not be empty")
   467  	}
   468  	var lockErr error
   469  	for count := uint64(0); count < 10; count++ {
   470  		time.Sleep(time.Millisecond * time.Duration(rand.Intn(1<<count)))
   471  		if err := tryLock(wOpts.Ref); err != nil {
   472  			if !errdefs.IsUnavailable(err) {
   473  				return nil, err
   474  			}
   475  
   476  			lockErr = err
   477  		} else {
   478  			lockErr = nil
   479  			break
   480  		}
   481  	}
   482  
   483  	if lockErr != nil {
   484  		return nil, lockErr
   485  	}
   486  
   487  	w, err := s.writer(ctx, wOpts.Ref, wOpts.Desc.Size, wOpts.Desc.Digest)
   488  	if err != nil {
   489  		unlock(wOpts.Ref)
   490  		return nil, err
   491  	}
   492  
   493  	return w, nil // lock is now held by w.
   494  }
   495  
   496  func (s *store) resumeStatus(ref string, total int64, digester digest.Digester) (content.Status, error) {
   497  	path, _, data := s.ingestPaths(ref)
   498  	status, err := s.status(path)
   499  	if err != nil {
   500  		return status, errors.Wrap(err, "failed reading status of resume write")
   501  	}
   502  	if ref != status.Ref {
   503  		// NOTE(stevvooe): This is fairly catastrophic. Either we have some
   504  		// layout corruption or a hash collision for the ref key.
   505  		return status, errors.Wrapf(err, "ref key does not match: %v != %v", ref, status.Ref)
   506  	}
   507  
   508  	if total > 0 && status.Total > 0 && total != status.Total {
   509  		return status, errors.Errorf("provided total differs from status: %v != %v", total, status.Total)
   510  	}
   511  
   512  	// TODO(stevvooe): slow slow slow!!, send to goroutine or use resumable hashes
   513  	fp, err := os.Open(data)
   514  	if err != nil {
   515  		return status, err
   516  	}
   517  
   518  	p := bufPool.Get().(*[]byte)
   519  	status.Offset, err = io.CopyBuffer(digester.Hash(), fp, *p)
   520  	bufPool.Put(p)
   521  	fp.Close()
   522  	return status, err
   523  }
   524  
   525  // writer provides the main implementation of the Writer method. The caller
   526  // must hold the lock correctly and release on error if there is a problem.
   527  func (s *store) writer(ctx context.Context, ref string, total int64, expected digest.Digest) (content.Writer, error) {
   528  	// TODO(stevvooe): Need to actually store expected here. We have
   529  	// code in the service that shouldn't be dealing with this.
   530  	if expected != "" {
   531  		p, err := s.blobPath(expected)
   532  		if err != nil {
   533  			return nil, errors.Wrap(err, "calculating expected blob path for writer")
   534  		}
   535  		if _, err := os.Stat(p); err == nil {
   536  			return nil, errors.Wrapf(errdefs.ErrAlreadyExists, "content %v", expected)
   537  		}
   538  	}
   539  
   540  	path, refp, data := s.ingestPaths(ref)
   541  
   542  	var (
   543  		digester  = digest.Canonical.Digester()
   544  		offset    int64
   545  		startedAt time.Time
   546  		updatedAt time.Time
   547  	)
   548  
   549  	foundValidIngest := false
   550  	// ensure that the ingest path has been created.
   551  	if err := os.Mkdir(path, 0755); err != nil {
   552  		if !os.IsExist(err) {
   553  			return nil, err
   554  		}
   555  		status, err := s.resumeStatus(ref, total, digester)
   556  		if err == nil {
   557  			foundValidIngest = true
   558  			updatedAt = status.UpdatedAt
   559  			startedAt = status.StartedAt
   560  			total = status.Total
   561  			offset = status.Offset
   562  		} else {
   563  			logrus.Infof("failed to resume the status from path %s: %s. will recreate them", path, err.Error())
   564  		}
   565  	}
   566  
   567  	if !foundValidIngest {
   568  		startedAt = time.Now()
   569  		updatedAt = startedAt
   570  
   571  		// the ingest is new, we need to setup the target location.
   572  		// write the ref to a file for later use
   573  		if err := ioutil.WriteFile(refp, []byte(ref), 0666); err != nil {
   574  			return nil, err
   575  		}
   576  
   577  		if err := writeTimestampFile(filepath.Join(path, "startedat"), startedAt); err != nil {
   578  			return nil, err
   579  		}
   580  
   581  		if err := writeTimestampFile(filepath.Join(path, "updatedat"), startedAt); err != nil {
   582  			return nil, err
   583  		}
   584  
   585  		if total > 0 {
   586  			if err := ioutil.WriteFile(filepath.Join(path, "total"), []byte(fmt.Sprint(total)), 0666); err != nil {
   587  				return nil, err
   588  			}
   589  		}
   590  	}
   591  
   592  	fp, err := os.OpenFile(data, os.O_WRONLY|os.O_CREATE, 0666)
   593  	if err != nil {
   594  		return nil, errors.Wrap(err, "failed to open data file")
   595  	}
   596  
   597  	if _, err := fp.Seek(offset, io.SeekStart); err != nil {
   598  		return nil, errors.Wrap(err, "could not seek to current write offset")
   599  	}
   600  
   601  	return &writer{
   602  		s:         s,
   603  		fp:        fp,
   604  		ref:       ref,
   605  		path:      path,
   606  		offset:    offset,
   607  		total:     total,
   608  		digester:  digester,
   609  		startedAt: startedAt,
   610  		updatedAt: updatedAt,
   611  	}, nil
   612  }
   613  
   614  // Abort an active transaction keyed by ref. If the ingest is active, it will
   615  // be cancelled. Any resources associated with the ingest will be cleaned.
   616  func (s *store) Abort(ctx context.Context, ref string) error {
   617  	root := s.ingestRoot(ref)
   618  	if err := os.RemoveAll(root); err != nil {
   619  		if os.IsNotExist(err) {
   620  			return errors.Wrapf(errdefs.ErrNotFound, "ingest ref %q", ref)
   621  		}
   622  
   623  		return err
   624  	}
   625  
   626  	return nil
   627  }
   628  
   629  func (s *store) blobPath(dgst digest.Digest) (string, error) {
   630  	if err := dgst.Validate(); err != nil {
   631  		return "", errors.Wrapf(errdefs.ErrInvalidArgument, "cannot calculate blob path from invalid digest: %v", err)
   632  	}
   633  
   634  	return filepath.Join(s.root, "blobs", dgst.Algorithm().String(), dgst.Hex()), nil
   635  }
   636  
   637  func (s *store) ingestRoot(ref string) string {
   638  	// we take a digest of the ref to keep the ingest paths constant length.
   639  	// Note that this is not the current or potential digest of incoming content.
   640  	dgst := digest.FromString(ref)
   641  	return filepath.Join(s.root, "ingest", dgst.Hex())
   642  }
   643  
   644  // ingestPaths are returned. The paths are the following:
   645  //
   646  // - root: entire ingest directory
   647  // - ref: name of the starting ref, must be unique
   648  // - data: file where data is written
   649  //
   650  func (s *store) ingestPaths(ref string) (string, string, string) {
   651  	var (
   652  		fp = s.ingestRoot(ref)
   653  		rp = filepath.Join(fp, "ref")
   654  		dp = filepath.Join(fp, "data")
   655  	)
   656  
   657  	return fp, rp, dp
   658  }
   659  
   660  func readFileString(path string) (string, error) {
   661  	p, err := ioutil.ReadFile(path)
   662  	return string(p), err
   663  }
   664  
   665  // readFileTimestamp reads a file with just a timestamp present.
   666  func readFileTimestamp(p string) (time.Time, error) {
   667  	b, err := ioutil.ReadFile(p)
   668  	if err != nil {
   669  		if os.IsNotExist(err) {
   670  			err = errors.Wrap(errdefs.ErrNotFound, err.Error())
   671  		}
   672  		return time.Time{}, err
   673  	}
   674  
   675  	var t time.Time
   676  	if err := t.UnmarshalText(b); err != nil {
   677  		return time.Time{}, errors.Wrapf(err, "could not parse timestamp file %v", p)
   678  	}
   679  
   680  	return t, nil
   681  }
   682  
   683  func writeTimestampFile(p string, t time.Time) error {
   684  	b, err := t.MarshalText()
   685  	if err != nil {
   686  		return err
   687  	}
   688  	return atomicWrite(p, b, 0666)
   689  }
   690  
   691  func atomicWrite(path string, data []byte, mode os.FileMode) error {
   692  	tmp := fmt.Sprintf("%s.tmp", path)
   693  	f, err := os.OpenFile(tmp, os.O_RDWR|os.O_CREATE|os.O_TRUNC|os.O_SYNC, mode)
   694  	if err != nil {
   695  		return errors.Wrap(err, "create tmp file")
   696  	}
   697  	_, err = f.Write(data)
   698  	f.Close()
   699  	if err != nil {
   700  		return errors.Wrap(err, "write atomic data")
   701  	}
   702  	return os.Rename(tmp, path)
   703  }