github.com/cloudfoundry-attic/cli-with-i18n@v6.32.1-0.20171002233121-7401370d3b85+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/ykk"
    14  	ignore "github.com/sabhiram/go-gitignore"
    15  	log "github.com/sirupsen/logrus"
    16  )
    17  
    18  const (
    19  	DefaultFolderPermissions      = 0755
    20  	DefaultArchiveFilePermissions = 0744
    21  	MaxResourceMatchChunkSize     = 1000
    22  )
    23  
    24  var DefaultIgnoreLines = []string{
    25  	".cfignore",
    26  	".DS_Store",
    27  	".git",
    28  	".gitignore",
    29  	".hg",
    30  	".svn",
    31  	"_darcs",
    32  	"manifest.yaml",
    33  	"manifest.yml",
    34  }
    35  
    36  type FileChangedError struct {
    37  	Filename string
    38  }
    39  
    40  func (e FileChangedError) Error() string {
    41  	return fmt.Sprint("SHA1 mismatch for:", e.Filename)
    42  }
    43  
    44  type EmptyDirectoryError struct {
    45  	Path string
    46  }
    47  
    48  func (e EmptyDirectoryError) Error() string {
    49  	return fmt.Sprint(e.Path, "is empty")
    50  }
    51  
    52  type Resource struct {
    53  	Filename string      `json:"fn"`
    54  	Mode     os.FileMode `json:"mode"`
    55  	SHA1     string      `json:"sha1"`
    56  	Size     int64       `json:"size"`
    57  }
    58  
    59  // GatherArchiveResources returns a list of resources for an archive.
    60  func (actor Actor) GatherArchiveResources(archivePath string) ([]Resource, error) {
    61  	var resources []Resource
    62  
    63  	archive, err := os.Open(archivePath)
    64  	if err != nil {
    65  		return nil, err
    66  	}
    67  	defer archive.Close()
    68  
    69  	reader, err := actor.newArchiveReader(archive)
    70  	if err != nil {
    71  		return nil, err
    72  	}
    73  
    74  	gitIgnore, err := actor.generateArchiveCFIgnoreMatcher(reader.File)
    75  	if err != nil {
    76  		log.Errorln("reading .cfignore file:", err)
    77  		return nil, err
    78  	}
    79  
    80  	for _, archivedFile := range reader.File {
    81  		filename := filepath.ToSlash(archivedFile.Name)
    82  		if gitIgnore.MatchesPath(filename) {
    83  			continue
    84  		}
    85  
    86  		resource := Resource{Filename: filename}
    87  		if archivedFile.FileInfo().IsDir() {
    88  			resource.Mode = DefaultFolderPermissions
    89  		} else {
    90  			fileReader, err := archivedFile.Open()
    91  			if err != nil {
    92  				return nil, err
    93  			}
    94  			defer fileReader.Close()
    95  
    96  			hash := sha1.New()
    97  
    98  			_, err = io.Copy(hash, fileReader)
    99  			if err != nil {
   100  				return nil, err
   101  			}
   102  
   103  			resource.Mode = DefaultArchiveFilePermissions
   104  			resource.SHA1 = fmt.Sprintf("%x", hash.Sum(nil))
   105  			resource.Size = archivedFile.FileInfo().Size()
   106  		}
   107  		resources = append(resources, resource)
   108  	}
   109  	return resources, nil
   110  }
   111  
   112  // GatherDirectoryResources returns a list of resources for a directory.
   113  func (actor Actor) GatherDirectoryResources(sourceDir string) ([]Resource, error) {
   114  	var (
   115  		resources []Resource
   116  		gitIgnore *ignore.GitIgnore
   117  	)
   118  
   119  	gitIgnore, err := actor.generateDirectoryCFIgnoreMatcher(sourceDir)
   120  	if err != nil {
   121  		log.Errorln("reading .cfignore file:", err)
   122  		return nil, err
   123  	}
   124  
   125  	walkErr := filepath.Walk(sourceDir, func(path string, info os.FileInfo, err error) error {
   126  		if err != nil {
   127  			return err
   128  		}
   129  
   130  		// if file ignored contine to the next file
   131  		if gitIgnore.MatchesPath(path) {
   132  			return nil
   133  		}
   134  
   135  		relPath, err := filepath.Rel(sourceDir, path)
   136  		if err != nil {
   137  			return err
   138  		}
   139  
   140  		if relPath == "." {
   141  			return nil
   142  		}
   143  
   144  		resource := Resource{
   145  			Filename: filepath.ToSlash(relPath),
   146  		}
   147  
   148  		if info.IsDir() {
   149  			resource.Mode = DefaultFolderPermissions
   150  		} else {
   151  			file, err := os.Open(path)
   152  			if err != nil {
   153  				return err
   154  			}
   155  			defer file.Close()
   156  
   157  			sum := sha1.New()
   158  			_, err = io.Copy(sum, file)
   159  			if err != nil {
   160  				return err
   161  			}
   162  
   163  			resource.Mode = fixMode(info.Mode())
   164  			resource.SHA1 = fmt.Sprintf("%x", sum.Sum(nil))
   165  			resource.Size = info.Size()
   166  		}
   167  		resources = append(resources, resource)
   168  		return nil
   169  	})
   170  
   171  	if len(resources) == 0 {
   172  		return nil, EmptyDirectoryError{Path: sourceDir}
   173  	}
   174  
   175  	return resources, walkErr
   176  }
   177  
   178  // ZipArchiveResources zips an archive and a sorted (based on full
   179  // path/filename) list of resources and returns the location. On Windows, the
   180  // filemode for user is forced to be readable and executable.
   181  func (actor Actor) ZipArchiveResources(sourceArchivePath string, filesToInclude []Resource) (string, error) {
   182  	log.WithField("sourceArchive", sourceArchivePath).Info("zipping source files from archive")
   183  	zipFile, err := ioutil.TempFile("", "cf-cli-")
   184  	if err != nil {
   185  		return "", err
   186  	}
   187  	defer zipFile.Close()
   188  
   189  	writer := zip.NewWriter(zipFile)
   190  	defer writer.Close()
   191  
   192  	source, err := os.Open(sourceArchivePath)
   193  	if err != nil {
   194  		return "", err
   195  	}
   196  	defer source.Close()
   197  
   198  	reader, err := actor.newArchiveReader(source)
   199  	if err != nil {
   200  		return "", err
   201  	}
   202  
   203  	for _, archiveFile := range reader.File {
   204  		resource, ok := actor.findInResources(archiveFile.Name, filesToInclude)
   205  		if !ok {
   206  			log.WithField("archiveFileName", archiveFile.Name).Debug("skipping file")
   207  			continue
   208  		}
   209  
   210  		log.WithField("archiveFileName", archiveFile.Name).Debug("zipping file")
   211  		reader, openErr := archiveFile.Open()
   212  		if openErr != nil {
   213  			log.WithField("archiveFile", archiveFile.Name).Errorln("opening path in dir:", openErr)
   214  			return "", openErr
   215  		}
   216  
   217  		err = actor.addFileToZipFromFileSystem(
   218  			resource.Filename, reader, archiveFile.FileInfo(),
   219  			resource.Filename, resource.SHA1, resource.Mode, writer,
   220  		)
   221  		if err != nil {
   222  			log.WithField("archiveFileName", archiveFile.Name).Errorln("zipping file:", err)
   223  			return "", err
   224  		}
   225  	}
   226  
   227  	log.WithFields(log.Fields{
   228  		"zip_file_location": zipFile.Name(),
   229  		"zipped_file_count": len(filesToInclude),
   230  	}).Info("zip file created")
   231  	return zipFile.Name(), nil
   232  }
   233  
   234  // ZipDirectoryResources zips a directory and a sorted (based on full
   235  // path/filename) list of resources and returns the location. On Windows, the
   236  // filemode for user is forced to be readable and executable.
   237  func (actor Actor) ZipDirectoryResources(sourceDir string, filesToInclude []Resource) (string, error) {
   238  	log.WithField("sourceDir", sourceDir).Info("zipping source files from directory")
   239  	zipFile, err := ioutil.TempFile("", "cf-cli-")
   240  	if err != nil {
   241  		return "", err
   242  	}
   243  	defer zipFile.Close()
   244  
   245  	writer := zip.NewWriter(zipFile)
   246  	defer writer.Close()
   247  
   248  	for _, resource := range filesToInclude {
   249  		fullPath := filepath.Join(sourceDir, resource.Filename)
   250  		log.WithField("fullPath", fullPath).Debug("zipping file")
   251  
   252  		srcFile, err := os.Open(fullPath)
   253  		if err != nil {
   254  			log.WithField("fullPath", fullPath).Errorln("opening path in dir:", err)
   255  			return "", err
   256  		}
   257  
   258  		fileInfo, err := srcFile.Stat()
   259  		if err != nil {
   260  			log.WithField("fullPath", fullPath).Errorln("stat error in dir:", err)
   261  			return "", err
   262  		}
   263  
   264  		err = actor.addFileToZipFromFileSystem(
   265  			fullPath, srcFile, fileInfo,
   266  			resource.Filename, resource.SHA1, resource.Mode, writer,
   267  		)
   268  		if err != nil {
   269  			log.WithField("fullPath", fullPath).Errorln("zipping file:", err)
   270  			return "", err
   271  		}
   272  	}
   273  
   274  	log.WithFields(log.Fields{
   275  		"zip_file_location": zipFile.Name(),
   276  		"zipped_file_count": len(filesToInclude),
   277  	}).Info("zip file created")
   278  	return zipFile.Name(), nil
   279  }
   280  
   281  func (Actor) addFileToZipFromFileSystem(
   282  	srcPath string, srcFile io.ReadCloser, fileInfo os.FileInfo,
   283  	destPath string, sha1Sum string, mode os.FileMode, zipFile *zip.Writer,
   284  ) error {
   285  	defer srcFile.Close()
   286  
   287  	header, err := zip.FileInfoHeader(fileInfo)
   288  	if err != nil {
   289  		log.WithField("srcPath", srcPath).Errorln("getting file info in dir:", err)
   290  		return err
   291  	}
   292  
   293  	// An extra '/' indicates that this file is a directory
   294  	if fileInfo.IsDir() && !strings.HasSuffix(destPath, "/") {
   295  		destPath += "/"
   296  	}
   297  
   298  	header.Name = destPath
   299  	header.Method = zip.Deflate
   300  
   301  	header.SetMode(mode)
   302  	log.WithFields(log.Fields{
   303  		"srcPath":  srcPath,
   304  		"destPath": destPath,
   305  		"mode":     mode,
   306  	}).Debug("setting mode for file")
   307  
   308  	destFileWriter, err := zipFile.CreateHeader(header)
   309  	if err != nil {
   310  		log.Errorln("creating header:", err)
   311  		return err
   312  	}
   313  
   314  	if !fileInfo.IsDir() {
   315  		sum := sha1.New()
   316  
   317  		multi := io.MultiWriter(sum, destFileWriter)
   318  		if _, err := io.Copy(multi, srcFile); err != nil {
   319  			log.WithField("srcPath", srcPath).Errorln("copying data in dir:", err)
   320  			return err
   321  		}
   322  
   323  		if currentSum := fmt.Sprintf("%x", sum.Sum(nil)); sha1Sum != currentSum {
   324  			log.WithFields(log.Fields{
   325  				"expected":   sha1Sum,
   326  				"currentSum": currentSum,
   327  			}).Error("setting mode for file")
   328  			return FileChangedError{Filename: srcPath}
   329  		}
   330  	}
   331  
   332  	return nil
   333  }
   334  
   335  func (Actor) generateArchiveCFIgnoreMatcher(files []*zip.File) (*ignore.GitIgnore, error) {
   336  	for _, item := range files {
   337  		if strings.HasSuffix(item.Name, ".cfignore") {
   338  			fileReader, err := item.Open()
   339  			if err != nil {
   340  				return nil, err
   341  			}
   342  			defer fileReader.Close()
   343  
   344  			raw, err := ioutil.ReadAll(fileReader)
   345  			if err != nil {
   346  				return nil, err
   347  			}
   348  			s := append(DefaultIgnoreLines, strings.Split(string(raw), "\n")...)
   349  			return ignore.CompileIgnoreLines(s...)
   350  		}
   351  	}
   352  	return ignore.CompileIgnoreLines(DefaultIgnoreLines...)
   353  }
   354  
   355  func (actor Actor) generateDirectoryCFIgnoreMatcher(sourceDir string) (*ignore.GitIgnore, error) {
   356  	pathToCFIgnore := filepath.Join(sourceDir, ".cfignore")
   357  
   358  	additionalIgnoreLines := DefaultIgnoreLines
   359  
   360  	// If verbose logging has files in the current dir, ignore them
   361  	_, traceFiles := actor.Config.Verbose()
   362  	for _, traceFilePath := range traceFiles {
   363  		if relPath, err := filepath.Rel(sourceDir, traceFilePath); err == nil {
   364  			additionalIgnoreLines = append(additionalIgnoreLines, relPath)
   365  		}
   366  	}
   367  
   368  	if _, err := os.Stat(pathToCFIgnore); !os.IsNotExist(err) {
   369  		return ignore.CompileIgnoreFileAndLines(pathToCFIgnore, additionalIgnoreLines...)
   370  	} else {
   371  		return ignore.CompileIgnoreLines(additionalIgnoreLines...)
   372  	}
   373  }
   374  
   375  func (Actor) findInResources(path string, filesToInclude []Resource) (Resource, bool) {
   376  	for _, resource := range filesToInclude {
   377  		if resource.Filename == filepath.ToSlash(path) {
   378  			log.WithField("resource", resource.Filename).Debug("found resource in files to include")
   379  			return resource, true
   380  		}
   381  	}
   382  
   383  	log.WithField("path", path).Debug("did not find resource in files to include")
   384  	return Resource{}, false
   385  }
   386  
   387  func (Actor) newArchiveReader(archive *os.File) (*zip.Reader, error) {
   388  	info, err := archive.Stat()
   389  	if err != nil {
   390  		return nil, err
   391  	}
   392  
   393  	return ykk.NewReader(archive, info.Size())
   394  }