github.com/nikkelma/oras-project_oras-go@v1.1.1-0.20220201001104-a75f6a419090/pkg/content/file.go (about)

     1  /*
     2  Copyright The ORAS Authors.
     3  Licensed under the Apache License, Version 2.0 (the "License");
     4  you may not use this file except in compliance with the License.
     5  You may obtain a copy of the License at
     6  
     7  http://www.apache.org/licenses/LICENSE-2.0
     8  
     9  Unless required by applicable law or agreed to in writing, software
    10  distributed under the License is distributed on an "AS IS" BASIS,
    11  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  See the License for the specific language governing permissions and
    13  limitations under the License.
    14  */
    15  
    16  package content
    17  
    18  import (
    19  	"bytes"
    20  	"compress/gzip"
    21  	"context"
    22  	_ "crypto/sha256"
    23  	"fmt"
    24  	"io"
    25  	"io/ioutil"
    26  	"os"
    27  	"path/filepath"
    28  	"strings"
    29  	"sync"
    30  	"time"
    31  
    32  	"github.com/containerd/containerd/content"
    33  	"github.com/containerd/containerd/errdefs"
    34  	"github.com/containerd/containerd/remotes"
    35  	digest "github.com/opencontainers/go-digest"
    36  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    37  	"github.com/pkg/errors"
    38  )
    39  
    40  // File provides content via files from the file system
    41  type File struct {
    42  	DisableOverwrite          bool
    43  	AllowPathTraversalOnWrite bool
    44  
    45  	// Reproducible enables stripping times from added files
    46  	Reproducible bool
    47  
    48  	root         string
    49  	descriptor   *sync.Map // map[digest.Digest]ocispec.Descriptor
    50  	pathMap      *sync.Map // map[name string](file string)
    51  	memoryMap    *sync.Map // map[digest.Digest]([]byte)
    52  	refMap       *sync.Map // map[string]ocispec.Descriptor
    53  	tmpFiles     *sync.Map
    54  	ignoreNoName bool
    55  }
    56  
    57  // NewFile creats a new file target. It represents a single root reference and all of its components.
    58  func NewFile(rootPath string, opts ...WriterOpt) *File {
    59  	// we have to process the opts to find if they told us to change defaults
    60  	wOpts := DefaultWriterOpts()
    61  	for _, opt := range opts {
    62  		if err := opt(&wOpts); err != nil {
    63  			continue
    64  		}
    65  	}
    66  	return &File{
    67  		root:         rootPath,
    68  		descriptor:   &sync.Map{},
    69  		pathMap:      &sync.Map{},
    70  		memoryMap:    &sync.Map{},
    71  		refMap:       &sync.Map{},
    72  		tmpFiles:     &sync.Map{},
    73  		ignoreNoName: wOpts.IgnoreNoName,
    74  	}
    75  }
    76  
    77  func (s *File) Resolver() remotes.Resolver {
    78  	return s
    79  }
    80  
    81  func (s *File) Resolve(ctx context.Context, ref string) (name string, desc ocispec.Descriptor, err error) {
    82  	desc, ok := s.getRef(ref)
    83  	if !ok {
    84  		return "", ocispec.Descriptor{}, fmt.Errorf("unknown reference: %s", ref)
    85  	}
    86  	return ref, desc, nil
    87  }
    88  
    89  func (s *File) Fetcher(ctx context.Context, ref string) (remotes.Fetcher, error) {
    90  	if _, ok := s.refMap.Load(ref); !ok {
    91  		return nil, fmt.Errorf("unknown reference: %s", ref)
    92  	}
    93  	return s, nil
    94  }
    95  
    96  // Fetch get an io.ReadCloser for the specific content
    97  func (s *File) Fetch(ctx context.Context, desc ocispec.Descriptor) (io.ReadCloser, error) {
    98  	// first see if it is in the in-memory manifest map
    99  	manifest, ok := s.getMemory(desc)
   100  	if ok {
   101  		return ioutil.NopCloser(bytes.NewReader(manifest)), nil
   102  	}
   103  	desc, ok = s.get(desc)
   104  	if !ok {
   105  		return nil, ErrNotFound
   106  	}
   107  	name, ok := ResolveName(desc)
   108  	if !ok {
   109  		return nil, ErrNoName
   110  	}
   111  	path := s.ResolvePath(name)
   112  	return os.Open(path)
   113  }
   114  
   115  func (s *File) Pusher(ctx context.Context, ref string) (remotes.Pusher, error) {
   116  	var tag, hash string
   117  	parts := strings.SplitN(ref, "@", 2)
   118  	if len(parts) > 0 {
   119  		tag = parts[0]
   120  	}
   121  	if len(parts) > 1 {
   122  		hash = parts[1]
   123  	}
   124  	return &filePusher{
   125  		store: s,
   126  		ref:   tag,
   127  		hash:  hash,
   128  	}, nil
   129  }
   130  
   131  type filePusher struct {
   132  	store *File
   133  	ref   string
   134  	hash  string
   135  }
   136  
   137  func (s *filePusher) Push(ctx context.Context, desc ocispec.Descriptor) (content.Writer, error) {
   138  	name, ok := ResolveName(desc)
   139  	now := time.Now()
   140  	if !ok {
   141  		// if we were not told to ignore NoName, then return an error
   142  		if !s.store.ignoreNoName {
   143  			return nil, ErrNoName
   144  		}
   145  
   146  		// just return a nil writer - we do not want to calculate the hash, so just use
   147  		// whatever was passed in the descriptor
   148  		return NewIoContentWriter(ioutil.Discard, WithOutputHash(desc.Digest)), nil
   149  	}
   150  	path, err := s.store.resolveWritePath(name)
   151  	if err != nil {
   152  		return nil, err
   153  	}
   154  	file, afterCommit, err := s.store.createWritePath(path, desc, name)
   155  	if err != nil {
   156  		return nil, err
   157  	}
   158  
   159  	return &fileWriter{
   160  		store:    s.store,
   161  		file:     file,
   162  		desc:     desc,
   163  		digester: digest.Canonical.Digester(),
   164  		status: content.Status{
   165  			Ref:       name,
   166  			Total:     desc.Size,
   167  			StartedAt: now,
   168  			UpdatedAt: now,
   169  		},
   170  		afterCommit: afterCommit,
   171  	}, nil
   172  }
   173  
   174  // Add adds a file reference from a path, either directory or single file,
   175  // and returns the reference descriptor.
   176  func (s *File) Add(name, mediaType, path string) (ocispec.Descriptor, error) {
   177  	if path == "" {
   178  		path = name
   179  	}
   180  	path = s.MapPath(name, path)
   181  
   182  	fileInfo, err := os.Stat(path)
   183  	if err != nil {
   184  		return ocispec.Descriptor{}, err
   185  	}
   186  
   187  	var desc ocispec.Descriptor
   188  	if fileInfo.IsDir() {
   189  		desc, err = s.descFromDir(name, mediaType, path)
   190  	} else {
   191  		desc, err = s.descFromFile(fileInfo, mediaType, path)
   192  	}
   193  	if err != nil {
   194  		return ocispec.Descriptor{}, err
   195  	}
   196  	if desc.Annotations == nil {
   197  		desc.Annotations = make(map[string]string)
   198  	}
   199  	desc.Annotations[ocispec.AnnotationTitle] = name
   200  
   201  	s.set(desc)
   202  	return desc, nil
   203  }
   204  
   205  // Load is a lower-level memory-only version of Add. Rather than taking a path,
   206  // generating a descriptor and creating a reference, it takes raw data and a descriptor
   207  // that describes that data and stores it in memory. It will disappear at process
   208  // termination.
   209  //
   210  // It is especially useful for adding ephemeral data, such as config, that must
   211  // exist in order to walk a manifest.
   212  func (s *File) Load(desc ocispec.Descriptor, data []byte) error {
   213  	s.memoryMap.Store(desc.Digest, data)
   214  	return nil
   215  }
   216  
   217  // Ref gets a reference's descriptor and content
   218  func (s *File) Ref(ref string) (ocispec.Descriptor, []byte, error) {
   219  	desc, ok := s.getRef(ref)
   220  	if !ok {
   221  		return ocispec.Descriptor{}, nil, ErrNotFound
   222  	}
   223  	// first see if it is in the in-memory manifest map
   224  	manifest, ok := s.getMemory(desc)
   225  	if !ok {
   226  		return ocispec.Descriptor{}, nil, ErrNotFound
   227  	}
   228  	return desc, manifest, nil
   229  }
   230  
   231  func (s *File) descFromFile(info os.FileInfo, mediaType, path string) (ocispec.Descriptor, error) {
   232  	file, err := os.Open(path)
   233  	if err != nil {
   234  		return ocispec.Descriptor{}, err
   235  	}
   236  	defer file.Close()
   237  	digest, err := digest.FromReader(file)
   238  	if err != nil {
   239  		return ocispec.Descriptor{}, err
   240  	}
   241  
   242  	if mediaType == "" {
   243  		mediaType = DefaultBlobMediaType
   244  	}
   245  	return ocispec.Descriptor{
   246  		MediaType: mediaType,
   247  		Digest:    digest,
   248  		Size:      info.Size(),
   249  	}, nil
   250  }
   251  
   252  func (s *File) descFromDir(name, mediaType, root string) (ocispec.Descriptor, error) {
   253  	// generate temp file
   254  	file, err := s.tempFile()
   255  	if err != nil {
   256  		return ocispec.Descriptor{}, err
   257  	}
   258  	defer file.Close()
   259  	s.MapPath(name, file.Name())
   260  
   261  	// compress directory
   262  	digester := digest.Canonical.Digester()
   263  	zw := gzip.NewWriter(io.MultiWriter(file, digester.Hash()))
   264  	defer zw.Close()
   265  	tarDigester := digest.Canonical.Digester()
   266  	if err := tarDirectory(root, name, io.MultiWriter(zw, tarDigester.Hash()), s.Reproducible); err != nil {
   267  		return ocispec.Descriptor{}, err
   268  	}
   269  
   270  	// flush all
   271  	if err := zw.Close(); err != nil {
   272  		return ocispec.Descriptor{}, err
   273  	}
   274  	if err := file.Sync(); err != nil {
   275  		return ocispec.Descriptor{}, err
   276  	}
   277  
   278  	// generate descriptor
   279  	if mediaType == "" {
   280  		mediaType = DefaultBlobDirMediaType
   281  	}
   282  	info, err := file.Stat()
   283  	if err != nil {
   284  		return ocispec.Descriptor{}, err
   285  	}
   286  	return ocispec.Descriptor{
   287  		MediaType: mediaType,
   288  		Digest:    digester.Digest(),
   289  		Size:      info.Size(),
   290  		Annotations: map[string]string{
   291  			AnnotationDigest: tarDigester.Digest().String(),
   292  			AnnotationUnpack: "true",
   293  		},
   294  	}, nil
   295  }
   296  
   297  func (s *File) tempFile() (*os.File, error) {
   298  	file, err := ioutil.TempFile("", TempFilePattern)
   299  	if err != nil {
   300  		return nil, err
   301  	}
   302  	s.tmpFiles.Store(file.Name(), file)
   303  	return file, nil
   304  }
   305  
   306  // Close frees up resources used by the file store
   307  func (s *File) Close() error {
   308  	var errs []string
   309  	s.tmpFiles.Range(func(name, _ interface{}) bool {
   310  		if err := os.Remove(name.(string)); err != nil {
   311  			errs = append(errs, err.Error())
   312  		}
   313  		return true
   314  	})
   315  	return errors.New(strings.Join(errs, "; "))
   316  }
   317  
   318  func (s *File) resolveWritePath(name string) (string, error) {
   319  	path := s.ResolvePath(name)
   320  	if !s.AllowPathTraversalOnWrite {
   321  		base, err := filepath.Abs(s.root)
   322  		if err != nil {
   323  			return "", err
   324  		}
   325  		target, err := filepath.Abs(path)
   326  		if err != nil {
   327  			return "", err
   328  		}
   329  		rel, err := filepath.Rel(base, target)
   330  		if err != nil {
   331  			return "", ErrPathTraversalDisallowed
   332  		}
   333  		rel = filepath.ToSlash(rel)
   334  		if strings.HasPrefix(rel, "../") || rel == ".." {
   335  			return "", ErrPathTraversalDisallowed
   336  		}
   337  	}
   338  	if s.DisableOverwrite {
   339  		if _, err := os.Stat(path); err == nil {
   340  			return "", ErrOverwriteDisallowed
   341  		} else if !os.IsNotExist(err) {
   342  			return "", err
   343  		}
   344  	}
   345  	return path, nil
   346  }
   347  
   348  func (s *File) createWritePath(path string, desc ocispec.Descriptor, prefix string) (*os.File, func() error, error) {
   349  	if value, ok := desc.Annotations[AnnotationUnpack]; !ok || value != "true" {
   350  		if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
   351  			return nil, nil, err
   352  		}
   353  		file, err := os.Create(path)
   354  		return file, nil, err
   355  	}
   356  
   357  	if err := os.MkdirAll(path, 0755); err != nil {
   358  		return nil, nil, err
   359  	}
   360  	file, err := s.tempFile()
   361  	checksum := desc.Annotations[AnnotationDigest]
   362  	afterCommit := func() error {
   363  		return extractTarGzip(path, prefix, file.Name(), checksum)
   364  	}
   365  	return file, afterCommit, err
   366  }
   367  
   368  // MapPath maps name to path
   369  func (s *File) MapPath(name, path string) string {
   370  	path = s.resolvePath(path)
   371  	s.pathMap.Store(name, path)
   372  	return path
   373  }
   374  
   375  // ResolvePath returns the path by name
   376  func (s *File) ResolvePath(name string) string {
   377  	if value, ok := s.pathMap.Load(name); ok {
   378  		if path, ok := value.(string); ok {
   379  			return path
   380  		}
   381  	}
   382  
   383  	// using the name as a fallback solution
   384  	return s.resolvePath(name)
   385  }
   386  
   387  func (s *File) resolvePath(path string) string {
   388  	if filepath.IsAbs(path) {
   389  		return path
   390  	}
   391  	return filepath.Join(s.root, path)
   392  }
   393  
   394  func (s *File) set(desc ocispec.Descriptor) {
   395  	s.descriptor.Store(desc.Digest, desc)
   396  }
   397  
   398  func (s *File) get(desc ocispec.Descriptor) (ocispec.Descriptor, bool) {
   399  	value, ok := s.descriptor.Load(desc.Digest)
   400  	if !ok {
   401  		return ocispec.Descriptor{}, false
   402  	}
   403  	desc, ok = value.(ocispec.Descriptor)
   404  	return desc, ok
   405  }
   406  
   407  func (s *File) getMemory(desc ocispec.Descriptor) ([]byte, bool) {
   408  	value, ok := s.memoryMap.Load(desc.Digest)
   409  	if !ok {
   410  		return nil, false
   411  	}
   412  	content, ok := value.([]byte)
   413  	return content, ok
   414  }
   415  
   416  func (s *File) getRef(ref string) (ocispec.Descriptor, bool) {
   417  	value, ok := s.refMap.Load(ref)
   418  	if !ok {
   419  		return ocispec.Descriptor{}, false
   420  	}
   421  	desc, ok := value.(ocispec.Descriptor)
   422  	return desc, ok
   423  }
   424  
   425  // StoreManifest stores a manifest linked to by the provided ref. The children of the
   426  // manifest, such as layers and config, should already exist in the file store, either
   427  // as files linked via Add(), or via Load(). If they do not exist, then a typical
   428  // Fetcher that walks the manifest will hit an unresolved hash.
   429  //
   430  // StoreManifest does *not* validate their presence.
   431  func (s *File) StoreManifest(ref string, desc ocispec.Descriptor, manifest []byte) error {
   432  	s.refMap.Store(ref, desc)
   433  	s.memoryMap.Store(desc.Digest, manifest)
   434  	return nil
   435  }
   436  
   437  type fileWriter struct {
   438  	store       *File
   439  	file        *os.File
   440  	desc        ocispec.Descriptor
   441  	digester    digest.Digester
   442  	status      content.Status
   443  	afterCommit func() error
   444  }
   445  
   446  func (w *fileWriter) Status() (content.Status, error) {
   447  	return w.status, nil
   448  }
   449  
   450  // Digest returns the current digest of the content, up to the current write.
   451  //
   452  // Cannot be called concurrently with `Write`.
   453  func (w *fileWriter) Digest() digest.Digest {
   454  	return w.digester.Digest()
   455  }
   456  
   457  // Write p to the transaction.
   458  func (w *fileWriter) Write(p []byte) (n int, err error) {
   459  	n, err = w.file.Write(p)
   460  	w.digester.Hash().Write(p[:n])
   461  	w.status.Offset += int64(len(p))
   462  	w.status.UpdatedAt = time.Now()
   463  	return n, err
   464  }
   465  
   466  func (w *fileWriter) Commit(ctx context.Context, size int64, expected digest.Digest, opts ...content.Opt) error {
   467  	var base content.Info
   468  	for _, opt := range opts {
   469  		if err := opt(&base); err != nil {
   470  			return err
   471  		}
   472  	}
   473  
   474  	if w.file == nil {
   475  		return errors.Wrap(errdefs.ErrFailedPrecondition, "cannot commit on closed writer")
   476  	}
   477  	file := w.file
   478  	w.file = nil
   479  
   480  	if err := file.Sync(); err != nil {
   481  		file.Close()
   482  		return errors.Wrap(err, "sync failed")
   483  	}
   484  
   485  	fileInfo, err := file.Stat()
   486  	if err != nil {
   487  		file.Close()
   488  		return errors.Wrap(err, "stat failed")
   489  	}
   490  	if err := file.Close(); err != nil {
   491  		return errors.Wrap(err, "failed to close file")
   492  	}
   493  
   494  	if size > 0 && size != fileInfo.Size() {
   495  		return errors.Wrapf(errdefs.ErrFailedPrecondition, "unexpected commit size %d, expected %d", fileInfo.Size(), size)
   496  	}
   497  	if dgst := w.digester.Digest(); expected != "" && expected != dgst {
   498  		return errors.Wrapf(errdefs.ErrFailedPrecondition, "unexpected commit digest %s, expected %s", dgst, expected)
   499  	}
   500  
   501  	w.store.set(w.desc)
   502  	if w.afterCommit != nil {
   503  		return w.afterCommit()
   504  	}
   505  	return nil
   506  }
   507  
   508  // Close the writer, flushing any unwritten data and leaving the progress in
   509  // tact.
   510  func (w *fileWriter) Close() error {
   511  	if w.file == nil {
   512  		return nil
   513  	}
   514  
   515  	w.file.Sync()
   516  	err := w.file.Close()
   517  	w.file = nil
   518  	return err
   519  }
   520  
   521  func (w *fileWriter) Truncate(size int64) error {
   522  	if size != 0 {
   523  		return ErrUnsupportedSize
   524  	}
   525  	w.status.Offset = 0
   526  	w.digester.Hash().Reset()
   527  	if _, err := w.file.Seek(0, io.SeekStart); err != nil {
   528  		return err
   529  	}
   530  	return w.file.Truncate(0)
   531  }