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  }