github.com/wanddynosios/cli/v8@v8.7.9-0.20240221182337-1a92e3a7017f/actor/sharedaction/resource.go (about)

     1  package sharedaction
     2  
     3  import (
     4  	"archive/zip"
     5  	"crypto/sha1"
     6  	"fmt"
     7  	"io"
     8  	"io/ioutil"
     9  	"os"
    10  	"path/filepath"
    11  	"strings"
    12  
    13  	"code.cloudfoundry.org/cli/api/cloudcontroller/ccv3"
    14  
    15  	"code.cloudfoundry.org/cli/actor/actionerror"
    16  	"code.cloudfoundry.org/ykk"
    17  	ignore "github.com/sabhiram/go-gitignore"
    18  	log "github.com/sirupsen/logrus"
    19  )
    20  
    21  const (
    22  	DefaultFolderPermissions      = 0755
    23  	DefaultArchiveFilePermissions = 0744
    24  	MaxResourceMatchChunkSize     = 1000
    25  )
    26  
    27  var DefaultIgnoreLines = []string{
    28  	".cfignore",
    29  	".DS_Store",
    30  	".git",
    31  	".gitignore",
    32  	".hg",
    33  	".svn",
    34  	"_darcs",
    35  	"manifest.yaml",
    36  	"manifest.yml",
    37  }
    38  
    39  type Resource struct {
    40  	Filename string      `json:"fn"`
    41  	Mode     os.FileMode `json:"mode"`
    42  	SHA1     string      `json:"sha1"`
    43  	Size     int64       `json:"size"`
    44  }
    45  
    46  type V3Resource ccv3.Resource
    47  
    48  // ToV3Resource converts a sharedaction Resource to V3 Resource format
    49  func (r Resource) ToV3Resource() V3Resource {
    50  	return V3Resource{
    51  		FilePath:    r.Filename,
    52  		Mode:        r.Mode,
    53  		Checksum:    ccv3.Checksum{Value: r.SHA1},
    54  		SizeInBytes: r.Size,
    55  	}
    56  }
    57  
    58  // ToV2Resource converts a V3 Resource to V2 Resource format
    59  func (r V3Resource) ToV2Resource() Resource {
    60  	return Resource{
    61  		Filename: r.FilePath,
    62  		Mode:     r.Mode,
    63  		SHA1:     r.Checksum.Value,
    64  		Size:     r.SizeInBytes,
    65  	}
    66  }
    67  
    68  // GatherArchiveResources returns a list of resources for an archive.
    69  func (actor Actor) GatherArchiveResources(archivePath string) ([]Resource, error) {
    70  	var resources []Resource
    71  
    72  	archive, err := os.Open(archivePath)
    73  	if err != nil {
    74  		return nil, err
    75  	}
    76  	defer archive.Close()
    77  
    78  	reader, err := actor.newArchiveReader(archive)
    79  	if err != nil {
    80  		return nil, err
    81  	}
    82  
    83  	gitIgnore, err := actor.generateArchiveCFIgnoreMatcher(reader.File)
    84  	if err != nil {
    85  		log.Errorln("reading .cfignore file:", err)
    86  		return nil, err
    87  	}
    88  
    89  	for _, archivedFile := range reader.File {
    90  		filename := filepath.ToSlash(archivedFile.Name)
    91  		if gitIgnore.MatchesPath(filename) {
    92  			continue
    93  		}
    94  
    95  		resource := Resource{Filename: filename}
    96  		info := archivedFile.FileInfo()
    97  
    98  		switch {
    99  		case info.IsDir():
   100  			resource.Mode = DefaultFolderPermissions
   101  		case info.Mode()&os.ModeSymlink == os.ModeSymlink:
   102  			resource.Mode = info.Mode()
   103  		default:
   104  			fileReader, err := archivedFile.Open()
   105  			if err != nil {
   106  				return nil, err
   107  			}
   108  			defer fileReader.Close()
   109  
   110  			hash := sha1.New()
   111  
   112  			_, err = io.Copy(hash, fileReader)
   113  			if err != nil {
   114  				return nil, err
   115  			}
   116  
   117  			resource.Mode = DefaultArchiveFilePermissions
   118  			resource.SHA1 = fmt.Sprintf("%x", hash.Sum(nil))
   119  			resource.Size = archivedFile.FileInfo().Size()
   120  		}
   121  
   122  		resources = append(resources, resource)
   123  	}
   124  	if len(resources) <= 1 {
   125  		return nil, actionerror.EmptyArchiveError{Path: archivePath}
   126  	}
   127  	return resources, nil
   128  }
   129  
   130  // GatherDirectoryResources returns a list of resources for a directory.
   131  func (actor Actor) GatherDirectoryResources(sourceDir string) ([]Resource, error) {
   132  	var (
   133  		resources []Resource
   134  		gitIgnore *ignore.GitIgnore
   135  	)
   136  
   137  	gitIgnore, err := actor.generateDirectoryCFIgnoreMatcher(sourceDir)
   138  	if err != nil {
   139  		log.Errorln("reading .cfignore file:", err)
   140  		return nil, err
   141  	}
   142  
   143  	evalDir, err := filepath.EvalSymlinks(sourceDir)
   144  	if err != nil {
   145  		log.Errorln("evaluating symlink:", err)
   146  		return nil, err
   147  	}
   148  
   149  	walkErr := filepath.Walk(evalDir, func(fullPath string, info os.FileInfo, err error) error {
   150  		if err != nil {
   151  			return err
   152  		}
   153  
   154  		relPath, err := filepath.Rel(evalDir, fullPath)
   155  		if err != nil {
   156  			return err
   157  		}
   158  
   159  		// if file ignored continue to the next file
   160  		if gitIgnore.MatchesPath(relPath) {
   161  			return nil
   162  		}
   163  
   164  		if relPath == "." {
   165  			return nil
   166  		}
   167  
   168  		resource := Resource{
   169  			Filename: filepath.ToSlash(relPath),
   170  		}
   171  
   172  		switch {
   173  		case info.IsDir():
   174  			// If the file is a directory
   175  			resource.Mode = DefaultFolderPermissions
   176  		case info.Mode()&os.ModeSymlink == os.ModeSymlink:
   177  			// If the file is a Symlink we just set the mode of the file
   178  			// We won't be using any sha information since we don't do
   179  			// any resource matching on symlinks.
   180  			resource.Mode = fixMode(info.Mode())
   181  		default:
   182  			// If the file is regular we want to open
   183  			// and calculate the sha of the file
   184  			file, err := os.Open(fullPath)
   185  			if err != nil {
   186  				return err
   187  			}
   188  			defer file.Close()
   189  
   190  			sum := sha1.New()
   191  			_, err = io.Copy(sum, file)
   192  			if err != nil {
   193  				return err
   194  			}
   195  
   196  			resource.Mode = fixMode(info.Mode())
   197  			resource.SHA1 = fmt.Sprintf("%x", sum.Sum(nil))
   198  			resource.Size = info.Size()
   199  		}
   200  
   201  		resources = append(resources, resource)
   202  		return nil
   203  	})
   204  
   205  	if len(resources) == 0 {
   206  		return nil, actionerror.EmptyDirectoryError{Path: sourceDir}
   207  	}
   208  
   209  	return resources, walkErr
   210  }
   211  
   212  // ZipArchiveResources zips an archive and a sorted (based on full
   213  // path/filename) list of resources and returns the location. On Windows, the
   214  // filemode for user is forced to be readable and executable.
   215  func (actor Actor) ZipArchiveResources(sourceArchivePath string, filesToInclude []Resource) (string, error) {
   216  	log.WithField("sourceArchive", sourceArchivePath).Info("zipping source files from archive")
   217  	zipFile, err := ioutil.TempFile("", "cf-cli-")
   218  	if err != nil {
   219  		return "", err
   220  	}
   221  	defer zipFile.Close()
   222  	zipPath := zipFile.Name()
   223  
   224  	writer := zip.NewWriter(zipFile)
   225  	defer writer.Close()
   226  
   227  	source, err := os.Open(sourceArchivePath)
   228  	if err != nil {
   229  		return zipPath, err
   230  	}
   231  	defer source.Close()
   232  
   233  	reader, err := actor.newArchiveReader(source)
   234  	if err != nil {
   235  		return zipPath, err
   236  	}
   237  
   238  	for _, archiveFile := range reader.File {
   239  		resource, ok := actor.findInResources(archiveFile.Name, filesToInclude)
   240  		if !ok {
   241  			log.WithField("archiveFileName", archiveFile.Name).Debug("skipping file")
   242  			continue
   243  		}
   244  
   245  		log.WithField("archiveFileName", archiveFile.Name).Debug("zipping file")
   246  		// archiveFile.Open opens the symlink file, not the file it points too
   247  		reader, openErr := archiveFile.Open()
   248  		if openErr != nil {
   249  			log.WithField("archiveFile", archiveFile.Name).Errorln("opening path in dir:", openErr)
   250  			return zipPath, openErr
   251  		}
   252  		defer reader.Close()
   253  
   254  		err = actor.addFileToZipFromFileSystem(
   255  			resource.Filename, reader, archiveFile.FileInfo(),
   256  			resource, writer,
   257  		)
   258  		if err != nil {
   259  			log.WithField("archiveFileName", archiveFile.Name).Errorln("zipping file:", err)
   260  			return zipPath, err
   261  		}
   262  		reader.Close()
   263  	}
   264  
   265  	log.WithFields(log.Fields{
   266  		"zip_file_location": zipFile.Name(),
   267  		"zipped_file_count": len(filesToInclude),
   268  	}).Info("zip file created")
   269  	return zipPath, nil
   270  }
   271  
   272  // ZipDirectoryResources zips a directory and a sorted (based on full
   273  // path/filename) list of resources and returns the location. On Windows, the
   274  // filemode for user is forced to be readable and executable.
   275  func (actor Actor) ZipDirectoryResources(sourceDir string, filesToInclude []Resource) (string, error) {
   276  	log.WithField("sourceDir", sourceDir).Info("zipping source files from directory")
   277  	zipFile, err := ioutil.TempFile("", "cf-cli-")
   278  	if err != nil {
   279  		return "", err
   280  	}
   281  	defer zipFile.Close()
   282  	zipPath := zipFile.Name()
   283  
   284  	writer := zip.NewWriter(zipFile)
   285  	defer writer.Close()
   286  
   287  	for _, resource := range filesToInclude {
   288  		fullPath := filepath.Join(sourceDir, resource.Filename)
   289  		log.WithField("fullPath", fullPath).Debug("zipping file")
   290  
   291  		fileInfo, err := os.Lstat(fullPath)
   292  		if err != nil {
   293  			log.WithField("fullPath", fullPath).Errorln("stat error in dir:", err)
   294  			return zipPath, err
   295  		}
   296  
   297  		log.WithField("file-mode", fileInfo.Mode().String()).Debug("resource file info")
   298  		if fileInfo.Mode()&os.ModeSymlink == os.ModeSymlink {
   299  			// we need to user os.Readlink to read a symlink file from a directory
   300  			err = actor.addLinkToZipFromFileSystem(fullPath, fileInfo, resource, writer)
   301  			if err != nil {
   302  				log.WithField("fullPath", fullPath).Errorln("zipping file:", err)
   303  				return zipPath, err
   304  			}
   305  		} else {
   306  			srcFile, err := os.Open(fullPath)
   307  			if err != nil {
   308  				log.WithField("fullPath", fullPath).Errorln("opening path in dir:", err)
   309  				return zipPath, err
   310  			}
   311  			defer srcFile.Close()
   312  
   313  			err = actor.addFileToZipFromFileSystem(
   314  				fullPath, srcFile, fileInfo,
   315  				resource, writer,
   316  			)
   317  			srcFile.Close()
   318  			if err != nil {
   319  				log.WithField("fullPath", fullPath).Errorln("zipping file:", err)
   320  				return zipPath, err
   321  			}
   322  		}
   323  	}
   324  
   325  	log.WithFields(log.Fields{
   326  		"zip_file_location": zipFile.Name(),
   327  		"zipped_file_count": len(filesToInclude),
   328  	}).Info("zip file created")
   329  	return zipPath, nil
   330  }
   331  
   332  func (Actor) addLinkToZipFromFileSystem(srcPath string,
   333  	fileInfo os.FileInfo, resource Resource,
   334  	zipFile *zip.Writer,
   335  ) error {
   336  	header, err := zip.FileInfoHeader(fileInfo)
   337  	if err != nil {
   338  		log.WithField("srcPath", srcPath).Errorln("getting file info in dir:", err)
   339  		return err
   340  	}
   341  
   342  	header.Name = resource.Filename
   343  	header.Method = zip.Deflate
   344  
   345  	log.WithFields(log.Fields{
   346  		"srcPath":  srcPath,
   347  		"destPath": header.Name,
   348  		"mode":     header.Mode().String(),
   349  	}).Debug("setting mode for file")
   350  
   351  	destFileWriter, err := zipFile.CreateHeader(header)
   352  	if err != nil {
   353  		log.Errorln("creating header:", err)
   354  		return err
   355  	}
   356  
   357  	pathInSymlink, err := os.Readlink(srcPath)
   358  	if err != nil {
   359  		return err
   360  	}
   361  	log.WithField("path", pathInSymlink).Debug("resolving symlink")
   362  	symLinkContents := strings.NewReader(pathInSymlink)
   363  	if _, err := io.Copy(destFileWriter, symLinkContents); err != nil {
   364  		log.WithField("srcPath", srcPath).Errorln("copying data in dir:", err)
   365  		return err
   366  	}
   367  
   368  	return nil
   369  }
   370  
   371  func (Actor) addFileToZipFromFileSystem(srcPath string,
   372  	srcFile io.Reader, fileInfo os.FileInfo, resource Resource,
   373  	zipFile *zip.Writer,
   374  ) error {
   375  	header, err := zip.FileInfoHeader(fileInfo)
   376  	if err != nil {
   377  		log.WithField("srcPath", srcPath).Errorln("getting file info in dir:", err)
   378  		return err
   379  	}
   380  
   381  	header.Name = resource.Filename
   382  
   383  	// An extra '/' indicates that this file is a directory
   384  	if fileInfo.IsDir() && !strings.HasSuffix(resource.Filename, "/") {
   385  		header.Name += "/"
   386  	}
   387  	header.Method = zip.Deflate
   388  	header.SetMode(resource.Mode)
   389  
   390  	log.WithFields(log.Fields{
   391  		"srcPath":  srcPath,
   392  		"destPath": header.Name,
   393  		"mode":     header.Mode().String(),
   394  	}).Debug("setting mode for file")
   395  
   396  	destFileWriter, err := zipFile.CreateHeader(header)
   397  	if err != nil {
   398  		log.Errorln("creating header:", err)
   399  		return err
   400  	}
   401  
   402  	if fileInfo.Mode().IsRegular() {
   403  		sum := sha1.New()
   404  		multi := io.MultiWriter(sum, destFileWriter)
   405  
   406  		if _, err := io.Copy(multi, srcFile); err != nil {
   407  			log.WithField("srcPath", srcPath).Errorln("copying data in dir:", err)
   408  			return err
   409  		}
   410  
   411  		if currentSum := fmt.Sprintf("%x", sum.Sum(nil)); resource.SHA1 != currentSum {
   412  			log.WithFields(log.Fields{
   413  				"expected":   resource.SHA1,
   414  				"currentSum": currentSum,
   415  			}).Error("setting mode for file")
   416  			return actionerror.FileChangedError{Filename: srcPath}
   417  		}
   418  	} else if fileInfo.Mode()&os.ModeSymlink == os.ModeSymlink {
   419  		_, err = io.Copy(destFileWriter, srcFile)
   420  		if err != nil {
   421  			return err
   422  		}
   423  	}
   424  
   425  	return nil
   426  }
   427  
   428  func (Actor) generateArchiveCFIgnoreMatcher(files []*zip.File) (*ignore.GitIgnore, error) {
   429  	for _, item := range files {
   430  		if strings.HasSuffix(item.Name, ".cfignore") {
   431  			fileReader, err := item.Open()
   432  			if err != nil {
   433  				return nil, err
   434  			}
   435  			defer fileReader.Close()
   436  
   437  			raw, err := ioutil.ReadAll(fileReader)
   438  			if err != nil {
   439  				return nil, err
   440  			}
   441  			s := append(DefaultIgnoreLines, strings.Split(string(raw), "\n")...)
   442  			return ignore.CompileIgnoreLines(s...)
   443  		}
   444  	}
   445  	return ignore.CompileIgnoreLines(DefaultIgnoreLines...)
   446  }
   447  
   448  func (actor Actor) generateDirectoryCFIgnoreMatcher(sourceDir string) (*ignore.GitIgnore, error) {
   449  	pathToCFIgnore := filepath.Join(sourceDir, ".cfignore")
   450  	log.WithFields(log.Fields{
   451  		"pathToCFIgnore": pathToCFIgnore,
   452  		"sourceDir":      sourceDir,
   453  	}).Debug("using ignore file")
   454  
   455  	additionalIgnoreLines := DefaultIgnoreLines
   456  
   457  	// If verbose logging has files in the current dir, ignore them
   458  	_, traceFiles := actor.Config.Verbose()
   459  	for _, traceFilePath := range traceFiles {
   460  		if relPath, err := filepath.Rel(sourceDir, traceFilePath); err == nil {
   461  			additionalIgnoreLines = append(additionalIgnoreLines, relPath)
   462  		}
   463  	}
   464  
   465  	log.Debugf("ignore rules: %v", additionalIgnoreLines)
   466  
   467  	if _, err := os.Stat(pathToCFIgnore); !os.IsNotExist(err) {
   468  		return ignore.CompileIgnoreFileAndLines(pathToCFIgnore, additionalIgnoreLines...)
   469  	}
   470  	return ignore.CompileIgnoreLines(additionalIgnoreLines...)
   471  }
   472  
   473  func (Actor) findInResources(path string, filesToInclude []Resource) (Resource, bool) {
   474  	for _, resource := range filesToInclude {
   475  		if resource.Filename == filepath.ToSlash(path) {
   476  			log.WithField("resource", resource.Filename).Debug("found resource in files to include")
   477  			return resource, true
   478  		}
   479  	}
   480  
   481  	log.WithField("path", path).Debug("did not find resource in files to include")
   482  	return Resource{}, false
   483  }
   484  
   485  func (Actor) newArchiveReader(archive *os.File) (*zip.Reader, error) {
   486  	info, err := archive.Stat()
   487  	if err != nil {
   488  		return nil, err
   489  	}
   490  
   491  	return ykk.NewReader(archive, info.Size())
   492  }
   493  
   494  func (actor Actor) CreateArchive(bitsPath string, resources []Resource) (io.ReadCloser, int64, error) {
   495  	archivePath, err := actor.ZipDirectoryResources(bitsPath, resources)
   496  	_ = err
   497  
   498  	return actor.ReadArchive(archivePath)
   499  }
   500  
   501  func (Actor) ReadArchive(archivePath string) (io.ReadCloser, int64, error) {
   502  	archive, err := os.Open(archivePath)
   503  	if err != nil {
   504  		log.WithField("archivePath", archivePath).Errorln("opening temp archive:", err)
   505  		return nil, -1, err
   506  	}
   507  
   508  	archiveInfo, err := archive.Stat()
   509  	if err != nil {
   510  		archive.Close()
   511  		log.WithField("archivePath", archivePath).Errorln("stat temp archive:", err)
   512  		return nil, -1, err
   513  	}
   514  
   515  	return archive, archiveInfo.Size(), nil
   516  }