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