github.com/vmware/go-vcloud-director/v2@v2.24.0/util/tar.go (about)

     1  /*
     2   * Copyright 2018 VMware, Inc.  All rights reserved.  Licensed under the Apache v2 License.
     3   */
     4  
     5  package util
     6  
     7  import (
     8  	"archive/tar"
     9  	"errors"
    10  	"io"
    11  	"net/http"
    12  	"os"
    13  	"path/filepath"
    14  	"strings"
    15  )
    16  
    17  const TmpDirPrefix = "govcd"
    18  
    19  // Unpack extracts files to system tmp dir with name govcd+random number. Created folder with files isn't deleted.
    20  // Returns extracted files paths in array and path where folder with files created.
    21  func Unpack(tarFile string) ([]string, string, error) {
    22  
    23  	var filePaths []string
    24  	var dst string
    25  
    26  	reader, err := os.Open(filepath.Clean(tarFile))
    27  	if err != nil {
    28  		return filePaths, dst, err
    29  	}
    30  	defer func() {
    31  		if err := reader.Close(); err != nil {
    32  			Logger.Printf("Error closing file: %s\n", err)
    33  		}
    34  	}()
    35  
    36  	tarReader := tar.NewReader(reader)
    37  
    38  	dst, err = os.MkdirTemp("", TmpDirPrefix)
    39  	if err != nil {
    40  		return filePaths, dst, err
    41  	}
    42  
    43  	var expectedFileSize int64 = -1
    44  
    45  	for {
    46  		header, err := tarReader.Next()
    47  
    48  		switch {
    49  
    50  		// if no more files are found return
    51  		case err == io.EOF:
    52  			return filePaths, dst, nil
    53  
    54  			// return any other error
    55  		case err != nil:
    56  			return filePaths, dst, err
    57  
    58  			// if the header is nil, just skip it (not sure how this happens)
    59  		case header == nil:
    60  			continue
    61  
    62  		case header != nil:
    63  			expectedFileSize = header.Size
    64  		}
    65  
    66  		// the target location where the dir/newFile should be created
    67  		target := filepath.Join(dst, sanitizedName(header.Name))
    68  		Logger.Printf("[TRACE] extracting newFile: %s \n", target)
    69  
    70  		// check the newFile type
    71  		switch header.Typeflag {
    72  
    73  		// if its a dir and it doesn't exist create it
    74  		case tar.TypeDir:
    75  			if _, err := os.Stat(target); err != nil {
    76  				if err := os.MkdirAll(target, 0750); err != nil {
    77  					return filePaths, dst, err
    78  				}
    79  			}
    80  
    81  		case tar.TypeSymlink:
    82  			if header.Linkname != "" {
    83  				err := os.Symlink(header.Linkname, target)
    84  				if err != nil {
    85  					return filePaths, dst, err
    86  				}
    87  			} else {
    88  				return filePaths, dst, errors.New("file %s is a symlink, but no link information was provided")
    89  			}
    90  
    91  			// if it's a newFile create it
    92  		case tar.TypeReg:
    93  			newFile, err := os.OpenFile(filepath.Clean(target), os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode))
    94  			if err != nil {
    95  				return filePaths, dst, err
    96  			}
    97  
    98  			// copy over contents
    99  			for {
   100  				_, err := io.CopyN(newFile, tarReader, 1024)
   101  				if err != nil {
   102  					if errors.Is(err, io.EOF) {
   103  						break
   104  					}
   105  					return filePaths, dst, err
   106  				}
   107  			}
   108  
   109  			filePaths = append(filePaths, newFile.Name())
   110  
   111  			if err := isExtractedFileValid(newFile, expectedFileSize); err != nil {
   112  				errClose := newFile.Close()
   113  				if errClose != nil {
   114  					Logger.Printf("[DEBUG - Unpack] error closing newFile: %s", errClose)
   115  				}
   116  				return filePaths, dst, err
   117  			}
   118  
   119  			// manually close here after each newFile operation; deferring would cause each newFile close
   120  			// to wait until all operations have completed.
   121  			errClose := newFile.Close()
   122  			if errClose != nil {
   123  				Logger.Printf("[DEBUG - Unpack] error closing newFile: %s", errClose)
   124  			}
   125  		}
   126  	}
   127  }
   128  
   129  func isExtractedFileValid(file *os.File, expectedFileSize int64) error {
   130  	if fInfo, err := file.Stat(); err == nil {
   131  		Logger.Printf("[TRACE] isExtractedFileValid: created file size %#v, size from header %#v.\n", fInfo.Size(), expectedFileSize)
   132  		if fInfo.Size() != expectedFileSize && expectedFileSize != -1 {
   133  			return errors.New("extracted file didn't match defined file size")
   134  		}
   135  	}
   136  	return nil
   137  }
   138  
   139  func sanitizedName(filename string) string {
   140  	if len(filename) > 1 && filename[1] == ':' {
   141  		filename = filename[2:]
   142  	}
   143  	filename = strings.TrimLeft(filename, "\\/.")
   144  	filename = strings.TrimLeft(filename, "./")
   145  	filename = strings.Replace(filename, "../../", "../", -1)
   146  	return strings.Replace(filename, "..\\", "", -1)
   147  }
   148  
   149  // GetFileContentType returns the real file type
   150  func GetFileContentType(file string) (string, error) { // Open File
   151  	f, err := os.Open(filepath.Clean(file))
   152  	if err != nil {
   153  		return "", err
   154  	}
   155  	defer func() {
   156  		if err := f.Close(); err != nil {
   157  			Logger.Printf("Error closing file: %s\n", err)
   158  		}
   159  	}()
   160  	// Only the first 512 bytes are used to sniff the content type.
   161  	buffer := make([]byte, 512)
   162  
   163  	_, err = f.Read(buffer)
   164  	if err != nil {
   165  		return "", err
   166  	}
   167  
   168  	// Use the net/http package's handy DectectContentType function. Always returns a valid
   169  	// content-type by returning "application/octet-stream" if no others seemed to match.
   170  	contentType := http.DetectContentType(buffer)
   171  
   172  	return contentType, nil
   173  }