github.com/ouraigua/jenkins-library@v0.0.0-20231028010029-fbeaf2f3aa9b/pkg/piperutils/fileUtils.go (about)

     1  package piperutils
     2  
     3  import (
     4  	"archive/tar"
     5  	"archive/zip"
     6  	"compress/gzip"
     7  	"crypto/sha256"
     8  	"errors"
     9  	"fmt"
    10  	"io"
    11  	"io/fs"
    12  	"os"
    13  	"path/filepath"
    14  	"strings"
    15  	"time"
    16  
    17  	"github.com/bmatcuk/doublestar"
    18  )
    19  
    20  // FileUtils ...
    21  type FileUtils interface {
    22  	Abs(path string) (string, error)
    23  	DirExists(path string) (bool, error)
    24  	FileExists(filename string) (bool, error)
    25  	Copy(src, dest string) (int64, error)
    26  	Move(src, dest string) error
    27  	FileRead(path string) ([]byte, error)
    28  	ReadFile(path string) ([]byte, error)
    29  	FileWrite(path string, content []byte, perm os.FileMode) error
    30  	WriteFile(path string, content []byte, perm os.FileMode) error
    31  	FileRemove(path string) error
    32  	MkdirAll(path string, perm os.FileMode) error
    33  	Chmod(path string, mode os.FileMode) error
    34  	Glob(pattern string) (matches []string, err error)
    35  	Chdir(path string) error
    36  	TempDir(string, string) (string, error)
    37  	RemoveAll(string) error
    38  	FileRename(string, string) error
    39  	Getwd() (string, error)
    40  	Symlink(oldname string, newname string) error
    41  	SHA256(path string) (string, error)
    42  	CurrentTime(format string) string
    43  	Open(name string) (io.ReadWriteCloser, error)
    44  	Create(name string) (io.ReadWriteCloser, error)
    45  }
    46  
    47  // Files ...
    48  type Files struct{}
    49  
    50  // TempDir creates a temporary directory
    51  func (f Files) TempDir(dir, pattern string) (name string, err error) {
    52  	if len(dir) == 0 {
    53  		// lazy init system temp dir in case it doesn't exist
    54  		if exists, _ := f.DirExists(os.TempDir()); !exists {
    55  			f.MkdirAll(os.TempDir(), 0o666)
    56  		}
    57  	}
    58  
    59  	return os.MkdirTemp(dir, pattern)
    60  }
    61  
    62  // FileExists returns true if the file system entry for the given path exists and is not a directory.
    63  func (f Files) FileExists(filename string) (bool, error) {
    64  	info, err := os.Stat(filename)
    65  
    66  	if os.IsNotExist(err) {
    67  		return false, nil
    68  	}
    69  	if err != nil {
    70  		return false, err
    71  	}
    72  
    73  	return !info.IsDir(), nil
    74  }
    75  
    76  // FileExists returns true if the file system entry for the given path exists and is not a directory.
    77  func FileExists(filename string) (bool, error) {
    78  	return Files{}.FileExists(filename)
    79  }
    80  
    81  // DirExists returns true if the file system entry for the given path exists and is a directory.
    82  func (f Files) DirExists(path string) (bool, error) {
    83  	info, err := os.Stat(path)
    84  
    85  	if os.IsNotExist(err) {
    86  		return false, nil
    87  	}
    88  	if err != nil {
    89  		return false, err
    90  	}
    91  
    92  	return info.IsDir(), nil
    93  }
    94  
    95  // Copy ...
    96  func (f Files) Copy(src, dst string) (int64, error) {
    97  	exists, err := f.FileExists(src)
    98  	if err != nil {
    99  		return 0, err
   100  	}
   101  
   102  	if !exists {
   103  		return 0, errors.New("Source file '" + src + "' does not exist")
   104  	}
   105  
   106  	source, err := os.Open(src)
   107  	if err != nil {
   108  		return 0, err
   109  	}
   110  	defer func() { _ = source.Close() }()
   111  
   112  	destination, err := os.Create(dst)
   113  	if err != nil {
   114  		return 0, err
   115  	}
   116  	stats, err := os.Stat(src)
   117  	if err != nil {
   118  		return 0, err
   119  	}
   120  
   121  	os.Chmod(dst, stats.Mode())
   122  	defer func() { _ = destination.Close() }()
   123  	nBytes, err := CopyData(destination, source)
   124  	return nBytes, err
   125  }
   126  
   127  // Move will move files from src to dst
   128  func (f Files) Move(src, dst string) error {
   129  	if exists, err := f.FileExists(src); err != nil {
   130  		return err
   131  	} else if !exists {
   132  		return fmt.Errorf("file doesn't exist: %s", src)
   133  	}
   134  
   135  	if _, err := f.Copy(src, dst); err != nil {
   136  		return err
   137  	}
   138  
   139  	return f.FileRemove(src)
   140  }
   141  
   142  // Chmod is a wrapper for os.Chmod().
   143  func (f Files) Chmod(path string, mode os.FileMode) error {
   144  	return os.Chmod(path, mode)
   145  }
   146  
   147  // Unzip will decompress a zip archive, moving all files and folders
   148  // within the zip file (parameter 1) to an output directory (parameter 2).
   149  // from https://golangcode.com/unzip-files-in-go/ with the following license:
   150  // MIT License
   151  //
   152  // # Copyright (c) 2017 Edd Turtle
   153  //
   154  // Permission is hereby granted, free of charge, to any person obtaining a copy
   155  // of this software and associated documentation files (the "Software"), to deal
   156  // in the Software without restriction, including without limitation the rights
   157  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
   158  // copies of the Software, and to permit persons to whom the Software is
   159  // furnished to do so, subject to the following conditions:
   160  //
   161  // The above copyright notice and this permission notice shall be included in all
   162  // copies or substantial portions of the Software.
   163  //
   164  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
   165  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
   166  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
   167  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
   168  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
   169  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
   170  // SOFTWARE.
   171  func Unzip(src, dest string) ([]string, error) {
   172  	var filenames []string
   173  
   174  	r, err := zip.OpenReader(src)
   175  	if err != nil {
   176  		return filenames, err
   177  	}
   178  	defer func() { _ = r.Close() }()
   179  
   180  	for _, f := range r.File {
   181  
   182  		// Store filename/path for returning and using later on
   183  		fpath := filepath.Join(dest, f.Name)
   184  
   185  		// Check for ZipSlip. More Info: http://bit.ly/2MsjAWE
   186  		if !strings.HasPrefix(fpath, filepath.Clean(dest)+string(os.PathSeparator)) {
   187  			return filenames, fmt.Errorf("%s: illegal file path", fpath)
   188  		}
   189  
   190  		filenames = append(filenames, fpath)
   191  
   192  		if f.FileInfo().IsDir() {
   193  			// Make Folder
   194  			err := os.MkdirAll(fpath, os.ModePerm)
   195  			if err != nil {
   196  				return filenames, fmt.Errorf("failed to create directory: %w", err)
   197  			}
   198  			continue
   199  		}
   200  
   201  		// Make File
   202  		if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
   203  			return filenames, err
   204  		}
   205  
   206  		outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
   207  		if err != nil {
   208  			return filenames, err
   209  		}
   210  
   211  		rc, err := f.Open()
   212  		if err != nil {
   213  			return filenames, err
   214  		}
   215  
   216  		_, err = CopyData(outFile, rc)
   217  
   218  		// Close the file without defer to close before next iteration of loop
   219  		_ = outFile.Close()
   220  		_ = rc.Close()
   221  
   222  		if err != nil {
   223  			return filenames, err
   224  		}
   225  	}
   226  	return filenames, nil
   227  }
   228  
   229  // Untar will decompress a gzipped archive and then untar it, moving all files and folders
   230  // within the tgz file (parameter 1) to an output directory (parameter 2).
   231  // some tar like the one created from npm have an addtional package folder which need to be removed during untar
   232  // stripComponent level acts the same like in the tar cli with level 1 corresponding to elimination of parent folder
   233  // stripComponentLevel = 1 -> parentFolder/someFile.Txt -> someFile.Txt
   234  // stripComponentLevel = 2 -> parentFolder/childFolder/someFile.Txt -> someFile.Txt
   235  // when stripCompenent in 0 the untar will retain the original tar folder structure
   236  // when stripCompmenet is greater than 0 the expectation is all files must be under that level folder and if not there is a hard check and failure condition
   237  func Untar(src string, dest string, stripComponentLevel int) error {
   238  	file, err := os.Open(src)
   239  	if err != nil {
   240  		return fmt.Errorf("unable to open src: %v", err)
   241  	}
   242  	defer file.Close()
   243  
   244  	if b, err := isFileGzipped(src); err == nil && b {
   245  		zr, err := gzip.NewReader(file)
   246  		if err != nil {
   247  			return fmt.Errorf("requires gzip-compressed body: %v", err)
   248  		}
   249  
   250  		return untar(zr, dest, stripComponentLevel)
   251  	}
   252  
   253  	return untar(file, dest, stripComponentLevel)
   254  }
   255  
   256  func untar(r io.Reader, dir string, level int) (err error) {
   257  	madeDir := map[string]bool{}
   258  
   259  	tr := tar.NewReader(r)
   260  	for {
   261  		f, err := tr.Next()
   262  		if err == io.EOF {
   263  			break
   264  		}
   265  		if err != nil {
   266  			return fmt.Errorf("tar error: %v", err)
   267  		}
   268  		if strings.HasPrefix(f.Name, "/") {
   269  			f.Name = fmt.Sprintf(".%s", f.Name)
   270  		}
   271  		if !validRelPath(f.Name) { // blocks path traversal attacks
   272  			return fmt.Errorf("tar contained invalid name error %q", f.Name)
   273  		}
   274  		rel := filepath.FromSlash(f.Name)
   275  
   276  		// when level X folder(s) needs to be removed we first check that the rel path must have atleast X or greater than X pathseperatorserr
   277  		// or else we might end in index out of range
   278  		if level > 0 {
   279  			if strings.Count(rel, string(os.PathSeparator)) >= level {
   280  				relSplit := strings.SplitN(rel, string(os.PathSeparator), level+1)
   281  				rel = relSplit[level]
   282  			} else {
   283  				return fmt.Errorf("files %q in tarball archive not under level %v", f.Name, level)
   284  			}
   285  		}
   286  
   287  		abs := filepath.Join(dir, rel)
   288  
   289  		fi := f.FileInfo()
   290  		mode := fi.Mode()
   291  		switch {
   292  		case mode.IsRegular():
   293  			// Make the directory. This is redundant because it should
   294  			// already be made by a directory entry in the tar
   295  			// beforehand. Thus, don't check for errors; the next
   296  			// write will fail with the same error.
   297  			dir := filepath.Dir(abs)
   298  			if !madeDir[dir] {
   299  				if err := os.MkdirAll(filepath.Dir(abs), 0o755); err != nil {
   300  					return err
   301  				}
   302  				madeDir[dir] = true
   303  			}
   304  			wf, err := os.OpenFile(abs, os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode.Perm())
   305  			if err != nil {
   306  				return err
   307  			}
   308  			n, err := io.Copy(wf, tr)
   309  			if closeErr := wf.Close(); closeErr != nil && err == nil {
   310  				err = closeErr
   311  			}
   312  			if err != nil {
   313  				return fmt.Errorf("error writing to %s: %v", abs, err)
   314  			}
   315  			if n != f.Size {
   316  				return fmt.Errorf("only wrote %d bytes to %s; expected %d", n, abs, f.Size)
   317  			}
   318  		case mode.IsDir():
   319  			if err := os.MkdirAll(abs, 0o755); err != nil {
   320  				return err
   321  			}
   322  			madeDir[abs] = true
   323  		case mode&fs.ModeSymlink != 0:
   324  			if err := os.Symlink(f.Linkname, abs); err != nil {
   325  				return err
   326  			}
   327  		default:
   328  			return fmt.Errorf("tar file entry %s contained unsupported file type %v", f.Name, mode)
   329  		}
   330  	}
   331  	return nil
   332  }
   333  
   334  // isFileGzipped checks the first 3 bytes of the given file to determine if it is gzipped or not. Returns `true` if the file is gzipped.
   335  func isFileGzipped(file string) (bool, error) {
   336  	f, err := os.Open(file)
   337  	defer f.Close()
   338  
   339  	if err != nil {
   340  		return false, err
   341  	}
   342  
   343  	b := make([]byte, 3)
   344  	_, err = io.ReadFull(f, b)
   345  
   346  	if err != nil {
   347  		return false, err
   348  	}
   349  
   350  	return b[0] == 0x1f && b[1] == 0x8b && b[2] == 8, nil
   351  }
   352  
   353  func validRelPath(p string) bool {
   354  	if p == "" || strings.Contains(p, `\`) || strings.HasPrefix(p, "/") || strings.Contains(p, "../") {
   355  		return false
   356  	}
   357  	return true
   358  }
   359  
   360  // Copy ...
   361  func Copy(src, dst string) (int64, error) {
   362  	return Files{}.Copy(src, dst)
   363  }
   364  
   365  // FileRead is a wrapper for os.ReadFile().
   366  func (f Files) FileRead(path string) ([]byte, error) {
   367  	return os.ReadFile(path)
   368  }
   369  
   370  // ReadFile is a wrapper for os.ReadFile() using the same name and syntax.
   371  func (f Files) ReadFile(path string) ([]byte, error) {
   372  	return f.FileRead(path)
   373  }
   374  
   375  // FileWrite is a wrapper for os.WriteFile().
   376  func (f Files) FileWrite(path string, content []byte, perm os.FileMode) error {
   377  	return os.WriteFile(path, content, perm)
   378  }
   379  
   380  // WriteFile is a wrapper for os.ReadFile() using the same name and syntax.
   381  func (f Files) WriteFile(path string, content []byte, perm os.FileMode) error {
   382  	return f.FileWrite(path, content, perm)
   383  }
   384  
   385  // FileRemove is a wrapper for os.Remove().
   386  func (f Files) FileRemove(path string) error {
   387  	return os.Remove(path)
   388  }
   389  
   390  // FileRename is a wrapper for os.Rename().
   391  func (f Files) FileRename(oldPath, newPath string) error {
   392  	return os.Rename(oldPath, newPath)
   393  }
   394  
   395  // FileOpen is a wrapper for os.OpenFile().
   396  func (f *Files) FileOpen(name string, flag int, perm os.FileMode) (*os.File, error) {
   397  	return os.OpenFile(name, flag, perm)
   398  }
   399  
   400  // MkdirAll is a wrapper for os.MkdirAll().
   401  func (f Files) MkdirAll(path string, perm os.FileMode) error {
   402  	return os.MkdirAll(path, perm)
   403  }
   404  
   405  // RemoveAll is a wrapper for os.RemoveAll().
   406  func (f Files) RemoveAll(path string) error {
   407  	return os.RemoveAll(path)
   408  }
   409  
   410  // Glob is a wrapper for doublestar.Glob().
   411  func (f Files) Glob(pattern string) (matches []string, err error) {
   412  	return doublestar.Glob(pattern)
   413  }
   414  
   415  // ExcludeFiles returns a slice of files, which contains only the sub-set of files that matched none
   416  // of the glob patterns in the provided excludes list.
   417  func ExcludeFiles(files, excludes []string) ([]string, error) {
   418  	if len(excludes) == 0 {
   419  		return files, nil
   420  	}
   421  
   422  	var filteredFiles []string
   423  	for _, file := range files {
   424  		includeFile := true
   425  		file = filepath.FromSlash(file)
   426  		for _, exclude := range excludes {
   427  			matched, err := doublestar.PathMatch(exclude, file)
   428  			if err != nil {
   429  				return nil, fmt.Errorf("failed to match file %s to pattern %s: %w", file, exclude, err)
   430  			}
   431  			if matched {
   432  				includeFile = false
   433  				break
   434  			}
   435  		}
   436  		if includeFile {
   437  			filteredFiles = append(filteredFiles, file)
   438  		}
   439  	}
   440  
   441  	return filteredFiles, nil
   442  }
   443  
   444  // Getwd is a wrapper for os.Getwd().
   445  func (f Files) Getwd() (string, error) {
   446  	return os.Getwd()
   447  }
   448  
   449  // Chdir is a wrapper for os.Chdir().
   450  func (f Files) Chdir(path string) error {
   451  	return os.Chdir(path)
   452  }
   453  
   454  // Stat is a wrapper for os.Stat()
   455  func (f Files) Stat(path string) (os.FileInfo, error) {
   456  	return os.Stat(path)
   457  }
   458  
   459  // Abs is a wrapper for filepath.Abs()
   460  func (f Files) Abs(path string) (string, error) {
   461  	return filepath.Abs(path)
   462  }
   463  
   464  // Symlink is a wrapper for os.Symlink
   465  func (f Files) Symlink(oldname, newname string) error {
   466  	return os.Symlink(oldname, newname)
   467  }
   468  
   469  // SHA256 computes a SHA256 for a given file
   470  func (f Files) SHA256(path string) (string, error) {
   471  	file, err := os.Open(path)
   472  	if err != nil {
   473  		return "", err
   474  	}
   475  	defer file.Close()
   476  
   477  	hash := sha256.New()
   478  	_, err = io.Copy(hash, file)
   479  	if err != nil {
   480  		return "", err
   481  	}
   482  
   483  	return fmt.Sprintf("%x", string(hash.Sum(nil))), nil
   484  }
   485  
   486  // CurrentTime returns the current time in the specified format
   487  func (f Files) CurrentTime(format string) string {
   488  	fString := format
   489  	if len(format) == 0 {
   490  		fString = "20060102-150405"
   491  	}
   492  	return fmt.Sprint(time.Now().Format(fString))
   493  }
   494  
   495  // Open is a wrapper for os.Open
   496  func (f Files) Open(name string) (io.ReadWriteCloser, error) {
   497  	return os.Open(name)
   498  }
   499  
   500  // Create is a wrapper for os.Create
   501  func (f Files) Create(name string) (io.ReadWriteCloser, error) {
   502  	return os.Create(name)
   503  }