github.com/ablease/cli@v6.37.1-0.20180613014814-3adbb7d7fb19+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  	return resources, nil
   101  }
   102  
   103  // GatherDirectoryResources returns a list of resources for a directory.
   104  func (actor Actor) GatherDirectoryResources(sourceDir string) ([]Resource, error) {
   105  	var (
   106  		resources []Resource
   107  		gitIgnore *ignore.GitIgnore
   108  	)
   109  
   110  	gitIgnore, err := actor.generateDirectoryCFIgnoreMatcher(sourceDir)
   111  	if err != nil {
   112  		log.Errorln("reading .cfignore file:", err)
   113  		return nil, err
   114  	}
   115  
   116  	evalDir, err := filepath.EvalSymlinks(sourceDir)
   117  	if err != nil {
   118  		log.Errorln("evaluating symlink:", err)
   119  		return nil, err
   120  	}
   121  
   122  	walkErr := filepath.Walk(evalDir, func(fullPath string, info os.FileInfo, err error) error {
   123  		if err != nil {
   124  			return err
   125  		}
   126  
   127  		relPath, err := filepath.Rel(evalDir, fullPath)
   128  		if err != nil {
   129  			return err
   130  		}
   131  
   132  		// if file ignored contine to the next file
   133  		if gitIgnore.MatchesPath(relPath) {
   134  			return nil
   135  		}
   136  
   137  		if relPath == "." {
   138  			return nil
   139  		}
   140  
   141  		resource := Resource{
   142  			Filename: filepath.ToSlash(relPath),
   143  		}
   144  
   145  		switch {
   146  		case info.IsDir():
   147  			// If the file is a directory
   148  			resource.Mode = DefaultFolderPermissions
   149  		case info.Mode()&os.ModeSymlink == os.ModeSymlink:
   150  			// If the file is a Symlink we just set the mode of the file
   151  			// We won't be using any sha information since we don't do
   152  			// any resource matching on symlinks.
   153  			resource.Mode = fixMode(info.Mode())
   154  		default:
   155  			// If the file is regular we want to open
   156  			// and calculate the sha of the file
   157  			file, err := os.Open(fullPath)
   158  			if err != nil {
   159  				return err
   160  			}
   161  			defer file.Close()
   162  
   163  			sum := sha1.New()
   164  			_, err = io.Copy(sum, file)
   165  			if err != nil {
   166  				return err
   167  			}
   168  
   169  			resource.Mode = fixMode(info.Mode())
   170  			resource.SHA1 = fmt.Sprintf("%x", sum.Sum(nil))
   171  			resource.Size = info.Size()
   172  		}
   173  
   174  		resources = append(resources, resource)
   175  		return nil
   176  	})
   177  
   178  	if len(resources) == 0 {
   179  		return nil, actionerror.EmptyDirectoryError{Path: sourceDir}
   180  	}
   181  
   182  	return resources, walkErr
   183  }
   184  
   185  // ZipArchiveResources zips an archive and a sorted (based on full
   186  // path/filename) list of resources and returns the location. On Windows, the
   187  // filemode for user is forced to be readable and executable.
   188  func (actor Actor) ZipArchiveResources(sourceArchivePath string, filesToInclude []Resource) (string, error) {
   189  	log.WithField("sourceArchive", sourceArchivePath).Info("zipping source files from archive")
   190  	zipFile, err := ioutil.TempFile("", "cf-cli-")
   191  	if err != nil {
   192  		return "", err
   193  	}
   194  	defer zipFile.Close()
   195  	zipPath := zipFile.Name()
   196  
   197  	writer := zip.NewWriter(zipFile)
   198  	defer writer.Close()
   199  
   200  	source, err := os.Open(sourceArchivePath)
   201  	if err != nil {
   202  		return zipPath, err
   203  	}
   204  	defer source.Close()
   205  
   206  	reader, err := actor.newArchiveReader(source)
   207  	if err != nil {
   208  		return zipPath, err
   209  	}
   210  
   211  	for _, archiveFile := range reader.File {
   212  		resource, ok := actor.findInResources(archiveFile.Name, filesToInclude)
   213  		if !ok {
   214  			log.WithField("archiveFileName", archiveFile.Name).Debug("skipping file")
   215  			continue
   216  		}
   217  
   218  		log.WithField("archiveFileName", archiveFile.Name).Debug("zipping file")
   219  		// archiveFile.Open opens the symlink file, not the file it points too
   220  		reader, openErr := archiveFile.Open()
   221  		if openErr != nil {
   222  			log.WithField("archiveFile", archiveFile.Name).Errorln("opening path in dir:", openErr)
   223  			return zipPath, openErr
   224  		}
   225  		defer reader.Close()
   226  
   227  		err = actor.addFileToZipFromFileSystem(
   228  			resource.Filename, reader, archiveFile.FileInfo(),
   229  			resource, writer,
   230  		)
   231  		if err != nil {
   232  			log.WithField("archiveFileName", archiveFile.Name).Errorln("zipping file:", err)
   233  			return zipPath, err
   234  		}
   235  		reader.Close()
   236  	}
   237  
   238  	log.WithFields(log.Fields{
   239  		"zip_file_location": zipFile.Name(),
   240  		"zipped_file_count": len(filesToInclude),
   241  	}).Info("zip file created")
   242  	return zipPath, nil
   243  }
   244  
   245  // ZipDirectoryResources zips a directory and a sorted (based on full
   246  // path/filename) list of resources and returns the location. On Windows, the
   247  // filemode for user is forced to be readable and executable.
   248  func (actor Actor) ZipDirectoryResources(sourceDir string, filesToInclude []Resource) (string, error) {
   249  	log.WithField("sourceDir", sourceDir).Info("zipping source files from directory")
   250  	zipFile, err := ioutil.TempFile("", "cf-cli-")
   251  	if err != nil {
   252  		return "", err
   253  	}
   254  	defer zipFile.Close()
   255  	zipPath := zipFile.Name()
   256  
   257  	writer := zip.NewWriter(zipFile)
   258  	defer writer.Close()
   259  
   260  	for _, resource := range filesToInclude {
   261  		fullPath := filepath.Join(sourceDir, resource.Filename)
   262  		log.WithField("fullPath", fullPath).Debug("zipping file")
   263  
   264  		fileInfo, err := os.Lstat(fullPath)
   265  		if err != nil {
   266  			log.WithField("fullPath", fullPath).Errorln("stat error in dir:", err)
   267  			return zipPath, err
   268  		}
   269  
   270  		log.WithField("file-mode", fileInfo.Mode().String()).Debug("resource file info")
   271  		if fileInfo.Mode()&os.ModeSymlink == os.ModeSymlink {
   272  			// we need to user os.Readlink to read a symlink file from a directory
   273  			err = actor.addLinkToZipFromFileSystem(fullPath, fileInfo, resource, writer)
   274  			if err != nil {
   275  				log.WithField("fullPath", fullPath).Errorln("zipping file:", err)
   276  				return zipPath, err
   277  			}
   278  		} else {
   279  			srcFile, err := os.Open(fullPath)
   280  			defer srcFile.Close()
   281  			if err != nil {
   282  				log.WithField("fullPath", fullPath).Errorln("opening path in dir:", err)
   283  				return zipPath, err
   284  			}
   285  
   286  			err = actor.addFileToZipFromFileSystem(
   287  				fullPath, srcFile, fileInfo,
   288  				resource, writer,
   289  			)
   290  			srcFile.Close()
   291  			if err != nil {
   292  				log.WithField("fullPath", fullPath).Errorln("zipping file:", err)
   293  				return zipPath, err
   294  			}
   295  		}
   296  	}
   297  
   298  	log.WithFields(log.Fields{
   299  		"zip_file_location": zipFile.Name(),
   300  		"zipped_file_count": len(filesToInclude),
   301  	}).Info("zip file created")
   302  	return zipPath, nil
   303  }
   304  
   305  func (Actor) addLinkToZipFromFileSystem(srcPath string,
   306  	fileInfo os.FileInfo, resource Resource,
   307  	zipFile *zip.Writer,
   308  ) error {
   309  	header, err := zip.FileInfoHeader(fileInfo)
   310  	if err != nil {
   311  		log.WithField("srcPath", srcPath).Errorln("getting file info in dir:", err)
   312  		return err
   313  	}
   314  
   315  	header.Name = resource.Filename
   316  	header.Method = zip.Deflate
   317  
   318  	log.WithFields(log.Fields{
   319  		"srcPath":  srcPath,
   320  		"destPath": header.Name,
   321  		"mode":     header.Mode().String(),
   322  	}).Debug("setting mode for file")
   323  
   324  	destFileWriter, err := zipFile.CreateHeader(header)
   325  	if err != nil {
   326  		log.Errorln("creating header:", err)
   327  		return err
   328  	}
   329  
   330  	pathInSymlink, err := os.Readlink(srcPath)
   331  	if err != nil {
   332  		return err
   333  	}
   334  	log.WithField("path", pathInSymlink).Debug("resolving symlink")
   335  	symLinkContents := strings.NewReader(pathInSymlink)
   336  	if _, err := io.Copy(destFileWriter, symLinkContents); err != nil {
   337  		log.WithField("srcPath", srcPath).Errorln("copying data in dir:", err)
   338  		return err
   339  	}
   340  
   341  	return nil
   342  }
   343  
   344  func (Actor) addFileToZipFromFileSystem(srcPath string,
   345  	srcFile io.Reader, fileInfo os.FileInfo, resource Resource,
   346  	zipFile *zip.Writer,
   347  ) error {
   348  	header, err := zip.FileInfoHeader(fileInfo)
   349  	if err != nil {
   350  		log.WithField("srcPath", srcPath).Errorln("getting file info in dir:", err)
   351  		return err
   352  	}
   353  
   354  	header.Name = resource.Filename
   355  
   356  	// An extra '/' indicates that this file is a directory
   357  	if fileInfo.IsDir() && !strings.HasSuffix(resource.Filename, "/") {
   358  		header.Name += "/"
   359  	}
   360  	header.Method = zip.Deflate
   361  	header.SetMode(resource.Mode)
   362  
   363  	log.WithFields(log.Fields{
   364  		"srcPath":  srcPath,
   365  		"destPath": header.Name,
   366  		"mode":     header.Mode().String(),
   367  	}).Debug("setting mode for file")
   368  
   369  	destFileWriter, err := zipFile.CreateHeader(header)
   370  	if err != nil {
   371  		log.Errorln("creating header:", err)
   372  		return err
   373  	}
   374  
   375  	if fileInfo.Mode().IsRegular() {
   376  		sum := sha1.New()
   377  		multi := io.MultiWriter(sum, destFileWriter)
   378  
   379  		if _, err := io.Copy(multi, srcFile); err != nil {
   380  			log.WithField("srcPath", srcPath).Errorln("copying data in dir:", err)
   381  			return err
   382  		}
   383  
   384  		if currentSum := fmt.Sprintf("%x", sum.Sum(nil)); resource.SHA1 != currentSum {
   385  			log.WithFields(log.Fields{
   386  				"expected":   resource.SHA1,
   387  				"currentSum": currentSum,
   388  			}).Error("setting mode for file")
   389  			return actionerror.FileChangedError{Filename: srcPath}
   390  		}
   391  	} else if fileInfo.Mode()&os.ModeSymlink == os.ModeSymlink {
   392  		io.Copy(destFileWriter, srcFile)
   393  	}
   394  
   395  	return nil
   396  }
   397  
   398  func (Actor) generateArchiveCFIgnoreMatcher(files []*zip.File) (*ignore.GitIgnore, error) {
   399  	for _, item := range files {
   400  		if strings.HasSuffix(item.Name, ".cfignore") {
   401  			fileReader, err := item.Open()
   402  			if err != nil {
   403  				return nil, err
   404  			}
   405  			defer fileReader.Close()
   406  
   407  			raw, err := ioutil.ReadAll(fileReader)
   408  			if err != nil {
   409  				return nil, err
   410  			}
   411  			s := append(DefaultIgnoreLines, strings.Split(string(raw), "\n")...)
   412  			return ignore.CompileIgnoreLines(s...)
   413  		}
   414  	}
   415  	return ignore.CompileIgnoreLines(DefaultIgnoreLines...)
   416  }
   417  
   418  func (actor Actor) generateDirectoryCFIgnoreMatcher(sourceDir string) (*ignore.GitIgnore, error) {
   419  	pathToCFIgnore := filepath.Join(sourceDir, ".cfignore")
   420  
   421  	additionalIgnoreLines := DefaultIgnoreLines
   422  
   423  	// If verbose logging has files in the current dir, ignore them
   424  	_, traceFiles := actor.Config.Verbose()
   425  	for _, traceFilePath := range traceFiles {
   426  		if relPath, err := filepath.Rel(sourceDir, traceFilePath); err == nil {
   427  			additionalIgnoreLines = append(additionalIgnoreLines, relPath)
   428  		}
   429  	}
   430  
   431  	if _, err := os.Stat(pathToCFIgnore); !os.IsNotExist(err) {
   432  		return ignore.CompileIgnoreFileAndLines(pathToCFIgnore, additionalIgnoreLines...)
   433  	} else {
   434  		return ignore.CompileIgnoreLines(additionalIgnoreLines...)
   435  	}
   436  }
   437  
   438  func (Actor) findInResources(path string, filesToInclude []Resource) (Resource, bool) {
   439  	for _, resource := range filesToInclude {
   440  		if resource.Filename == filepath.ToSlash(path) {
   441  			log.WithField("resource", resource.Filename).Debug("found resource in files to include")
   442  			return resource, true
   443  		}
   444  	}
   445  
   446  	log.WithField("path", path).Debug("did not find resource in files to include")
   447  	return Resource{}, false
   448  }
   449  
   450  func (Actor) newArchiveReader(archive *os.File) (*zip.Reader, error) {
   451  	info, err := archive.Stat()
   452  	if err != nil {
   453  		return nil, err
   454  	}
   455  
   456  	return ykk.NewReader(archive, info.Size())
   457  }