1package main
2
3import (
4	"fmt"
5	"os"
6	"path/filepath"
7	"time"
8
9	"github.com/direnv/direnv/gzenv"
10)
11
12// FileTime represents a single recorded file status
13type FileTime struct {
14	Path    string
15	Modtime int64
16	Exists  bool
17}
18
19// FileTimes represent a record of all the known files and times
20type FileTimes struct {
21	list *[]FileTime
22}
23
24// NewFileTimes creates a new empty FileTimes
25func NewFileTimes() (times FileTimes) {
26	list := make([]FileTime, 0)
27	times.list = &list
28	return
29}
30
31// Update gets the latest stats on the path and updates the record.
32func (times *FileTimes) Update(path string) (err error) {
33	var modtime int64
34	var exists bool
35
36	stat, err := getLatestStat(path)
37	if os.IsNotExist(err) {
38		exists = false
39	} else {
40		exists = true
41		if err != nil {
42			return
43		}
44		modtime = stat.ModTime().Unix()
45	}
46
47	err = times.NewTime(path, modtime, exists)
48
49	return
50}
51
52// NewTime add the file on path, with modtime and exists flag to the list of known
53// files.
54func (times *FileTimes) NewTime(path string, modtime int64, exists bool) (err error) {
55	var time *FileTime
56
57	path, err = filepath.Abs(path)
58	if err != nil {
59		return
60	}
61
62	path = filepath.Clean(path)
63
64	for idx := range *(times.list) {
65		if (*times.list)[idx].Path == path {
66			time = &(*times.list)[idx]
67			break
68		}
69	}
70	if time == nil {
71		newTimes := append(*times.list, FileTime{Path: path})
72		times.list = &newTimes
73		time = &((*times.list)[len(*times.list)-1])
74	}
75
76	time.Modtime = modtime
77	time.Exists = exists
78
79	return
80}
81
82type checkFailed struct {
83	message string
84}
85
86func (err checkFailed) Error() string {
87	return err.message
88}
89
90// Check validates all the recorded file times
91func (times *FileTimes) Check() (err error) {
92	if len(*times.list) == 0 {
93		return checkFailed{"Times list is empty"}
94	}
95	for idx := range *times.list {
96		err = (*times.list)[idx].Check()
97		if err != nil {
98			return
99		}
100	}
101	return
102}
103
104// CheckOne compares notes between the given path and the recorded times
105func (times *FileTimes) CheckOne(path string) (err error) {
106	path, err = filepath.Abs(path)
107	if err != nil {
108		return
109	}
110	for idx := range *times.list {
111		if time := (*times.list)[idx]; time.Path == path {
112			err = time.Check()
113			return
114		}
115	}
116	return checkFailed{fmt.Sprintf("File %q is unknown", path)}
117}
118
119// Check verifies that the file is good and hasn't changed
120func (times FileTime) Check() (err error) {
121	stat, err := getLatestStat(times.Path)
122
123	switch {
124	case os.IsNotExist(err):
125		if times.Exists {
126			logDebug("Stat Check: %s: gone", times.Path)
127			return checkFailed{fmt.Sprintf("File %q is missing (Stat)", times.Path)}
128		}
129	case err != nil:
130		logDebug("Stat Check: %s: ERR: %v", times.Path, err)
131		return err
132	case !times.Exists:
133		logDebug("Check: %s: appeared", times.Path)
134		return checkFailed{fmt.Sprintf("File %q newly created", times.Path)}
135	case stat.ModTime().Unix() != times.Modtime:
136		logDebug("Check: %s: stale (stat: %v, lastcheck: %v)",
137			times.Path, stat.ModTime().Unix(), times.Modtime)
138		return checkFailed{fmt.Sprintf("File %q has changed", times.Path)}
139	}
140	logDebug("Check: %s: up to date", times.Path)
141	return nil
142}
143
144// Formatted shows the times in a user-friendly format.
145func (times *FileTime) Formatted(relDir string) string {
146	timeBytes, err := time.Unix(times.Modtime, 0).MarshalText()
147	if err != nil {
148		timeBytes = []byte("<<???>>")
149	}
150	path, err := filepath.Rel(relDir, times.Path)
151	if err != nil {
152		path = times.Path
153	}
154	return fmt.Sprintf("%q - %s", path, timeBytes)
155}
156
157// Marshal dumps the times into gzenv format
158func (times *FileTimes) Marshal() string {
159	return gzenv.Marshal(*times.list)
160}
161
162// Unmarshal loads the watches back from gzenv
163func (times *FileTimes) Unmarshal(from string) error {
164	return gzenv.Unmarshal(from, times.list)
165}
166
167func getLatestStat(path string) (os.FileInfo, error) {
168	var lstatModTime int64
169	var statModTime int64
170
171	// Check the examine-a-symlink case first:
172	lstat, err := os.Lstat(path)
173	if err != nil {
174		logDebug("getLatestStat,Lstat: %s: error: %v", path, err)
175		return nil, err
176	}
177	lstatModTime = lstat.ModTime().Unix()
178
179	stat, err := os.Stat(path)
180	if err != nil {
181		logDebug("getLatestStat,Stat: %s: error: %v (Lstat time: %v)",
182			path, err, lstatModTime)
183		return nil, err
184	}
185	statModTime = stat.ModTime().Unix()
186
187	if lstatModTime > statModTime {
188		logDebug("getLatestStat: %s: Lstat: %v, Stat: %v -> preferring Lstat",
189			path, lstatModTime, statModTime)
190		return lstat, nil
191	}
192	logDebug("getLatestStat: %s: Lstat: %v, Stat: %v -> preferring Stat",
193		path, lstatModTime, statModTime)
194	return stat, nil
195}
196