github.com/Racer159/jackal@v0.32.7-0.20240401174413-0bd2339e4f2e/src/pkg/utils/io.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // SPDX-FileCopyrightText: 2021-Present The Jackal Authors
     3  
     4  // Package utils provides generic helper functions.
     5  package utils
     6  
     7  import (
     8  	"crypto/sha256"
     9  	"encoding/json"
    10  	"fmt"
    11  	"io"
    12  	"os"
    13  	"path/filepath"
    14  
    15  	"github.com/Racer159/jackal/src/config"
    16  	"github.com/Racer159/jackal/src/pkg/message"
    17  	"github.com/Racer159/jackal/src/types"
    18  	"github.com/defenseunicorns/pkg/helpers"
    19  )
    20  
    21  const (
    22  	tmpPathPrefix = "jackal-"
    23  )
    24  
    25  // MakeTempDir creates a temp directory with the jackal- prefix.
    26  func MakeTempDir(basePath string) (string, error) {
    27  	if basePath != "" {
    28  		if err := helpers.CreateDirectory(basePath, helpers.ReadWriteExecuteUser); err != nil {
    29  			return "", err
    30  		}
    31  	}
    32  
    33  	tmp, err := os.MkdirTemp(basePath, tmpPathPrefix)
    34  	if err != nil {
    35  		return "", err
    36  	}
    37  
    38  	message.Debug("Using temporary directory:", tmp)
    39  
    40  	return tmp, nil
    41  }
    42  
    43  // GetFinalExecutablePath returns the absolute path to the current executable, following any symlinks along the way.
    44  func GetFinalExecutablePath() (string, error) {
    45  	message.Debug("utils.GetExecutablePath()")
    46  
    47  	binaryPath, err := os.Executable()
    48  	if err != nil {
    49  		return "", err
    50  	}
    51  
    52  	// In case the binary is symlinked somewhere else, get the final destination
    53  	linkedPath, err := filepath.EvalSymlinks(binaryPath)
    54  	return linkedPath, err
    55  }
    56  
    57  // GetFinalExecutableCommand returns the final path to the Jackal executable including and library prefixes and overrides.
    58  func GetFinalExecutableCommand() (string, error) {
    59  	// In case the binary is symlinked somewhere else, get the final destination
    60  	jackalCommand, err := GetFinalExecutablePath()
    61  	if err != nil {
    62  		return jackalCommand, err
    63  	}
    64  
    65  	if config.ActionsCommandJackalPrefix != "" {
    66  		jackalCommand = fmt.Sprintf("%s %s", jackalCommand, config.ActionsCommandJackalPrefix)
    67  	}
    68  
    69  	// If a library user has chosen to override config to use system Jackal instead, reset the binary path.
    70  	if config.ActionsUseSystemJackal {
    71  		jackalCommand = "jackal"
    72  	}
    73  
    74  	return jackalCommand, err
    75  }
    76  
    77  // SplitFile will take a srcFile path and split it into files based on chunkSizeBytes
    78  // the first file will be a metadata file containing:
    79  // - sha256sum of the original file
    80  // - number of bytes in the original file
    81  // - number of files the srcFile was split into
    82  // SplitFile will delete the original file
    83  //
    84  // Returns:
    85  // - fileNames: list of file paths srcFile was split across
    86  // - sha256sum: sha256sum of the srcFile before splitting
    87  // - err: any errors encountered
    88  func SplitFile(srcPath string, chunkSizeBytes int) (err error) {
    89  	var fileNames []string
    90  	var sha256sum string
    91  	hash := sha256.New()
    92  
    93  	// Set buffer size to some multiple of 4096 KiB for modern file system cluster sizes
    94  	bufferSize := 16 * 1024 * 1024 // 16 MiB
    95  	// if chunkSizeBytes is less than bufferSize, use chunkSizeBytes as bufferSize for simplicity
    96  	if chunkSizeBytes < bufferSize {
    97  		bufferSize = chunkSizeBytes
    98  	}
    99  	buf := make([]byte, bufferSize)
   100  
   101  	// get file size
   102  	fi, err := os.Stat(srcPath)
   103  	if err != nil {
   104  		return err
   105  	}
   106  	fileSize := fi.Size()
   107  
   108  	// start progress bar
   109  	title := fmt.Sprintf("[0/%d] MB bytes written", fileSize/1000/1000)
   110  	progressBar := message.NewProgressBar(fileSize, title)
   111  	defer progressBar.Stop()
   112  
   113  	// open srcFile
   114  	srcFile, err := os.Open(srcPath)
   115  	if err != nil {
   116  		return err
   117  	}
   118  	defer srcFile.Close()
   119  
   120  	// create file path starting from part 001
   121  	path := fmt.Sprintf("%s.part001", srcPath)
   122  	chunkFile, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, helpers.ReadAllWriteUser)
   123  	if err != nil {
   124  		return err
   125  	}
   126  	fileNames = append(fileNames, path)
   127  	defer chunkFile.Close()
   128  
   129  	// setup counter for tracking how many bytes are left to write to file
   130  	chunkBytesRemaining := chunkSizeBytes
   131  	// Loop over the tarball hashing as we go and breaking it into chunks based on the chunkSizeBytes
   132  	for {
   133  		bytesRead, err := srcFile.Read(buf)
   134  
   135  		if err != nil {
   136  			if err == io.EOF {
   137  				// At end of file, break out of loop
   138  				break
   139  			}
   140  			return err
   141  		}
   142  
   143  		// Pass data to hash
   144  		hash.Write(buf[0:bytesRead])
   145  
   146  		// handle if we should split the data between two chunks
   147  		if chunkBytesRemaining < bytesRead {
   148  			// write the remaining chunk size to file
   149  			_, err := chunkFile.Write(buf[0:chunkBytesRemaining])
   150  			if err != nil {
   151  				return err
   152  			}
   153  			err = chunkFile.Close()
   154  			if err != nil {
   155  				return err
   156  			}
   157  
   158  			// create new file
   159  			path = fmt.Sprintf("%s.part%03d", srcPath, len(fileNames)+1)
   160  			chunkFile, err = os.OpenFile(path, os.O_CREATE|os.O_WRONLY, helpers.ReadAllWriteUser)
   161  			if err != nil {
   162  				return err
   163  			}
   164  			fileNames = append(fileNames, path)
   165  			defer chunkFile.Close()
   166  
   167  			// write to new file where we left off
   168  			_, err = chunkFile.Write(buf[chunkBytesRemaining:bytesRead])
   169  			if err != nil {
   170  				return err
   171  			}
   172  
   173  			// set chunkBytesRemaining considering how many bytes are already written to new file
   174  			chunkBytesRemaining = chunkSizeBytes - (bufferSize - chunkBytesRemaining)
   175  		} else {
   176  			_, err := chunkFile.Write(buf[0:bytesRead])
   177  			if err != nil {
   178  				return err
   179  			}
   180  			chunkBytesRemaining = chunkBytesRemaining - bytesRead
   181  		}
   182  
   183  		// update progress bar
   184  		progressBar.Add(bufferSize)
   185  		title := fmt.Sprintf("[%d/%d] MB bytes written", progressBar.GetCurrent()/1000/1000, fileSize/1000/1000)
   186  		progressBar.UpdateTitle(title)
   187  	}
   188  	srcFile.Close()
   189  	_ = os.RemoveAll(srcPath)
   190  
   191  	// calculate sha256 sum
   192  	sha256sum = fmt.Sprintf("%x", hash.Sum(nil))
   193  
   194  	// Marshal the data into a json file.
   195  	jsonData, err := json.Marshal(types.JackalSplitPackageData{
   196  		Count:     len(fileNames),
   197  		Bytes:     fileSize,
   198  		Sha256Sum: sha256sum,
   199  	})
   200  	if err != nil {
   201  		return fmt.Errorf("unable to marshal the split package data: %w", err)
   202  	}
   203  
   204  	// write header file
   205  	path = fmt.Sprintf("%s.part000", srcPath)
   206  	if err := os.WriteFile(path, jsonData, helpers.ReadAllWriteUser); err != nil {
   207  		return fmt.Errorf("unable to write the file %s: %w", path, err)
   208  	}
   209  	fileNames = append(fileNames, path)
   210  	progressBar.Successf("Package split across %d files", len(fileNames))
   211  
   212  	return nil
   213  }