2019-11-07 09:17:42 +02:00
package piperutils
import (
2021-10-20 14:10:39 +02:00
"archive/tar"
2020-03-23 11:38:31 +02:00
"archive/zip"
2021-10-20 14:10:39 +02:00
"compress/gzip"
2022-01-24 10:48:01 +02:00
"crypto/sha256"
2019-12-05 15:22:38 +02:00
"errors"
2020-03-23 11:38:31 +02:00
"fmt"
2021-10-20 14:10:39 +02:00
"io"
2022-03-23 11:02:00 +02:00
"io/fs"
2020-02-25 15:17:00 +02:00
"io/ioutil"
2019-11-07 09:17:42 +02:00
"os"
2020-03-23 11:38:31 +02:00
"path/filepath"
"strings"
2022-03-17 16:32:48 +02:00
"time"
2020-11-03 13:02:13 +02:00
"github.com/bmatcuk/doublestar"
2019-11-07 09:17:42 +02:00
)
2020-02-25 15:17:00 +02:00
// FileUtils ...
type FileUtils interface {
2020-07-28 17:06:17 +02:00
Abs ( path string ) ( string , error )
2021-10-11 11:10:21 +02:00
DirExists ( path string ) ( bool , error )
2020-02-25 15:17:00 +02:00
FileExists ( filename string ) ( bool , error )
Copy ( src , dest string ) ( int64 , error )
2022-03-04 11:26:46 +02:00
Move ( src , dest string ) error
2020-02-25 15:17:00 +02:00
FileRead ( path string ) ( [ ] byte , error )
2022-08-09 10:57:02 +02:00
ReadFile ( path string ) ( [ ] byte , error )
2020-02-25 15:17:00 +02:00
FileWrite ( path string , content [ ] byte , perm os . FileMode ) error
2022-08-09 10:57:02 +02:00
WriteFile ( path string , content [ ] byte , perm os . FileMode ) error
2022-01-11 11:01:15 +02:00
FileRemove ( path string ) error
2020-02-25 15:17:00 +02:00
MkdirAll ( path string , perm os . FileMode ) error
2020-07-16 14:25:01 +02:00
Chmod ( path string , mode os . FileMode ) error
2020-07-30 10:35:46 +02:00
Glob ( pattern string ) ( matches [ ] string , err error )
2021-07-22 11:06:46 +02:00
Chdir ( path string ) error
2021-09-14 16:14:50 +02:00
TempDir ( string , string ) ( string , error )
RemoveAll ( string ) error
FileRename ( string , string ) error
Getwd ( ) ( string , error )
2022-01-14 12:05:11 +02:00
Symlink ( oldname string , newname string ) error
2022-03-17 16:32:48 +02:00
SHA256 ( path string ) ( string , error )
CurrentTime ( format string ) string
2022-03-23 11:02:00 +02:00
Open ( name string ) ( io . ReadWriteCloser , error )
Create ( name string ) ( io . ReadWriteCloser , error )
2020-02-25 15:17:00 +02:00
}
// Files ...
2022-08-09 10:57:02 +02:00
type Files struct { }
2020-02-25 15:17:00 +02:00
2020-10-20 09:05:17 +02:00
// TempDir creates a temporary directory
func ( f Files ) TempDir ( dir , pattern string ) ( name string , err error ) {
2022-02-23 12:41:26 +02:00
if len ( dir ) == 0 {
// lazy init system temp dir in case it doesn't exist
if exists , _ := f . DirExists ( os . TempDir ( ) ) ; ! exists {
2022-08-09 10:57:02 +02:00
f . MkdirAll ( os . TempDir ( ) , 0 o666 )
2022-02-23 12:41:26 +02:00
}
}
2020-10-20 09:05:17 +02:00
return ioutil . TempDir ( dir , pattern )
}
2020-04-03 21:32:38 +02:00
// FileExists returns true if the file system entry for the given path exists and is not a directory.
2020-02-25 15:17:00 +02:00
func ( f Files ) FileExists ( filename string ) ( bool , error ) {
2019-11-07 09:17:42 +02:00
info , err := os . Stat ( filename )
2019-12-11 11:13:23 +02:00
2019-11-07 09:17:42 +02:00
if os . IsNotExist ( err ) {
2019-12-11 11:13:23 +02:00
return false , nil
}
if err != nil {
return false , err
2019-11-07 09:17:42 +02:00
}
2019-12-11 11:13:23 +02:00
return ! info . IsDir ( ) , nil
2019-11-07 09:17:42 +02:00
}
2019-12-05 15:22:38 +02:00
2020-04-03 21:32:38 +02:00
// FileExists returns true if the file system entry for the given path exists and is not a directory.
2020-02-25 15:17:00 +02:00
func FileExists ( filename string ) ( bool , error ) {
return Files { } . FileExists ( filename )
}
2020-06-15 09:47:33 +02:00
// DirExists returns true if the file system entry for the given path exists and is a directory.
func ( f Files ) DirExists ( path string ) ( bool , error ) {
info , err := os . Stat ( path )
if os . IsNotExist ( err ) {
return false , nil
}
if err != nil {
return false , err
}
return info . IsDir ( ) , nil
}
2019-12-05 15:22:38 +02:00
// Copy ...
2020-02-25 15:17:00 +02:00
func ( f Files ) Copy ( src , dst string ) ( int64 , error ) {
exists , err := f . FileExists ( src )
2019-12-11 11:13:23 +02:00
if err != nil {
return 0 , err
}
if ! exists {
2019-12-11 09:17:16 +02:00
return 0 , errors . New ( "Source file '" + src + "' does not exist" )
2019-12-05 15:22:38 +02:00
}
source , err := os . Open ( src )
if err != nil {
return 0 , err
}
2020-09-18 11:54:45 +02:00
defer func ( ) { _ = source . Close ( ) } ( )
2019-12-05 15:22:38 +02:00
destination , err := os . Create ( dst )
if err != nil {
return 0 , err
}
2022-03-30 13:58:16 +02:00
stats , err := os . Stat ( src )
if err != nil {
return 0 , err
}
os . Chmod ( dst , stats . Mode ( ) )
2020-09-18 11:54:45 +02:00
defer func ( ) { _ = destination . Close ( ) } ( )
2021-06-23 14:41:52 +02:00
nBytes , err := CopyData ( destination , source )
2019-12-05 15:22:38 +02:00
return nBytes , err
}
2020-02-25 15:17:00 +02:00
2022-08-09 10:57:02 +02:00
// Move will move files from src to dst
2022-03-04 11:26:46 +02:00
func ( f Files ) Move ( src , dst string ) error {
if exists , err := f . FileExists ( src ) ; err != nil {
return err
} else if ! exists {
return fmt . Errorf ( "file doesn't exist: %s" , src )
}
if _ , err := f . Copy ( src , dst ) ; err != nil {
return err
}
return f . FileRemove ( src )
}
2022-08-09 10:57:02 +02:00
// Chmod is a wrapper for os.Chmod().
2020-07-16 14:25:01 +02:00
func ( f Files ) Chmod ( path string , mode os . FileMode ) error {
return os . Chmod ( path , mode )
}
2020-03-23 11:38:31 +02:00
// Unzip will decompress a zip archive, moving all files and folders
// within the zip file (parameter 1) to an output directory (parameter 2).
// from https://golangcode.com/unzip-files-in-go/ with the following license:
// MIT License
//
// Copyright (c) 2017 Edd Turtle
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
func Unzip ( src , dest string ) ( [ ] string , error ) {
var filenames [ ] string
r , err := zip . OpenReader ( src )
if err != nil {
return filenames , err
}
2020-09-18 11:54:45 +02:00
defer func ( ) { _ = r . Close ( ) } ( )
2020-03-23 11:38:31 +02:00
for _ , f := range r . File {
// Store filename/path for returning and using later on
fpath := filepath . Join ( dest , f . Name )
// Check for ZipSlip. More Info: http://bit.ly/2MsjAWE
if ! strings . HasPrefix ( fpath , filepath . Clean ( dest ) + string ( os . PathSeparator ) ) {
return filenames , fmt . Errorf ( "%s: illegal file path" , fpath )
}
filenames = append ( filenames , fpath )
if f . FileInfo ( ) . IsDir ( ) {
// Make Folder
2020-09-18 11:54:45 +02:00
err := os . MkdirAll ( fpath , os . ModePerm )
if err != nil {
return filenames , fmt . Errorf ( "failed to create directory: %w" , err )
}
2020-03-23 11:38:31 +02:00
continue
}
// Make File
if err = os . MkdirAll ( filepath . Dir ( fpath ) , os . ModePerm ) ; err != nil {
return filenames , err
}
outFile , err := os . OpenFile ( fpath , os . O_WRONLY | os . O_CREATE | os . O_TRUNC , f . Mode ( ) )
if err != nil {
return filenames , err
}
rc , err := f . Open ( )
if err != nil {
return filenames , err
}
2021-06-23 14:41:52 +02:00
_ , err = CopyData ( outFile , rc )
2020-03-23 11:38:31 +02:00
// Close the file without defer to close before next iteration of loop
2020-09-18 11:54:45 +02:00
_ = outFile . Close ( )
_ = rc . Close ( )
2020-03-23 11:38:31 +02:00
if err != nil {
return filenames , err
}
}
return filenames , nil
}
2021-10-20 14:10:39 +02:00
// Untar will decompress a gzipped archive and then untar it, moving all files and folders
// within the tgz file (parameter 1) to an output directory (parameter 2).
// some tar like the one created from npm have an addtional package folder which need to be removed during untar
// stripComponent level acts the same like in the tar cli with level 1 corresponding to elimination of parent folder
// stripComponentLevel = 1 -> parentFolder/someFile.Txt -> someFile.Txt
// stripComponentLevel = 2 -> parentFolder/childFolder/someFile.Txt -> someFile.Txt
// when stripCompenent in 0 the untar will retain the original tar folder structure
// 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
func Untar ( src string , dest string , stripComponentLevel int ) error {
file , err := os . Open ( src )
if err != nil {
2022-03-30 13:58:16 +02:00
return fmt . Errorf ( "unable to open src: %v" , err )
2021-10-20 14:10:39 +02:00
}
2022-08-09 10:57:02 +02:00
defer file . Close ( )
2022-03-23 11:02:00 +02:00
2022-03-30 12:30:57 +02:00
if b , err := isFileGzipped ( src ) ; err == nil && b {
2022-03-23 11:02:00 +02:00
zr , err := gzip . NewReader ( file )
if err != nil {
return fmt . Errorf ( "requires gzip-compressed body: %v" , err )
}
return untar ( zr , dest , stripComponentLevel )
}
2021-10-20 14:10:39 +02:00
return untar ( file , dest , stripComponentLevel )
}
func untar ( r io . Reader , dir string , level int ) ( err error ) {
madeDir := map [ string ] bool { }
2022-03-23 11:02:00 +02:00
tr := tar . NewReader ( r )
2021-10-20 14:10:39 +02:00
for {
f , err := tr . Next ( )
if err == io . EOF {
break
}
if err != nil {
return fmt . Errorf ( "tar error: %v" , err )
}
2022-03-23 11:02:00 +02:00
if strings . HasPrefix ( f . Name , "/" ) {
f . Name = fmt . Sprintf ( ".%s" , f . Name )
}
if ! validRelPath ( f . Name ) { // blocks path traversal attacks
2021-10-20 14:10:39 +02:00
return fmt . Errorf ( "tar contained invalid name error %q" , f . Name )
}
rel := filepath . FromSlash ( f . Name )
// 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
// or else we might end in index out of range
if level > 0 {
if strings . Count ( rel , string ( os . PathSeparator ) ) >= level {
relSplit := strings . SplitN ( rel , string ( os . PathSeparator ) , level + 1 )
rel = relSplit [ level ]
} else {
return fmt . Errorf ( "files %q in tarball archive not under level %v" , f . Name , level )
}
}
abs := filepath . Join ( dir , rel )
fi := f . FileInfo ( )
mode := fi . Mode ( )
switch {
case mode . IsRegular ( ) :
// Make the directory. This is redundant because it should
// already be made by a directory entry in the tar
// beforehand. Thus, don't check for errors; the next
// write will fail with the same error.
dir := filepath . Dir ( abs )
if ! madeDir [ dir ] {
2022-08-09 10:57:02 +02:00
if err := os . MkdirAll ( filepath . Dir ( abs ) , 0 o755 ) ; err != nil {
2021-10-20 14:10:39 +02:00
return err
}
madeDir [ dir ] = true
}
wf , err := os . OpenFile ( abs , os . O_RDWR | os . O_CREATE | os . O_TRUNC , mode . Perm ( ) )
if err != nil {
return err
}
n , err := io . Copy ( wf , tr )
if closeErr := wf . Close ( ) ; closeErr != nil && err == nil {
err = closeErr
}
if err != nil {
return fmt . Errorf ( "error writing to %s: %v" , abs , err )
}
if n != f . Size {
return fmt . Errorf ( "only wrote %d bytes to %s; expected %d" , n , abs , f . Size )
}
case mode . IsDir ( ) :
2022-08-09 10:57:02 +02:00
if err := os . MkdirAll ( abs , 0 o755 ) ; err != nil {
2021-10-20 14:10:39 +02:00
return err
}
madeDir [ abs ] = true
2022-03-23 11:02:00 +02:00
case mode & fs . ModeSymlink != 0 :
if err := os . Symlink ( f . Linkname , abs ) ; err != nil {
return err
}
2021-10-20 14:10:39 +02:00
default :
return fmt . Errorf ( "tar file entry %s contained unsupported file type %v" , f . Name , mode )
}
}
return nil
}
2022-03-23 11:02:00 +02:00
// 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.
func isFileGzipped ( file string ) ( bool , error ) {
f , err := os . Open ( file )
defer f . Close ( )
if err != nil {
return false , err
}
b := make ( [ ] byte , 3 )
_ , err = io . ReadFull ( f , b )
if err != nil {
return false , err
}
return b [ 0 ] == 0x1f && b [ 1 ] == 0x8b && b [ 2 ] == 8 , nil
}
2021-10-20 14:10:39 +02:00
func validRelPath ( p string ) bool {
if p == "" || strings . Contains ( p , ` \ ` ) || strings . HasPrefix ( p , "/" ) || strings . Contains ( p , "../" ) {
return false
}
return true
}
2020-02-25 15:17:00 +02:00
// Copy ...
func Copy ( src , dst string ) ( int64 , error ) {
return Files { } . Copy ( src , dst )
}
2022-08-09 10:57:02 +02:00
// FileRead is a wrapper for os.ReadFile().
2020-02-25 15:17:00 +02:00
func ( f Files ) FileRead ( path string ) ( [ ] byte , error ) {
2022-08-09 10:57:02 +02:00
return os . ReadFile ( path )
}
// ReadFile is a wrapper for os.ReadFile() using the same name and syntax.
func ( f Files ) ReadFile ( path string ) ( [ ] byte , error ) {
return f . FileRead ( path )
2020-02-25 15:17:00 +02:00
}
2022-08-09 10:57:02 +02:00
// FileWrite is a wrapper for os.WriteFile().
2020-02-25 15:17:00 +02:00
func ( f Files ) FileWrite ( path string , content [ ] byte , perm os . FileMode ) error {
2022-08-09 10:57:02 +02:00
return os . WriteFile ( path , content , perm )
}
// WriteFile is a wrapper for os.ReadFile() using the same name and syntax.
func ( f Files ) WriteFile ( path string , content [ ] byte , perm os . FileMode ) error {
return f . FileWrite ( path , content , perm )
2020-02-25 15:17:00 +02:00
}
2020-09-18 11:54:45 +02:00
// FileRemove is a wrapper for os.Remove().
2020-06-15 09:47:33 +02:00
func ( f Files ) FileRemove ( path string ) error {
return os . Remove ( path )
}
2020-09-18 11:54:45 +02:00
// FileRename is a wrapper for os.Rename().
func ( f Files ) FileRename ( oldPath , newPath string ) error {
return os . Rename ( oldPath , newPath )
}
// FileOpen is a wrapper for os.OpenFile().
func ( f * Files ) FileOpen ( name string , flag int , perm os . FileMode ) ( * os . File , error ) {
return os . OpenFile ( name , flag , perm )
}
2020-06-15 09:47:33 +02:00
// MkdirAll is a wrapper for os.MkdirAll().
2020-02-25 15:17:00 +02:00
func ( f Files ) MkdirAll ( path string , perm os . FileMode ) error {
return os . MkdirAll ( path , perm )
}
2020-06-15 09:47:33 +02:00
2020-09-18 11:54:45 +02:00
// RemoveAll is a wrapper for os.RemoveAll().
func ( f Files ) RemoveAll ( path string ) error {
return os . RemoveAll ( path )
}
2020-06-15 09:47:33 +02:00
// Glob is a wrapper for doublestar.Glob().
func ( f Files ) Glob ( pattern string ) ( matches [ ] string , err error ) {
return doublestar . Glob ( pattern )
}
2020-11-26 12:45:53 +02:00
// ExcludeFiles returns a slice of files, which contains only the sub-set of files that matched none
// of the glob patterns in the provided excludes list.
func ExcludeFiles ( files , excludes [ ] string ) ( [ ] string , error ) {
if len ( excludes ) == 0 {
return files , nil
}
var filteredFiles [ ] string
for _ , file := range files {
includeFile := true
2021-03-03 23:58:29 +02:00
file = filepath . FromSlash ( file )
2020-11-26 12:45:53 +02:00
for _ , exclude := range excludes {
matched , err := doublestar . PathMatch ( exclude , file )
if err != nil {
return nil , fmt . Errorf ( "failed to match file %s to pattern %s: %w" , file , exclude , err )
}
if matched {
includeFile = false
break
}
}
if includeFile {
filteredFiles = append ( filteredFiles , file )
}
}
return filteredFiles , nil
}
2020-06-15 09:47:33 +02:00
// Getwd is a wrapper for os.Getwd().
func ( f Files ) Getwd ( ) ( string , error ) {
return os . Getwd ( )
}
// Chdir is a wrapper for os.Chdir().
func ( f Files ) Chdir ( path string ) error {
return os . Chdir ( path )
}
2020-07-15 10:31:36 +02:00
// Stat is a wrapper for os.Stat()
func ( f Files ) Stat ( path string ) ( os . FileInfo , error ) {
return os . Stat ( path )
}
2020-07-28 17:06:17 +02:00
// Abs is a wrapper for filepath.Abs()
func ( f Files ) Abs ( path string ) ( string , error ) {
return filepath . Abs ( path )
}
2022-01-14 12:05:11 +02:00
// Symlink is a wrapper for os.Symlink
func ( f Files ) Symlink ( oldname , newname string ) error {
return os . Symlink ( oldname , newname )
}
2022-01-24 10:48:01 +02:00
2022-03-17 16:32:48 +02:00
// SHA256 computes a SHA256 for a given file
2022-01-24 10:48:01 +02:00
func ( f Files ) SHA256 ( path string ) ( string , error ) {
file , err := os . Open ( path )
if err != nil {
return "" , err
}
defer file . Close ( )
hash := sha256 . New ( )
_ , err = io . Copy ( hash , file )
if err != nil {
return "" , err
}
return fmt . Sprintf ( "%x" , string ( hash . Sum ( nil ) ) ) , nil
}
2022-03-17 16:32:48 +02:00
// CurrentTime returns the current time in the specified format
func ( f Files ) CurrentTime ( format string ) string {
fString := format
if len ( format ) == 0 {
fString = "20060102-150405"
}
return fmt . Sprint ( time . Now ( ) . Format ( fString ) )
}
2022-03-23 11:02:00 +02:00
// Open is a wrapper for os.Open
func ( f Files ) Open ( name string ) ( io . ReadWriteCloser , error ) {
return os . Open ( name )
}
// Create is a wrapper for os.Create
func ( f Files ) Create ( name string ) ( io . ReadWriteCloser , error ) {
return os . Create ( name )
}