github.com/paketo-buildpacks/packit@v1.3.2-0.20211206231111-86b75c657449/fs/checksum_calculator.go (about)

     1  package fs
     2  
     3  import (
     4  	"crypto/sha256"
     5  	"encoding/hex"
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"path/filepath"
    10  	"runtime"
    11  	"sort"
    12  )
    13  
    14  // ChecksumCalculator can be used to calculate the SHA256 checksum of a given file or
    15  // directory. When given a directory, checksum calculation will be performed in
    16  // parallel.
    17  type ChecksumCalculator struct{}
    18  
    19  // NewChecksumCalculator returns a new instance of a ChecksumCalculator.
    20  func NewChecksumCalculator() ChecksumCalculator {
    21  	return ChecksumCalculator{}
    22  }
    23  
    24  type calculatedFile struct {
    25  	path     string
    26  	checksum []byte
    27  	err      error
    28  }
    29  
    30  // Sum returns a hex-encoded SHA256 checksum value of a file or directory given a path.
    31  func (c ChecksumCalculator) Sum(paths ...string) (string, error) {
    32  	var files []string
    33  	for _, path := range paths {
    34  		err := filepath.Walk(path, func(path string, info os.FileInfo, err error) error {
    35  			if err != nil {
    36  				return err
    37  			}
    38  
    39  			if info.Mode().IsRegular() {
    40  				files = append(files, path)
    41  			}
    42  
    43  			return nil
    44  		})
    45  		if err != nil {
    46  			return "", fmt.Errorf("failed to calculate checksum: %w", err)
    47  		}
    48  	}
    49  
    50  	//Gather all checksums
    51  	var sums [][]byte
    52  	for _, f := range getParallelChecksums(files) {
    53  		if f.err != nil {
    54  			return "", fmt.Errorf("failed to calculate checksum: %w", f.err)
    55  		}
    56  
    57  		sums = append(sums, f.checksum)
    58  	}
    59  
    60  	if len(sums) == 1 {
    61  		return hex.EncodeToString(sums[0]), nil
    62  	}
    63  
    64  	hash := sha256.New()
    65  	for _, sum := range sums {
    66  		_, err := hash.Write(sum)
    67  		if err != nil {
    68  			return "", fmt.Errorf("failed to calculate checksum: %w", err)
    69  		}
    70  	}
    71  	return hex.EncodeToString(hash.Sum(nil)), nil
    72  }
    73  
    74  func getParallelChecksums(filesFromDir []string) []calculatedFile {
    75  	var checksumResults []calculatedFile
    76  	numFiles := len(filesFromDir)
    77  	files := make(chan string, numFiles)
    78  	calculatedFiles := make(chan calculatedFile, numFiles)
    79  
    80  	//Spawns workers
    81  	for i := 0; i < runtime.NumCPU(); i++ {
    82  		go fileChecksumer(files, calculatedFiles)
    83  	}
    84  
    85  	//Puts files in worker queue
    86  	for _, f := range filesFromDir {
    87  		files <- f
    88  	}
    89  
    90  	close(files)
    91  
    92  	//Pull all calculated files off of result queue
    93  	for i := 0; i < numFiles; i++ {
    94  		checksumResults = append(checksumResults, <-calculatedFiles)
    95  	}
    96  
    97  	//Sort calculated files for consistent checksuming
    98  	sort.Slice(checksumResults, func(i, j int) bool {
    99  		return checksumResults[i].path < checksumResults[j].path
   100  	})
   101  
   102  	return checksumResults
   103  }
   104  
   105  func fileChecksumer(files chan string, calculatedFiles chan calculatedFile) {
   106  	for path := range files {
   107  		result := calculatedFile{path: path}
   108  
   109  		file, err := os.Open(path)
   110  		if err != nil {
   111  			result.err = err
   112  			calculatedFiles <- result
   113  			continue
   114  		}
   115  
   116  		hash := sha256.New()
   117  		_, err = io.Copy(hash, file)
   118  		if err != nil {
   119  			result.err = err
   120  			calculatedFiles <- result
   121  			continue
   122  		}
   123  
   124  		if err := file.Close(); err != nil {
   125  			result.err = err
   126  			calculatedFiles <- result
   127  			continue
   128  		}
   129  
   130  		result.checksum = hash.Sum(nil)
   131  		calculatedFiles <- result
   132  	}
   133  }