1189 lines
27 KiB
Go
1189 lines
27 KiB
Go
package contenthash
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/sha256"
|
|
"io"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/docker/docker/pkg/fileutils"
|
|
iradix "github.com/hashicorp/go-immutable-radix"
|
|
"github.com/hashicorp/golang-lru/simplelru"
|
|
"github.com/moby/buildkit/cache"
|
|
"github.com/moby/buildkit/session"
|
|
"github.com/moby/buildkit/snapshot"
|
|
"github.com/moby/locker"
|
|
digest "github.com/opencontainers/go-digest"
|
|
"github.com/pkg/errors"
|
|
"github.com/tonistiigi/fsutil"
|
|
fstypes "github.com/tonistiigi/fsutil/types"
|
|
)
|
|
|
|
var errNotFound = errors.Errorf("not found")
|
|
|
|
var defaultManager *cacheManager
|
|
var defaultManagerOnce sync.Once
|
|
|
|
func getDefaultManager() *cacheManager {
|
|
defaultManagerOnce.Do(func() {
|
|
lru, _ := simplelru.NewLRU(20, nil) // error is impossible on positive size
|
|
defaultManager = &cacheManager{lru: lru, locker: locker.New()}
|
|
})
|
|
return defaultManager
|
|
}
|
|
|
|
// Layout in the radix tree: Every path is saved by cleaned absolute unix path.
|
|
// Directories have 2 records, one contains digest for directory header, other
|
|
// the recursive digest for directory contents. "/dir/" is the record for
|
|
// header, "/dir" is for contents. For the root node "" (empty string) is the
|
|
// key for root, "/" for the root header
|
|
|
|
type ChecksumOpts struct {
|
|
FollowLinks bool
|
|
Wildcard bool
|
|
IncludePatterns []string
|
|
ExcludePatterns []string
|
|
}
|
|
|
|
func Checksum(ctx context.Context, ref cache.ImmutableRef, path string, opts ChecksumOpts, s session.Group) (digest.Digest, error) {
|
|
return getDefaultManager().Checksum(ctx, ref, path, opts, s)
|
|
}
|
|
|
|
func GetCacheContext(ctx context.Context, md cache.RefMetadata) (CacheContext, error) {
|
|
return getDefaultManager().GetCacheContext(ctx, md)
|
|
}
|
|
|
|
func SetCacheContext(ctx context.Context, md cache.RefMetadata, cc CacheContext) error {
|
|
return getDefaultManager().SetCacheContext(ctx, md, cc)
|
|
}
|
|
|
|
func ClearCacheContext(md cache.RefMetadata) {
|
|
getDefaultManager().clearCacheContext(md.ID())
|
|
}
|
|
|
|
type CacheContext interface {
|
|
Checksum(ctx context.Context, ref cache.Mountable, p string, opts ChecksumOpts, s session.Group) (digest.Digest, error)
|
|
HandleChange(kind fsutil.ChangeKind, p string, fi os.FileInfo, err error) error
|
|
}
|
|
|
|
type Hashed interface {
|
|
Digest() digest.Digest
|
|
}
|
|
|
|
type includedPath struct {
|
|
path string
|
|
record *CacheRecord
|
|
matchedIncludePattern bool
|
|
matchedExcludePattern bool
|
|
}
|
|
|
|
type cacheManager struct {
|
|
locker *locker.Locker
|
|
lru *simplelru.LRU
|
|
lruMu sync.Mutex
|
|
}
|
|
|
|
func (cm *cacheManager) Checksum(ctx context.Context, ref cache.ImmutableRef, p string, opts ChecksumOpts, s session.Group) (digest.Digest, error) {
|
|
if ref == nil {
|
|
if p == "/" {
|
|
return digest.FromBytes(nil), nil
|
|
}
|
|
return "", errors.Errorf("%s: no such file or directory", p)
|
|
}
|
|
cc, err := cm.GetCacheContext(ctx, ensureOriginMetadata(ref))
|
|
if err != nil {
|
|
return "", nil
|
|
}
|
|
return cc.Checksum(ctx, ref, p, opts, s)
|
|
}
|
|
|
|
func (cm *cacheManager) GetCacheContext(ctx context.Context, md cache.RefMetadata) (CacheContext, error) {
|
|
cm.locker.Lock(md.ID())
|
|
cm.lruMu.Lock()
|
|
v, ok := cm.lru.Get(md.ID())
|
|
cm.lruMu.Unlock()
|
|
if ok {
|
|
cm.locker.Unlock(md.ID())
|
|
v.(*cacheContext).linkMap = map[string][][]byte{}
|
|
return v.(*cacheContext), nil
|
|
}
|
|
cc, err := newCacheContext(md)
|
|
if err != nil {
|
|
cm.locker.Unlock(md.ID())
|
|
return nil, err
|
|
}
|
|
cm.lruMu.Lock()
|
|
cm.lru.Add(md.ID(), cc)
|
|
cm.lruMu.Unlock()
|
|
cm.locker.Unlock(md.ID())
|
|
return cc, nil
|
|
}
|
|
|
|
func (cm *cacheManager) SetCacheContext(ctx context.Context, md cache.RefMetadata, cci CacheContext) error {
|
|
cc, ok := cci.(*cacheContext)
|
|
if !ok {
|
|
return errors.Errorf("invalid cachecontext: %T", cc)
|
|
}
|
|
if md.ID() != cc.md.ID() {
|
|
cc = &cacheContext{
|
|
md: cacheMetadata{md},
|
|
tree: cci.(*cacheContext).tree,
|
|
dirtyMap: map[string]struct{}{},
|
|
linkMap: map[string][][]byte{},
|
|
}
|
|
} else {
|
|
if err := cc.save(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
cm.lruMu.Lock()
|
|
cm.lru.Add(md.ID(), cc)
|
|
cm.lruMu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
func (cm *cacheManager) clearCacheContext(id string) {
|
|
cm.lruMu.Lock()
|
|
cm.lru.Remove(id)
|
|
cm.lruMu.Unlock()
|
|
}
|
|
|
|
type cacheContext struct {
|
|
mu sync.RWMutex
|
|
md cacheMetadata
|
|
tree *iradix.Tree
|
|
dirty bool // needs to be persisted to disk
|
|
|
|
// used in HandleChange
|
|
txn *iradix.Txn
|
|
node *iradix.Node
|
|
dirtyMap map[string]struct{}
|
|
linkMap map[string][][]byte
|
|
}
|
|
|
|
type cacheMetadata struct {
|
|
cache.RefMetadata
|
|
}
|
|
|
|
const keyContentHash = "buildkit.contenthash.v0"
|
|
|
|
func (md cacheMetadata) GetContentHash() ([]byte, error) {
|
|
return md.GetExternal(keyContentHash)
|
|
}
|
|
|
|
func (md cacheMetadata) SetContentHash(dt []byte) error {
|
|
return md.SetExternal(keyContentHash, dt)
|
|
}
|
|
|
|
type mount struct {
|
|
mountable cache.Mountable
|
|
mountPath string
|
|
unmount func() error
|
|
session session.Group
|
|
}
|
|
|
|
func (m *mount) mount(ctx context.Context) (string, error) {
|
|
if m.mountPath != "" {
|
|
return m.mountPath, nil
|
|
}
|
|
mounts, err := m.mountable.Mount(ctx, true, m.session)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
lm := snapshot.LocalMounter(mounts)
|
|
|
|
mp, err := lm.Mount()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
m.mountPath = mp
|
|
m.unmount = lm.Unmount
|
|
return mp, nil
|
|
}
|
|
|
|
func (m *mount) clean() error {
|
|
if m.mountPath != "" {
|
|
if err := m.unmount(); err != nil {
|
|
return err
|
|
}
|
|
m.mountPath = ""
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func newCacheContext(md cache.RefMetadata) (*cacheContext, error) {
|
|
cc := &cacheContext{
|
|
md: cacheMetadata{md},
|
|
tree: iradix.New(),
|
|
dirtyMap: map[string]struct{}{},
|
|
linkMap: map[string][][]byte{},
|
|
}
|
|
if err := cc.load(); err != nil {
|
|
return nil, err
|
|
}
|
|
return cc, nil
|
|
}
|
|
|
|
func (cc *cacheContext) load() error {
|
|
dt, err := cc.md.GetContentHash()
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
var l CacheRecords
|
|
if err := l.Unmarshal(dt); err != nil {
|
|
return err
|
|
}
|
|
|
|
txn := cc.tree.Txn()
|
|
for _, p := range l.Paths {
|
|
txn.Insert([]byte(p.Path), p.Record)
|
|
}
|
|
cc.tree = txn.Commit()
|
|
return nil
|
|
}
|
|
|
|
func (cc *cacheContext) save() error {
|
|
cc.mu.Lock()
|
|
defer cc.mu.Unlock()
|
|
|
|
if cc.txn != nil {
|
|
cc.commitActiveTransaction()
|
|
}
|
|
|
|
var l CacheRecords
|
|
node := cc.tree.Root()
|
|
node.Walk(func(k []byte, v interface{}) bool {
|
|
l.Paths = append(l.Paths, &CacheRecordWithPath{
|
|
Path: string(k),
|
|
Record: v.(*CacheRecord),
|
|
})
|
|
return false
|
|
})
|
|
|
|
dt, err := l.Marshal()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return cc.md.SetContentHash(dt)
|
|
}
|
|
|
|
func keyPath(p string) string {
|
|
p = path.Join("/", filepath.ToSlash(p))
|
|
if p == "/" {
|
|
p = ""
|
|
}
|
|
return p
|
|
}
|
|
|
|
// HandleChange notifies the source about a modification operation
|
|
func (cc *cacheContext) HandleChange(kind fsutil.ChangeKind, p string, fi os.FileInfo, err error) (retErr error) {
|
|
p = keyPath(p)
|
|
k := convertPathToKey([]byte(p))
|
|
|
|
deleteDir := func(cr *CacheRecord) {
|
|
if cr.Type == CacheRecordTypeDir {
|
|
cc.node.WalkPrefix(append(k, 0), func(k []byte, v interface{}) bool {
|
|
cc.txn.Delete(k)
|
|
return false
|
|
})
|
|
}
|
|
}
|
|
|
|
cc.mu.Lock()
|
|
defer cc.mu.Unlock()
|
|
if cc.txn == nil {
|
|
cc.txn = cc.tree.Txn()
|
|
cc.node = cc.tree.Root()
|
|
|
|
// root is not called by HandleChange. need to fake it
|
|
if _, ok := cc.node.Get([]byte{0}); !ok {
|
|
cc.txn.Insert([]byte{0}, &CacheRecord{
|
|
Type: CacheRecordTypeDirHeader,
|
|
Digest: digest.FromBytes(nil),
|
|
})
|
|
cc.txn.Insert([]byte(""), &CacheRecord{
|
|
Type: CacheRecordTypeDir,
|
|
})
|
|
}
|
|
}
|
|
|
|
if kind == fsutil.ChangeKindDelete {
|
|
v, ok := cc.txn.Delete(k)
|
|
if ok {
|
|
deleteDir(v.(*CacheRecord))
|
|
}
|
|
d := path.Dir(p)
|
|
if d == "/" {
|
|
d = ""
|
|
}
|
|
cc.dirtyMap[d] = struct{}{}
|
|
return
|
|
}
|
|
|
|
stat, ok := fi.Sys().(*fstypes.Stat)
|
|
if !ok {
|
|
return errors.Errorf("%s invalid change without stat information", p)
|
|
}
|
|
|
|
h, ok := fi.(Hashed)
|
|
if !ok {
|
|
return errors.Errorf("invalid fileinfo: %s", p)
|
|
}
|
|
|
|
v, ok := cc.node.Get(k)
|
|
if ok {
|
|
deleteDir(v.(*CacheRecord))
|
|
}
|
|
|
|
cr := &CacheRecord{
|
|
Type: CacheRecordTypeFile,
|
|
}
|
|
if fi.Mode()&os.ModeSymlink != 0 {
|
|
cr.Type = CacheRecordTypeSymlink
|
|
cr.Linkname = filepath.ToSlash(stat.Linkname)
|
|
}
|
|
if fi.IsDir() {
|
|
cr.Type = CacheRecordTypeDirHeader
|
|
cr2 := &CacheRecord{
|
|
Type: CacheRecordTypeDir,
|
|
}
|
|
cc.txn.Insert(k, cr2)
|
|
k = append(k, 0)
|
|
p += "/"
|
|
}
|
|
cr.Digest = h.Digest()
|
|
|
|
// if we receive a hardlink just use the digest of the source
|
|
// note that the source may be called later because data writing is async
|
|
if fi.Mode()&os.ModeSymlink == 0 && stat.Linkname != "" {
|
|
ln := path.Join("/", filepath.ToSlash(stat.Linkname))
|
|
v, ok := cc.txn.Get(convertPathToKey([]byte(ln)))
|
|
if ok {
|
|
cp := *v.(*CacheRecord)
|
|
cr = &cp
|
|
}
|
|
cc.linkMap[ln] = append(cc.linkMap[ln], k)
|
|
}
|
|
|
|
cc.txn.Insert(k, cr)
|
|
if !fi.IsDir() {
|
|
if links, ok := cc.linkMap[p]; ok {
|
|
for _, l := range links {
|
|
pp := convertKeyToPath(l)
|
|
cc.txn.Insert(l, cr)
|
|
d := path.Dir(string(pp))
|
|
if d == "/" {
|
|
d = ""
|
|
}
|
|
cc.dirtyMap[d] = struct{}{}
|
|
}
|
|
delete(cc.linkMap, p)
|
|
}
|
|
}
|
|
|
|
d := path.Dir(p)
|
|
if d == "/" {
|
|
d = ""
|
|
}
|
|
cc.dirtyMap[d] = struct{}{}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (cc *cacheContext) Checksum(ctx context.Context, mountable cache.Mountable, p string, opts ChecksumOpts, s session.Group) (digest.Digest, error) {
|
|
m := &mount{mountable: mountable, session: s}
|
|
defer m.clean()
|
|
|
|
if !opts.Wildcard && len(opts.IncludePatterns) == 0 && len(opts.ExcludePatterns) == 0 {
|
|
return cc.checksumFollow(ctx, m, p, opts.FollowLinks)
|
|
}
|
|
|
|
includedPaths, err := cc.includedPaths(ctx, m, p, opts)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if opts.FollowLinks {
|
|
for i, w := range includedPaths {
|
|
if w.record.Type == CacheRecordTypeSymlink {
|
|
dgst, err := cc.checksumFollow(ctx, m, w.path, opts.FollowLinks)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
includedPaths[i].record = &CacheRecord{Digest: dgst}
|
|
}
|
|
}
|
|
}
|
|
if len(includedPaths) == 0 {
|
|
return digest.FromBytes([]byte{}), nil
|
|
}
|
|
|
|
if len(includedPaths) == 1 && path.Base(p) == path.Base(includedPaths[0].path) {
|
|
return includedPaths[0].record.Digest, nil
|
|
}
|
|
|
|
digester := digest.Canonical.Digester()
|
|
for i, w := range includedPaths {
|
|
if i != 0 {
|
|
digester.Hash().Write([]byte{0})
|
|
}
|
|
digester.Hash().Write([]byte(path.Base(w.path)))
|
|
digester.Hash().Write([]byte(w.record.Digest))
|
|
}
|
|
return digester.Digest(), nil
|
|
}
|
|
|
|
func (cc *cacheContext) checksumFollow(ctx context.Context, m *mount, p string, follow bool) (digest.Digest, error) {
|
|
const maxSymlinkLimit = 255
|
|
i := 0
|
|
for {
|
|
if i > maxSymlinkLimit {
|
|
return "", errors.Errorf("too many symlinks: %s", p)
|
|
}
|
|
cr, err := cc.checksumNoFollow(ctx, m, p)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if cr.Type == CacheRecordTypeSymlink && follow {
|
|
link := cr.Linkname
|
|
if !path.IsAbs(cr.Linkname) {
|
|
link = path.Join(path.Dir(p), link)
|
|
}
|
|
i++
|
|
p = link
|
|
} else {
|
|
return cr.Digest, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func (cc *cacheContext) includedPaths(ctx context.Context, m *mount, p string, opts ChecksumOpts) ([]*includedPath, error) {
|
|
cc.mu.Lock()
|
|
defer cc.mu.Unlock()
|
|
|
|
if cc.txn != nil {
|
|
cc.commitActiveTransaction()
|
|
}
|
|
|
|
root := cc.tree.Root()
|
|
scan, err := cc.needsScan(root, "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if scan {
|
|
if err := cc.scanPath(ctx, m, ""); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
defer func() {
|
|
if cc.dirty {
|
|
go cc.save()
|
|
cc.dirty = false
|
|
}
|
|
}()
|
|
|
|
endsInSep := len(p) != 0 && p[len(p)-1] == filepath.Separator
|
|
p = keyPath(p)
|
|
|
|
var includePatternMatcher *fileutils.PatternMatcher
|
|
if len(opts.IncludePatterns) != 0 {
|
|
includePatternMatcher, err = fileutils.NewPatternMatcher(opts.IncludePatterns)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "invalid includepatterns: %s", opts.IncludePatterns)
|
|
}
|
|
}
|
|
|
|
var excludePatternMatcher *fileutils.PatternMatcher
|
|
if len(opts.ExcludePatterns) != 0 {
|
|
excludePatternMatcher, err = fileutils.NewPatternMatcher(opts.ExcludePatterns)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "invalid excludepatterns: %s", opts.ExcludePatterns)
|
|
}
|
|
}
|
|
|
|
includedPaths := make([]*includedPath, 0, 2)
|
|
|
|
txn := cc.tree.Txn()
|
|
root = txn.Root()
|
|
var (
|
|
updated bool
|
|
iter *iradix.Iterator
|
|
k []byte
|
|
kOk bool
|
|
origPrefix string
|
|
resolvedPrefix string
|
|
)
|
|
|
|
iter = root.Iterator()
|
|
|
|
if opts.Wildcard {
|
|
origPrefix, k, kOk, err = wildcardPrefix(root, p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
origPrefix = p
|
|
k = convertPathToKey([]byte(origPrefix))
|
|
|
|
// We need to resolve symlinks here, in case the base path
|
|
// involves a symlink. That will match fsutil behavior of
|
|
// calling functions such as stat and walk.
|
|
var cr *CacheRecord
|
|
k, cr, err = getFollowLinks(root, k, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
kOk = (cr != nil)
|
|
}
|
|
|
|
if origPrefix != "" {
|
|
if kOk {
|
|
iter.SeekLowerBound(append(append([]byte{}, k...), 0))
|
|
}
|
|
|
|
resolvedPrefix = string(convertKeyToPath(k))
|
|
} else {
|
|
k, _, kOk = iter.Next()
|
|
}
|
|
|
|
var (
|
|
parentDirHeaders []*includedPath
|
|
lastMatchedDir string
|
|
)
|
|
|
|
for kOk {
|
|
fn := string(convertKeyToPath(k))
|
|
|
|
// Convert the path prefix from what we found in the prefix
|
|
// tree to what the argument specified.
|
|
//
|
|
// For example, if the original 'p' argument was /a/b and there
|
|
// is a symlink a->c, we want fn to be /a/b/foo rather than
|
|
// /c/b/foo. This is necessary to ensure correct pattern
|
|
// matching.
|
|
//
|
|
// When wildcards are enabled, this translation applies to the
|
|
// portion of 'p' before any wildcards.
|
|
if strings.HasPrefix(fn, resolvedPrefix) {
|
|
fn = origPrefix + strings.TrimPrefix(fn, resolvedPrefix)
|
|
}
|
|
|
|
for len(parentDirHeaders) != 0 {
|
|
lastParentDir := parentDirHeaders[len(parentDirHeaders)-1]
|
|
if strings.HasPrefix(fn, lastParentDir.path+"/") {
|
|
break
|
|
}
|
|
parentDirHeaders = parentDirHeaders[:len(parentDirHeaders)-1]
|
|
}
|
|
var parentDir *includedPath
|
|
if len(parentDirHeaders) != 0 {
|
|
parentDir = parentDirHeaders[len(parentDirHeaders)-1]
|
|
}
|
|
|
|
dirHeader := false
|
|
if len(k) > 0 && k[len(k)-1] == byte(0) {
|
|
dirHeader = true
|
|
fn = fn[:len(fn)-1]
|
|
if fn == p && endsInSep {
|
|
// We don't include the metadata header for a source dir which ends with a separator
|
|
k, _, kOk = iter.Next()
|
|
continue
|
|
}
|
|
}
|
|
|
|
maybeIncludedPath := &includedPath{path: fn}
|
|
var shouldInclude bool
|
|
if opts.Wildcard {
|
|
if p != "" && (lastMatchedDir == "" || !strings.HasPrefix(fn, lastMatchedDir+"/")) {
|
|
include, err := path.Match(p, fn)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !include {
|
|
k, _, kOk = iter.Next()
|
|
continue
|
|
}
|
|
lastMatchedDir = fn
|
|
}
|
|
|
|
shouldInclude, err = shouldIncludePath(
|
|
strings.TrimSuffix(strings.TrimPrefix(fn+"/", lastMatchedDir+"/"), "/"),
|
|
includePatternMatcher,
|
|
excludePatternMatcher,
|
|
maybeIncludedPath,
|
|
parentDir,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
if !strings.HasPrefix(fn+"/", p+"/") {
|
|
break
|
|
}
|
|
|
|
shouldInclude, err = shouldIncludePath(
|
|
strings.TrimSuffix(strings.TrimPrefix(fn+"/", p+"/"), "/"),
|
|
includePatternMatcher,
|
|
excludePatternMatcher,
|
|
maybeIncludedPath,
|
|
parentDir,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if !shouldInclude && !dirHeader {
|
|
k, _, kOk = iter.Next()
|
|
continue
|
|
}
|
|
|
|
cr, upt, err := cc.checksum(ctx, root, txn, m, k, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if upt {
|
|
updated = true
|
|
}
|
|
|
|
if cr.Type == CacheRecordTypeDir {
|
|
// We only hash dir headers and files, not dir contents. Hashing
|
|
// dir contents could be wrong if there are exclusions within the
|
|
// dir.
|
|
shouldInclude = false
|
|
}
|
|
maybeIncludedPath.record = cr
|
|
|
|
if !shouldInclude {
|
|
if cr.Type == CacheRecordTypeDirHeader {
|
|
// We keep track of non-included parent dir headers in case an
|
|
// include pattern matches a file inside one of these dirs.
|
|
parentDirHeaders = append(parentDirHeaders, maybeIncludedPath)
|
|
}
|
|
} else {
|
|
includedPaths = append(includedPaths, parentDirHeaders...)
|
|
parentDirHeaders = nil
|
|
includedPaths = append(includedPaths, maybeIncludedPath)
|
|
}
|
|
k, _, kOk = iter.Next()
|
|
}
|
|
|
|
cc.tree = txn.Commit()
|
|
cc.dirty = updated
|
|
|
|
return includedPaths, nil
|
|
}
|
|
|
|
func shouldIncludePath(
|
|
candidate string,
|
|
includePatternMatcher *fileutils.PatternMatcher,
|
|
excludePatternMatcher *fileutils.PatternMatcher,
|
|
maybeIncludedPath *includedPath,
|
|
parentDir *includedPath,
|
|
) (bool, error) {
|
|
var (
|
|
m bool
|
|
err error
|
|
)
|
|
if includePatternMatcher != nil {
|
|
if parentDir != nil {
|
|
m, err = includePatternMatcher.MatchesUsingParentResult(candidate, parentDir.matchedIncludePattern)
|
|
} else {
|
|
m, err = includePatternMatcher.MatchesOrParentMatches(candidate)
|
|
}
|
|
if err != nil {
|
|
return false, errors.Wrap(err, "failed to match includepatterns")
|
|
}
|
|
maybeIncludedPath.matchedIncludePattern = m
|
|
if !m {
|
|
return false, nil
|
|
}
|
|
}
|
|
|
|
if excludePatternMatcher != nil {
|
|
if parentDir != nil {
|
|
m, err = excludePatternMatcher.MatchesUsingParentResult(candidate, parentDir.matchedExcludePattern)
|
|
} else {
|
|
m, err = excludePatternMatcher.MatchesOrParentMatches(candidate)
|
|
}
|
|
if err != nil {
|
|
return false, errors.Wrap(err, "failed to match excludepatterns")
|
|
}
|
|
maybeIncludedPath.matchedExcludePattern = m
|
|
if m {
|
|
return false, nil
|
|
}
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
func wildcardPrefix(root *iradix.Node, p string) (string, []byte, bool, error) {
|
|
// For consistency with what the copy implementation in fsutil
|
|
// does: split pattern into non-wildcard prefix and rest of
|
|
// pattern, then follow symlinks when resolving the non-wildcard
|
|
// prefix.
|
|
|
|
d1, d2 := splitWildcards(p)
|
|
if d1 == "/" {
|
|
return "", nil, false, nil
|
|
}
|
|
|
|
linksWalked := 0
|
|
k, cr, err := getFollowLinksWalk(root, convertPathToKey([]byte(d1)), true, &linksWalked)
|
|
if err != nil {
|
|
return "", k, false, err
|
|
}
|
|
|
|
if d2 != "" && cr != nil && cr.Type == CacheRecordTypeSymlink {
|
|
// getFollowLinks only handles symlinks in path
|
|
// components before the last component, so
|
|
// handle last component in d1 specially.
|
|
resolved := string(convertKeyToPath(k))
|
|
for {
|
|
v, ok := root.Get(k)
|
|
|
|
if !ok {
|
|
return d1, k, false, nil
|
|
}
|
|
if v.(*CacheRecord).Type != CacheRecordTypeSymlink {
|
|
break
|
|
}
|
|
|
|
linksWalked++
|
|
if linksWalked > 255 {
|
|
return "", k, false, errors.Errorf("too many links")
|
|
}
|
|
|
|
resolved := cleanLink(resolved, v.(*CacheRecord).Linkname)
|
|
k = convertPathToKey([]byte(resolved))
|
|
}
|
|
}
|
|
return d1, k, cr != nil, nil
|
|
}
|
|
|
|
func splitWildcards(p string) (d1, d2 string) {
|
|
parts := strings.Split(path.Join(p), "/")
|
|
var p1, p2 []string
|
|
var found bool
|
|
for _, p := range parts {
|
|
if !found && containsWildcards(p) {
|
|
found = true
|
|
}
|
|
if p == "" {
|
|
p = "/"
|
|
}
|
|
if !found {
|
|
p1 = append(p1, p)
|
|
} else {
|
|
p2 = append(p2, p)
|
|
}
|
|
}
|
|
return filepath.Join(p1...), filepath.Join(p2...)
|
|
}
|
|
|
|
func containsWildcards(name string) bool {
|
|
for i := 0; i < len(name); i++ {
|
|
ch := name[i]
|
|
if ch == '\\' {
|
|
i++
|
|
} else if ch == '*' || ch == '?' || ch == '[' {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (cc *cacheContext) checksumNoFollow(ctx context.Context, m *mount, p string) (*CacheRecord, error) {
|
|
p = keyPath(p)
|
|
|
|
cc.mu.RLock()
|
|
if cc.txn == nil {
|
|
root := cc.tree.Root()
|
|
cc.mu.RUnlock()
|
|
v, ok := root.Get(convertPathToKey([]byte(p)))
|
|
if ok {
|
|
cr := v.(*CacheRecord)
|
|
if cr.Digest != "" {
|
|
return cr, nil
|
|
}
|
|
}
|
|
} else {
|
|
cc.mu.RUnlock()
|
|
}
|
|
|
|
cc.mu.Lock()
|
|
defer cc.mu.Unlock()
|
|
|
|
if cc.txn != nil {
|
|
cc.commitActiveTransaction()
|
|
}
|
|
|
|
defer func() {
|
|
if cc.dirty {
|
|
go cc.save()
|
|
cc.dirty = false
|
|
}
|
|
}()
|
|
|
|
return cc.lazyChecksum(ctx, m, p)
|
|
}
|
|
|
|
func (cc *cacheContext) commitActiveTransaction() {
|
|
for d := range cc.dirtyMap {
|
|
addParentToMap(d, cc.dirtyMap)
|
|
}
|
|
for d := range cc.dirtyMap {
|
|
k := convertPathToKey([]byte(d))
|
|
if _, ok := cc.txn.Get(k); ok {
|
|
cc.txn.Insert(k, &CacheRecord{Type: CacheRecordTypeDir})
|
|
}
|
|
}
|
|
cc.tree = cc.txn.Commit()
|
|
cc.node = nil
|
|
cc.dirtyMap = map[string]struct{}{}
|
|
cc.txn = nil
|
|
}
|
|
|
|
func (cc *cacheContext) lazyChecksum(ctx context.Context, m *mount, p string) (*CacheRecord, error) {
|
|
root := cc.tree.Root()
|
|
scan, err := cc.needsScan(root, p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if scan {
|
|
if err := cc.scanPath(ctx, m, p); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
k := convertPathToKey([]byte(p))
|
|
txn := cc.tree.Txn()
|
|
root = txn.Root()
|
|
cr, updated, err := cc.checksum(ctx, root, txn, m, k, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cc.tree = txn.Commit()
|
|
cc.dirty = updated
|
|
return cr, err
|
|
}
|
|
|
|
func (cc *cacheContext) checksum(ctx context.Context, root *iradix.Node, txn *iradix.Txn, m *mount, k []byte, follow bool) (*CacheRecord, bool, error) {
|
|
origk := k
|
|
k, cr, err := getFollowLinks(root, k, follow)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
if cr == nil {
|
|
return nil, false, errors.Wrapf(errNotFound, "%q", convertKeyToPath(origk))
|
|
}
|
|
if cr.Digest != "" {
|
|
return cr, false, nil
|
|
}
|
|
var dgst digest.Digest
|
|
|
|
switch cr.Type {
|
|
case CacheRecordTypeDir:
|
|
h := sha256.New()
|
|
next := append(k, 0)
|
|
iter := root.Iterator()
|
|
iter.SeekLowerBound(append(append([]byte{}, next...), 0))
|
|
subk := next
|
|
ok := true
|
|
for {
|
|
if !ok || !bytes.HasPrefix(subk, next) {
|
|
break
|
|
}
|
|
h.Write(bytes.TrimPrefix(subk, k))
|
|
|
|
subcr, _, err := cc.checksum(ctx, root, txn, m, subk, true)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
|
|
h.Write([]byte(subcr.Digest))
|
|
|
|
if subcr.Type == CacheRecordTypeDir { // skip subfiles
|
|
next := append(subk, 0, 0xff)
|
|
iter = root.Iterator()
|
|
iter.SeekLowerBound(next)
|
|
}
|
|
subk, _, ok = iter.Next()
|
|
}
|
|
dgst = digest.NewDigest(digest.SHA256, h)
|
|
|
|
default:
|
|
p := string(convertKeyToPath(bytes.TrimSuffix(k, []byte{0})))
|
|
|
|
target, err := m.mount(ctx)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
|
|
// no FollowSymlinkInScope because invalid paths should not be inserted
|
|
fp := filepath.Join(target, filepath.FromSlash(p))
|
|
|
|
fi, err := os.Lstat(fp)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
|
|
dgst, err = prepareDigest(fp, p, fi)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
}
|
|
|
|
cr2 := &CacheRecord{
|
|
Digest: dgst,
|
|
Type: cr.Type,
|
|
Linkname: cr.Linkname,
|
|
}
|
|
|
|
txn.Insert(k, cr2)
|
|
|
|
return cr2, true, nil
|
|
}
|
|
|
|
// needsScan returns false if path is in the tree or a parent path is in tree
|
|
// and subpath is missing
|
|
func (cc *cacheContext) needsScan(root *iradix.Node, p string) (bool, error) {
|
|
var linksWalked int
|
|
return cc.needsScanFollow(root, p, &linksWalked)
|
|
}
|
|
|
|
func (cc *cacheContext) needsScanFollow(root *iradix.Node, p string, linksWalked *int) (bool, error) {
|
|
if p == "/" {
|
|
p = ""
|
|
}
|
|
v, ok := root.Get(convertPathToKey([]byte(p)))
|
|
if !ok {
|
|
if p == "" {
|
|
return true, nil
|
|
}
|
|
return cc.needsScanFollow(root, path.Clean(path.Dir(p)), linksWalked)
|
|
}
|
|
cr := v.(*CacheRecord)
|
|
if cr.Type == CacheRecordTypeSymlink {
|
|
if *linksWalked > 255 {
|
|
return false, errTooManyLinks
|
|
}
|
|
*linksWalked++
|
|
link := path.Clean(cr.Linkname)
|
|
if !path.IsAbs(cr.Linkname) {
|
|
link = path.Join("/", path.Dir(p), link)
|
|
}
|
|
return cc.needsScanFollow(root, link, linksWalked)
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
func (cc *cacheContext) scanPath(ctx context.Context, m *mount, p string) (retErr error) {
|
|
p = path.Join("/", p)
|
|
d, _ := path.Split(p)
|
|
|
|
mp, err := m.mount(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
n := cc.tree.Root()
|
|
txn := cc.tree.Txn()
|
|
|
|
parentPath, err := rootPath(mp, filepath.FromSlash(d), func(p, link string) error {
|
|
cr := &CacheRecord{
|
|
Type: CacheRecordTypeSymlink,
|
|
Linkname: filepath.ToSlash(link),
|
|
}
|
|
k := []byte(filepath.Join("/", filepath.ToSlash(p)))
|
|
k = convertPathToKey(k)
|
|
txn.Insert(k, cr)
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = filepath.Walk(parentPath, func(path string, fi os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to walk %s", path)
|
|
}
|
|
rel, err := filepath.Rel(mp, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
k := []byte(filepath.Join("/", filepath.ToSlash(rel)))
|
|
if string(k) == "/" {
|
|
k = []byte{}
|
|
}
|
|
k = convertPathToKey(k)
|
|
if _, ok := n.Get(k); !ok {
|
|
cr := &CacheRecord{
|
|
Type: CacheRecordTypeFile,
|
|
}
|
|
if fi.Mode()&os.ModeSymlink != 0 {
|
|
cr.Type = CacheRecordTypeSymlink
|
|
link, err := os.Readlink(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cr.Linkname = filepath.ToSlash(link)
|
|
}
|
|
if fi.IsDir() {
|
|
cr.Type = CacheRecordTypeDirHeader
|
|
cr2 := &CacheRecord{
|
|
Type: CacheRecordTypeDir,
|
|
}
|
|
txn.Insert(k, cr2)
|
|
k = append(k, 0)
|
|
}
|
|
txn.Insert(k, cr)
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cc.tree = txn.Commit()
|
|
return nil
|
|
}
|
|
|
|
func getFollowLinks(root *iradix.Node, k []byte, follow bool) ([]byte, *CacheRecord, error) {
|
|
var linksWalked int
|
|
return getFollowLinksWalk(root, k, follow, &linksWalked)
|
|
}
|
|
|
|
func getFollowLinksWalk(root *iradix.Node, k []byte, follow bool, linksWalked *int) ([]byte, *CacheRecord, error) {
|
|
v, ok := root.Get(k)
|
|
if ok {
|
|
return k, v.(*CacheRecord), nil
|
|
}
|
|
if !follow || len(k) == 0 {
|
|
return k, nil, nil
|
|
}
|
|
|
|
dir, file := splitKey(k)
|
|
|
|
k, parent, err := getFollowLinksWalk(root, dir, follow, linksWalked)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if parent != nil {
|
|
if parent.Type == CacheRecordTypeSymlink {
|
|
*linksWalked++
|
|
if *linksWalked > 255 {
|
|
return nil, nil, errors.Errorf("too many links")
|
|
}
|
|
|
|
link := cleanLink(string(convertKeyToPath(dir)), parent.Linkname)
|
|
return getFollowLinksWalk(root, append(convertPathToKey([]byte(link)), file...), follow, linksWalked)
|
|
}
|
|
}
|
|
k = append(k, file...)
|
|
v, ok = root.Get(k)
|
|
if ok {
|
|
return k, v.(*CacheRecord), nil
|
|
}
|
|
return k, nil, nil
|
|
}
|
|
|
|
func cleanLink(dir, linkname string) string {
|
|
dirPath := path.Clean(dir)
|
|
if dirPath == "." || dirPath == "/" {
|
|
dirPath = ""
|
|
}
|
|
link := path.Clean(linkname)
|
|
if !path.IsAbs(link) {
|
|
return path.Join("/", path.Join(path.Dir(dirPath), link))
|
|
}
|
|
return link
|
|
}
|
|
|
|
func prepareDigest(fp, p string, fi os.FileInfo) (digest.Digest, error) {
|
|
h, err := NewFileHash(fp, fi)
|
|
if err != nil {
|
|
return "", errors.Wrapf(err, "failed to create hash for %s", p)
|
|
}
|
|
if fi.Mode().IsRegular() && fi.Size() > 0 {
|
|
// TODO: would be nice to put the contents to separate hash first
|
|
// so it can be cached for hardlinks
|
|
f, err := os.Open(fp)
|
|
if err != nil {
|
|
return "", errors.Wrapf(err, "failed to open %s", p)
|
|
}
|
|
defer f.Close()
|
|
if _, err := poolsCopy(h, f); err != nil {
|
|
return "", errors.Wrapf(err, "failed to copy file data for %s", p)
|
|
}
|
|
}
|
|
return digest.NewDigest(digest.SHA256, h), nil
|
|
}
|
|
|
|
func addParentToMap(d string, m map[string]struct{}) {
|
|
if d == "" {
|
|
return
|
|
}
|
|
d = path.Dir(d)
|
|
if d == "/" {
|
|
d = ""
|
|
}
|
|
m[d] = struct{}{}
|
|
addParentToMap(d, m)
|
|
}
|
|
|
|
func ensureOriginMetadata(md cache.RefMetadata) cache.RefMetadata {
|
|
em, ok := md.GetEqualMutable()
|
|
if !ok {
|
|
em = md
|
|
}
|
|
return em
|
|
}
|
|
|
|
var pool32K = sync.Pool{
|
|
New: func() interface{} {
|
|
buf := make([]byte, 32*1024) // 32K
|
|
return &buf
|
|
},
|
|
}
|
|
|
|
func poolsCopy(dst io.Writer, src io.Reader) (written int64, err error) {
|
|
buf := pool32K.Get().(*[]byte)
|
|
written, err = io.CopyBuffer(dst, src, *buf)
|
|
pool32K.Put(buf)
|
|
return
|
|
}
|
|
|
|
func convertPathToKey(p []byte) []byte {
|
|
return bytes.Replace([]byte(p), []byte("/"), []byte{0}, -1)
|
|
}
|
|
|
|
func convertKeyToPath(p []byte) []byte {
|
|
return bytes.Replace([]byte(p), []byte{0}, []byte("/"), -1)
|
|
}
|
|
|
|
func splitKey(k []byte) ([]byte, []byte) {
|
|
foundBytes := false
|
|
i := len(k) - 1
|
|
for {
|
|
if i <= 0 || foundBytes && k[i] == 0 {
|
|
break
|
|
}
|
|
if k[i] != 0 {
|
|
foundBytes = true
|
|
}
|
|
i--
|
|
}
|
|
return append([]byte{}, k[:i]...), k[i:]
|
|
}
|