1package remotecontext // import "github.com/docker/docker/builder/remotecontext"
2
3import (
4	"os"
5	"sync"
6
7	"github.com/docker/docker/pkg/containerfs"
8	iradix "github.com/hashicorp/go-immutable-radix"
9	"github.com/opencontainers/go-digest"
10	"github.com/pkg/errors"
11	"github.com/tonistiigi/fsutil"
12)
13
14type hashed interface {
15	Digest() digest.Digest
16}
17
18// CachableSource is a source that contains cache records for its contents
19type CachableSource struct {
20	mu   sync.Mutex
21	root containerfs.ContainerFS
22	tree *iradix.Tree
23	txn  *iradix.Txn
24}
25
26// NewCachableSource creates new CachableSource
27func NewCachableSource(root string) *CachableSource {
28	ts := &CachableSource{
29		tree: iradix.New(),
30		root: containerfs.NewLocalContainerFS(root),
31	}
32	return ts
33}
34
35// MarshalBinary marshals current cache information to a byte array
36func (cs *CachableSource) MarshalBinary() ([]byte, error) {
37	b := TarsumBackup{Hashes: make(map[string]string)}
38	root := cs.getRoot()
39	root.Walk(func(k []byte, v interface{}) bool {
40		b.Hashes[string(k)] = v.(*fileInfo).sum
41		return false
42	})
43	return b.Marshal()
44}
45
46// UnmarshalBinary decodes cache information for presented byte array
47func (cs *CachableSource) UnmarshalBinary(data []byte) error {
48	var b TarsumBackup
49	if err := b.Unmarshal(data); err != nil {
50		return err
51	}
52	txn := iradix.New().Txn()
53	for p, v := range b.Hashes {
54		txn.Insert([]byte(p), &fileInfo{sum: v})
55	}
56	cs.mu.Lock()
57	defer cs.mu.Unlock()
58	cs.tree = txn.Commit()
59	return nil
60}
61
62// Scan rescans the cache information from the file system
63func (cs *CachableSource) Scan() error {
64	lc, err := NewLazySource(cs.root)
65	if err != nil {
66		return err
67	}
68	txn := iradix.New().Txn()
69	err = cs.root.Walk(cs.root.Path(), func(path string, info os.FileInfo, err error) error {
70		if err != nil {
71			return errors.Wrapf(err, "failed to walk %s", path)
72		}
73		rel, err := Rel(cs.root, path)
74		if err != nil {
75			return err
76		}
77		h, err := lc.Hash(rel)
78		if err != nil {
79			return err
80		}
81		txn.Insert([]byte(rel), &fileInfo{sum: h})
82		return nil
83	})
84	if err != nil {
85		return err
86	}
87	cs.mu.Lock()
88	defer cs.mu.Unlock()
89	cs.tree = txn.Commit()
90	return nil
91}
92
93// HandleChange notifies the source about a modification operation
94func (cs *CachableSource) HandleChange(kind fsutil.ChangeKind, p string, fi os.FileInfo, err error) (retErr error) {
95	cs.mu.Lock()
96	if cs.txn == nil {
97		cs.txn = cs.tree.Txn()
98	}
99	if kind == fsutil.ChangeKindDelete {
100		cs.txn.Delete([]byte(p))
101		cs.mu.Unlock()
102		return
103	}
104
105	h, ok := fi.(hashed)
106	if !ok {
107		cs.mu.Unlock()
108		return errors.Errorf("invalid fileinfo: %s", p)
109	}
110
111	hfi := &fileInfo{
112		sum: h.Digest().Hex(),
113	}
114	cs.txn.Insert([]byte(p), hfi)
115	cs.mu.Unlock()
116	return nil
117}
118
119func (cs *CachableSource) getRoot() *iradix.Node {
120	cs.mu.Lock()
121	if cs.txn != nil {
122		cs.tree = cs.txn.Commit()
123		cs.txn = nil
124	}
125	t := cs.tree
126	cs.mu.Unlock()
127	return t.Root()
128}
129
130// Close closes the source
131func (cs *CachableSource) Close() error {
132	return nil
133}
134
135// Hash returns a hash for a single file in the source
136func (cs *CachableSource) Hash(path string) (string, error) {
137	n := cs.getRoot()
138	// TODO: check this for symlinks
139	v, ok := n.Get([]byte(path))
140	if !ok {
141		return path, nil
142	}
143	return v.(*fileInfo).sum, nil
144}
145
146// Root returns a root directory for the source
147func (cs *CachableSource) Root() containerfs.ContainerFS {
148	return cs.root
149}
150
151type fileInfo struct {
152	sum string
153}
154
155func (fi *fileInfo) Hash() string {
156	return fi.sum
157}
158