github.com/randomtask1155/cli@v6.41.1-0.20181227003417-a98eed78cbde+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 log.WithFields(log.Fields{ 421 "pathToCFIgnore": pathToCFIgnore, 422 "sourceDir": sourceDir, 423 }).Debug("using ignore file") 424 425 additionalIgnoreLines := DefaultIgnoreLines 426 427 // If verbose logging has files in the current dir, ignore them 428 _, traceFiles := actor.Config.Verbose() 429 for _, traceFilePath := range traceFiles { 430 if relPath, err := filepath.Rel(sourceDir, traceFilePath); err == nil { 431 additionalIgnoreLines = append(additionalIgnoreLines, relPath) 432 } 433 } 434 435 log.Debugf("ignore rules: %v", additionalIgnoreLines) 436 437 if _, err := os.Stat(pathToCFIgnore); !os.IsNotExist(err) { 438 return ignore.CompileIgnoreFileAndLines(pathToCFIgnore, additionalIgnoreLines...) 439 } 440 return ignore.CompileIgnoreLines(additionalIgnoreLines...) 441 } 442 443 func (Actor) findInResources(path string, filesToInclude []Resource) (Resource, bool) { 444 for _, resource := range filesToInclude { 445 if resource.Filename == filepath.ToSlash(path) { 446 log.WithField("resource", resource.Filename).Debug("found resource in files to include") 447 return resource, true 448 } 449 } 450 451 log.WithField("path", path).Debug("did not find resource in files to include") 452 return Resource{}, false 453 } 454 455 func (Actor) newArchiveReader(archive *os.File) (*zip.Reader, error) { 456 info, err := archive.Stat() 457 if err != nil { 458 return nil, err 459 } 460 461 return ykk.NewReader(archive, info.Size()) 462 } 463 464 func (actor Actor) CreateArchive(bitsPath string, resources []Resource) (io.ReadCloser, int64, error) { 465 archivePath, err := actor.ZipDirectoryResources(bitsPath, resources) 466 _ = err 467 468 return actor.ReadArchive(archivePath) 469 } 470 471 func (Actor) ReadArchive(archivePath string) (io.ReadCloser, int64, error) { 472 archive, err := os.Open(archivePath) 473 if err != nil { 474 log.WithField("archivePath", archivePath).Errorln("opening temp archive:", err) 475 return nil, -1, err 476 } 477 478 archiveInfo, err := archive.Stat() 479 if err != nil { 480 archive.Close() 481 log.WithField("archivePath", archivePath).Errorln("stat temp archive:", err) 482 return nil, -1, err 483 } 484 485 return archive, archiveInfo.Size(), nil 486 }