github.com/loggregator/cli@v6.33.1-0.20180224010324-82334f081791+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  
   196  	writer := zip.NewWriter(zipFile)
   197  	defer writer.Close()
   198  
   199  	source, err := os.Open(sourceArchivePath)
   200  	if err != nil {
   201  		return "", err
   202  	}
   203  	defer source.Close()
   204  
   205  	reader, err := actor.newArchiveReader(source)
   206  	if err != nil {
   207  		return "", err
   208  	}
   209  
   210  	for _, archiveFile := range reader.File {
   211  		resource, ok := actor.findInResources(archiveFile.Name, filesToInclude)
   212  		if !ok {
   213  			log.WithField("archiveFileName", archiveFile.Name).Debug("skipping file")
   214  			continue
   215  		}
   216  
   217  		log.WithField("archiveFileName", archiveFile.Name).Debug("zipping file")
   218  		// archiveFile.Open opens the symlink file, not the file it points too
   219  		reader, openErr := archiveFile.Open()
   220  		if openErr != nil {
   221  			log.WithField("archiveFile", archiveFile.Name).Errorln("opening path in dir:", openErr)
   222  			return "", openErr
   223  		}
   224  		defer reader.Close()
   225  
   226  		err = actor.addFileToZipFromFileSystem(
   227  			resource.Filename, reader, archiveFile.FileInfo(),
   228  			resource, writer,
   229  		)
   230  		if err != nil {
   231  			log.WithField("archiveFileName", archiveFile.Name).Errorln("zipping file:", err)
   232  			return "", err
   233  		}
   234  		reader.Close()
   235  	}
   236  
   237  	log.WithFields(log.Fields{
   238  		"zip_file_location": zipFile.Name(),
   239  		"zipped_file_count": len(filesToInclude),
   240  	}).Info("zip file created")
   241  	return zipFile.Name(), nil
   242  }
   243  
   244  // ZipDirectoryResources zips a directory and a sorted (based on full
   245  // path/filename) list of resources and returns the location. On Windows, the
   246  // filemode for user is forced to be readable and executable.
   247  func (actor Actor) ZipDirectoryResources(sourceDir string, filesToInclude []Resource) (string, error) {
   248  	log.WithField("sourceDir", sourceDir).Info("zipping source files from directory")
   249  	zipFile, err := ioutil.TempFile("", "cf-cli-")
   250  	if err != nil {
   251  		return "", err
   252  	}
   253  	defer zipFile.Close()
   254  
   255  	writer := zip.NewWriter(zipFile)
   256  	defer writer.Close()
   257  
   258  	for _, resource := range filesToInclude {
   259  		fullPath := filepath.Join(sourceDir, resource.Filename)
   260  		log.WithField("fullPath", fullPath).Debug("zipping file")
   261  
   262  		fileInfo, err := os.Lstat(fullPath)
   263  		if err != nil {
   264  			log.WithField("fullPath", fullPath).Errorln("stat error in dir:", err)
   265  			return "", err
   266  		}
   267  
   268  		log.WithField("file-mode", fileInfo.Mode().String()).Debug("resource file info")
   269  		if fileInfo.Mode()&os.ModeSymlink == os.ModeSymlink {
   270  			// we need to user os.Readlink to read a symlink file from a directory
   271  			err = actor.addLinkToZipFromFileSystem(fullPath, fileInfo, resource, writer)
   272  			if err != nil {
   273  				log.WithField("fullPath", fullPath).Errorln("zipping file:", err)
   274  				return "", err
   275  			}
   276  		} else {
   277  			srcFile, err := os.Open(fullPath)
   278  			defer srcFile.Close()
   279  			if err != nil {
   280  				log.WithField("fullPath", fullPath).Errorln("opening path in dir:", err)
   281  				return "", err
   282  			}
   283  
   284  			err = actor.addFileToZipFromFileSystem(
   285  				fullPath, srcFile, fileInfo,
   286  				resource, writer,
   287  			)
   288  			srcFile.Close()
   289  			if err != nil {
   290  				log.WithField("fullPath", fullPath).Errorln("zipping file:", err)
   291  				return "", err
   292  			}
   293  		}
   294  	}
   295  
   296  	log.WithFields(log.Fields{
   297  		"zip_file_location": zipFile.Name(),
   298  		"zipped_file_count": len(filesToInclude),
   299  	}).Info("zip file created")
   300  	return zipFile.Name(), nil
   301  }
   302  
   303  func (Actor) addLinkToZipFromFileSystem(srcPath string,
   304  	fileInfo os.FileInfo, resource Resource,
   305  	zipFile *zip.Writer,
   306  ) error {
   307  	header, err := zip.FileInfoHeader(fileInfo)
   308  	if err != nil {
   309  		log.WithField("srcPath", srcPath).Errorln("getting file info in dir:", err)
   310  		return err
   311  	}
   312  
   313  	header.Name = resource.Filename
   314  	header.Method = zip.Deflate
   315  
   316  	log.WithFields(log.Fields{
   317  		"srcPath":  srcPath,
   318  		"destPath": header.Name,
   319  		"mode":     header.Mode().String(),
   320  	}).Debug("setting mode for file")
   321  
   322  	destFileWriter, err := zipFile.CreateHeader(header)
   323  	if err != nil {
   324  		log.Errorln("creating header:", err)
   325  		return err
   326  	}
   327  
   328  	pathInSymlink, err := os.Readlink(srcPath)
   329  	if err != nil {
   330  		return err
   331  	}
   332  	log.WithField("path", pathInSymlink).Debug("resolving symlink")
   333  	symLinkContents := strings.NewReader(pathInSymlink)
   334  	if _, err := io.Copy(destFileWriter, symLinkContents); err != nil {
   335  		log.WithField("srcPath", srcPath).Errorln("copying data in dir:", err)
   336  		return err
   337  	}
   338  
   339  	return nil
   340  }
   341  
   342  func (Actor) addFileToZipFromFileSystem(srcPath string,
   343  	srcFile io.Reader, fileInfo os.FileInfo, resource Resource,
   344  	zipFile *zip.Writer,
   345  ) error {
   346  	header, err := zip.FileInfoHeader(fileInfo)
   347  	if err != nil {
   348  		log.WithField("srcPath", srcPath).Errorln("getting file info in dir:", err)
   349  		return err
   350  	}
   351  
   352  	header.Name = resource.Filename
   353  
   354  	// An extra '/' indicates that this file is a directory
   355  	if fileInfo.IsDir() && !strings.HasSuffix(resource.Filename, "/") {
   356  		header.Name += "/"
   357  	}
   358  	header.Method = zip.Deflate
   359  	header.SetMode(resource.Mode)
   360  
   361  	log.WithFields(log.Fields{
   362  		"srcPath":  srcPath,
   363  		"destPath": header.Name,
   364  		"mode":     header.Mode().String(),
   365  	}).Debug("setting mode for file")
   366  
   367  	destFileWriter, err := zipFile.CreateHeader(header)
   368  	if err != nil {
   369  		log.Errorln("creating header:", err)
   370  		return err
   371  	}
   372  
   373  	if fileInfo.Mode().IsRegular() {
   374  		sum := sha1.New()
   375  		multi := io.MultiWriter(sum, destFileWriter)
   376  
   377  		if _, err := io.Copy(multi, srcFile); err != nil {
   378  			log.WithField("srcPath", srcPath).Errorln("copying data in dir:", err)
   379  			return err
   380  		}
   381  
   382  		if currentSum := fmt.Sprintf("%x", sum.Sum(nil)); resource.SHA1 != currentSum {
   383  			log.WithFields(log.Fields{
   384  				"expected":   resource.SHA1,
   385  				"currentSum": currentSum,
   386  			}).Error("setting mode for file")
   387  			return actionerror.FileChangedError{Filename: srcPath}
   388  		}
   389  	} else if fileInfo.Mode()&os.ModeSymlink == os.ModeSymlink {
   390  		io.Copy(destFileWriter, srcFile)
   391  	}
   392  
   393  	return nil
   394  }
   395  
   396  func (Actor) generateArchiveCFIgnoreMatcher(files []*zip.File) (*ignore.GitIgnore, error) {
   397  	for _, item := range files {
   398  		if strings.HasSuffix(item.Name, ".cfignore") {
   399  			fileReader, err := item.Open()
   400  			if err != nil {
   401  				return nil, err
   402  			}
   403  			defer fileReader.Close()
   404  
   405  			raw, err := ioutil.ReadAll(fileReader)
   406  			if err != nil {
   407  				return nil, err
   408  			}
   409  			s := append(DefaultIgnoreLines, strings.Split(string(raw), "\n")...)
   410  			return ignore.CompileIgnoreLines(s...)
   411  		}
   412  	}
   413  	return ignore.CompileIgnoreLines(DefaultIgnoreLines...)
   414  }
   415  
   416  func (actor Actor) generateDirectoryCFIgnoreMatcher(sourceDir string) (*ignore.GitIgnore, error) {
   417  	pathToCFIgnore := filepath.Join(sourceDir, ".cfignore")
   418  
   419  	additionalIgnoreLines := DefaultIgnoreLines
   420  
   421  	// If verbose logging has files in the current dir, ignore them
   422  	_, traceFiles := actor.Config.Verbose()
   423  	for _, traceFilePath := range traceFiles {
   424  		if relPath, err := filepath.Rel(sourceDir, traceFilePath); err == nil {
   425  			additionalIgnoreLines = append(additionalIgnoreLines, relPath)
   426  		}
   427  	}
   428  
   429  	if _, err := os.Stat(pathToCFIgnore); !os.IsNotExist(err) {
   430  		return ignore.CompileIgnoreFileAndLines(pathToCFIgnore, additionalIgnoreLines...)
   431  	} else {
   432  		return ignore.CompileIgnoreLines(additionalIgnoreLines...)
   433  	}
   434  }
   435  
   436  func (Actor) findInResources(path string, filesToInclude []Resource) (Resource, bool) {
   437  	for _, resource := range filesToInclude {
   438  		if resource.Filename == filepath.ToSlash(path) {
   439  			log.WithField("resource", resource.Filename).Debug("found resource in files to include")
   440  			return resource, true
   441  		}
   442  	}
   443  
   444  	log.WithField("path", path).Debug("did not find resource in files to include")
   445  	return Resource{}, false
   446  }
   447  
   448  func (Actor) newArchiveReader(archive *os.File) (*zip.Reader, error) {
   449  	info, err := archive.Stat()
   450  	if err != nil {
   451  		return nil, err
   452  	}
   453  
   454  	return ykk.NewReader(archive, info.Size())
   455  }