1package dependency
2
3import (
4	"fmt"
5	"io/ioutil"
6	"log"
7	"os"
8	"strings"
9	"time"
10
11	"github.com/pkg/errors"
12)
13
14var (
15	// Ensure implements
16	_ Dependency = (*FileQuery)(nil)
17
18	// FileQuerySleepTime is the amount of time to sleep between queries, since
19	// the fsnotify library is not compatible with solaris and other OSes yet.
20	FileQuerySleepTime = 2 * time.Second
21)
22
23// FileQuery represents a local file dependency.
24type FileQuery struct {
25	stopCh chan struct{}
26
27	path string
28	stat os.FileInfo
29}
30
31// NewFileQuery creates a file dependency from the given path.
32func NewFileQuery(s string) (*FileQuery, error) {
33	s = strings.TrimSpace(s)
34	if s == "" {
35		return nil, fmt.Errorf("file: invalid format: %q", s)
36	}
37
38	return &FileQuery{
39		stopCh: make(chan struct{}, 1),
40		path:   s,
41	}, nil
42}
43
44// Fetch retrieves this dependency and returns the result or any errors that
45// occur in the process.
46func (d *FileQuery) Fetch(clients *ClientSet, opts *QueryOptions) (interface{}, *ResponseMetadata, error) {
47	log.Printf("[TRACE] %s: READ %s", d, d.path)
48
49	select {
50	case <-d.stopCh:
51		log.Printf("[TRACE] %s: stopped", d)
52		return "", nil, ErrStopped
53	case r := <-d.watch(d.stat):
54		if r.err != nil {
55			return "", nil, errors.Wrap(r.err, d.String())
56		}
57
58		log.Printf("[TRACE] %s: reported change", d)
59
60		data, err := ioutil.ReadFile(d.path)
61		if err != nil {
62			return "", nil, errors.Wrap(err, d.String())
63		}
64
65		d.stat = r.stat
66		return respWithMetadata(string(data))
67	}
68}
69
70// CanShare returns a boolean if this dependency is shareable.
71func (d *FileQuery) CanShare() bool {
72	return false
73}
74
75// Stop halts the dependency's fetch function.
76func (d *FileQuery) Stop() {
77	close(d.stopCh)
78}
79
80// String returns the human-friendly version of this dependency.
81func (d *FileQuery) String() string {
82	return fmt.Sprintf("file(%s)", d.path)
83}
84
85// Type returns the type of this dependency.
86func (d *FileQuery) Type() Type {
87	return TypeLocal
88}
89
90type watchResult struct {
91	stat os.FileInfo
92	err  error
93}
94
95// watch watchers the file for changes
96func (d *FileQuery) watch(lastStat os.FileInfo) <-chan *watchResult {
97	ch := make(chan *watchResult, 1)
98
99	go func(lastStat os.FileInfo) {
100		for {
101			stat, err := os.Stat(d.path)
102			if err != nil {
103				select {
104				case <-d.stopCh:
105					return
106				case ch <- &watchResult{err: err}:
107					return
108				}
109			}
110
111			changed := lastStat == nil ||
112				lastStat.Size() != stat.Size() ||
113				lastStat.ModTime() != stat.ModTime()
114
115			if changed {
116				select {
117				case <-d.stopCh:
118					return
119				case ch <- &watchResult{stat: stat}:
120					return
121				}
122			}
123
124			time.Sleep(FileQuerySleepTime)
125		}
126	}(lastStat)
127
128	return ch
129}
130