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