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 }