github.com/oNaiPs/go-generate-fast@v0.3.0/src/core/cache/cache.go (about) 1 package cache 2 3 import ( 4 "errors" 5 "fmt" 6 "os" 7 "path" 8 "strings" 9 "time" 10 11 "github.com/bmatcuk/doublestar/v4" 12 "github.com/oNaiPs/go-generate-fast/src/core/config" 13 "github.com/oNaiPs/go-generate-fast/src/plugins" 14 "github.com/oNaiPs/go-generate-fast/src/utils/copy" 15 "github.com/oNaiPs/go-generate-fast/src/utils/fs" 16 "github.com/oNaiPs/go-generate-fast/src/utils/hash" 17 "github.com/oNaiPs/go-generate-fast/src/utils/str" 18 "go.uber.org/zap" 19 ) 20 21 type VerifyResult struct { 22 PluginMatch *plugins.Plugin 23 CacheHit bool 24 CacheHitDir string 25 CanSave bool 26 IoFiles plugins.InputOutputFiles 27 } 28 29 func Verify(opts plugins.GenerateOpts) (VerifyResult, error) { 30 zap.S().Debugf("%s: verifying cache for \"%s\"", opts.Path, opts.Command()) 31 32 verifyResult := VerifyResult{} 33 var ioFiles *plugins.InputOutputFiles 34 35 plugin := plugins.MatchPlugin(opts) 36 if plugin != nil { 37 verifyResult.PluginMatch = &plugin 38 39 zap.S().Debugf("Using plugin \"%s\"", plugin.Name()) 40 41 ioFiles = plugin.ComputeInputOutputFiles(opts) 42 if ioFiles == nil { 43 zap.S().Debugf("No input output files, skipping cache.") 44 return verifyResult, nil 45 } 46 } else { 47 zap.S().Debugf("No plugin was found to handle command.") 48 ioFiles = &plugins.InputOutputFiles{} 49 50 if len(opts.ExtraInputPatterns) == 0 || len(opts.ExtraOutputPatterns) == 0 { 51 return verifyResult, nil 52 } 53 } 54 55 for _, globPattern := range opts.ExtraInputPatterns { 56 matches, err := doublestar.FilepathGlob(globPattern) 57 if err != nil { 58 zap.S().Error("cannot get extra input files: ", err) 59 continue 60 } 61 ioFiles.InputFiles = append(ioFiles.InputFiles, matches...) 62 } 63 64 ioFiles.OutputPatterns = append(ioFiles.OutputPatterns, opts.ExtraOutputPatterns...) 65 66 str.RemoveDuplicatesAndSort(&ioFiles.InputFiles) 67 str.RemoveDuplicatesAndSort(&ioFiles.OutputFiles) 68 69 _ = str.ConvertToRelativePaths(&ioFiles.InputFiles, opts.Dir()) 70 _ = str.ConvertToRelativePaths(&ioFiles.OutputFiles, opts.Dir()) 71 72 zap.S().Debugf("Got %d input files: %s", len(ioFiles.InputFiles), strings.Join(ioFiles.InputFiles, ", ")) 73 zap.S().Debugf("Got %d output files: %s", len(ioFiles.OutputFiles), strings.Join(ioFiles.OutputFiles, ", ")) 74 zap.S().Debugf("Got %d output globs: %s", len(ioFiles.OutputPatterns), strings.Join(ioFiles.OutputPatterns, ", ")) 75 76 cacheHitDir, err := calculateCacheDirectoryFromInputData(opts, *ioFiles) 77 if err != nil { 78 zap.S().Debugf("Cannot get cache hit dir: %s", err) 79 return verifyResult, err 80 } 81 82 verifyResult.IoFiles = *ioFiles 83 verifyResult.CacheHitDir = cacheHitDir 84 zap.S().Debugf("Cache hit dir: %s", cacheHitDir) 85 86 fileInfo, err := os.Stat(cacheHitDir) 87 if os.IsNotExist(err) { 88 zap.S().Debugf("Cache hit dir not found: %s", cacheHitDir) 89 } else if os.IsPermission(err) { 90 zap.S().Debugf("Cache hit dir permission denied: %s", cacheHitDir) 91 } else if err != nil { 92 return VerifyResult{}, fmt.Errorf("cannot get cache dir info: %w", err) 93 } 94 95 verifyResult.CacheHit = fileInfo != nil && fileInfo.IsDir() 96 verifyResult.CanSave = true 97 98 return verifyResult, nil 99 } 100 101 func Save(result VerifyResult) error { 102 outputFiles := result.IoFiles.OutputFiles 103 for _, globPattern := range result.IoFiles.OutputPatterns { 104 matches, err := doublestar.FilepathGlob(globPattern, doublestar.WithFilesOnly()) 105 if err != nil { 106 zap.S().Error("cannot extra output files: ", err) 107 continue 108 } 109 outputFiles = append(outputFiles, matches...) 110 } 111 112 err := os.MkdirAll(result.CacheHitDir, 0700) 113 if err != nil { 114 return fmt.Errorf("cannot create cache dir: %w", err) 115 } 116 117 cacheConfig := CacheConfig{} 118 119 //use an intermediary file since we don't know the file hash until we finish copying it 120 tmpFile := path.Join(result.CacheHitDir, "file.swp") 121 122 for _, file := range outputFiles { 123 if err != nil { 124 return fmt.Errorf("cannot create temp dir: %w", err) 125 } 126 127 hash, err := copy.CopyHashFile(file, tmpFile) 128 if err != nil { 129 return fmt.Errorf("cannot copy file to cache: %w", err) 130 } 131 132 err = os.Rename(tmpFile, path.Join(result.CacheHitDir, hash)) 133 if err != nil { 134 return fmt.Errorf("rename file to be cached: %w", err) 135 } 136 137 fileStat, err := os.Stat(file) 138 if err != nil { 139 return fmt.Errorf("cannot stat cached file: %w", err) 140 } 141 142 cacheConfig.OutputFiles = append(cacheConfig.OutputFiles, CacheConfigOutputFileInfo{ 143 Hash: hash, 144 Path: file, 145 ModTime: fileStat.ModTime(), 146 }) 147 } 148 149 err = SaveConfig(cacheConfig, result.CacheHitDir) 150 if err != nil { 151 return fmt.Errorf("cannot write cache config: %w", err) 152 } 153 154 zap.S().Debug("Saved cache on ", result.CacheHitDir) 155 156 return nil 157 } 158 159 func Restore(result VerifyResult) error { 160 zap.S().Debugf("Restoring cache") 161 162 cacheConfig, err := LoadConfig(result.CacheHitDir) 163 if err != nil { 164 return fmt.Errorf("cannot read cache config: %w", err) 165 } 166 167 // confirm that the expected output files match the ones in the saved cache config 168 // we can only do this when there are no globs defined 169 // TODO: check if the non-matching output files match the provided glob 170 if len(result.IoFiles.OutputPatterns) == 0 && 171 !areOutputsMatching(cacheConfig.OutputFiles, result.IoFiles.OutputFiles) { 172 return errors.New("expected output files differ") 173 } 174 175 for _, dstFile := range cacheConfig.OutputFiles { 176 srcFile := path.Join(result.CacheHitDir, dstFile.Hash) 177 178 // skip if modification time is the same 179 dstFileStat, err := os.Stat(dstFile.Path) 180 if err == nil && dstFileStat.ModTime() == dstFile.ModTime { 181 zap.S().Debug("Skipping copy of file with same modtime: ", dstFile.Path) 182 continue 183 } 184 185 err = os.MkdirAll(path.Dir(dstFile.Path), 0755) 186 if err != nil { 187 return fmt.Errorf("cannot create destination directory: %w", err) 188 } 189 190 hash, err := copy.CopyHashFile(srcFile, dstFile.Path) 191 if err != nil { 192 return fmt.Errorf("cannot copy file from cache: %w", err) 193 } 194 zap.S().Debug("Copied file from cache: ", dstFile.Path) 195 196 err = os.Chtimes(dstFile.Path, dstFile.ModTime, dstFile.ModTime) 197 if err != nil { 198 return fmt.Errorf("cannot restore times for destination file: %w", err) 199 } 200 201 if hash != dstFile.Hash { 202 return errors.New("file hash is different, corruption") 203 } 204 } 205 206 return nil 207 } 208 209 func areOutputsMatching(outputFiles []CacheConfigOutputFileInfo, resultFiles []string) bool { 210 // Create a map for faster lookup. 211 resultFileMap := make(map[string]bool) 212 for _, file := range resultFiles { 213 resultFileMap[file] = true 214 } 215 216 // Check if each value in outputFiles is present in the resultFileMap. 217 for _, value := range outputFiles { 218 if !resultFileMap[value.Path] { 219 return false 220 } 221 } 222 223 return true 224 } 225 226 func calculateCacheDirectoryFromInputData(opts plugins.GenerateOpts, ioFiles plugins.InputOutputFiles) (string, error) { 227 contentToHash := 228 opts.Dir() + 229 strings.Join(opts.Words, "\n") + 230 strings.Join(ioFiles.InputFiles, "\n") + 231 strings.Join(ioFiles.OutputFiles, "\n") + 232 strings.Join(ioFiles.OutputPatterns, "\n") + 233 strings.Join(ioFiles.Extra, "\n") 234 235 for _, file := range ioFiles.InputFiles { 236 hash, err := hash.HashFile(file) 237 if err != nil { 238 return "", fmt.Errorf("cannot hash file '%s': %w", file, err) 239 } 240 contentToHash += hash 241 } 242 243 if opts.GoPackage == "" { 244 execInfo, err := getExecutableDetails(opts.ExecutableName) 245 if err != nil { 246 return "", fmt.Errorf("cannot get path for executable '%s': %s", opts.ExecutableName, err) 247 } 248 contentToHash += execInfo 249 } else { 250 // we can only hash specific versions/hashes 251 if opts.GoPackageVersion != "" && opts.GoPackageVersion != "latest" { 252 hash, err := hash.HashString(opts.GoPackage + "/" + opts.GoPackageVersion) 253 if err != nil { 254 return "", fmt.Errorf("cannot hash string: %w", err) 255 } 256 contentToHash += hash 257 } 258 } 259 260 finalHash, err := hash.HashString(contentToHash) 261 if err != nil { 262 return "", fmt.Errorf("cannot get final hash: %s", err) 263 } 264 265 cacheHitDir := path.Join( 266 config.Get().CacheDir, 267 finalHash[0:1], 268 finalHash[1:3], 269 finalHash[3:]) 270 271 return cacheHitDir, nil 272 } 273 274 func getExecutableDetails(ExecutablePath string) (string, error) { 275 ExecutablePath, err := fs.FindExecutablePath(ExecutablePath) 276 if err != nil { 277 return "", err 278 } 279 280 info, err := os.Stat(ExecutablePath) 281 if err != nil { 282 return "", err 283 } 284 285 execInfo := fmt.Sprint( 286 ExecutablePath, 287 fmt.Sprintf("%019d", info.Size()), 288 info.ModTime().Format(time.RFC3339)) 289 290 zap.S().Debugf("Exec info %s", execInfo) 291 292 return execInfo, nil 293 }