github.com/wanddynosios/cli/v8@v8.7.9-0.20240221182337-1a92e3a7017f/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/api/cloudcontroller/ccv3" 14 15 "code.cloudfoundry.org/cli/actor/actionerror" 16 "code.cloudfoundry.org/ykk" 17 ignore "github.com/sabhiram/go-gitignore" 18 log "github.com/sirupsen/logrus" 19 ) 20 21 const ( 22 DefaultFolderPermissions = 0755 23 DefaultArchiveFilePermissions = 0744 24 MaxResourceMatchChunkSize = 1000 25 ) 26 27 var DefaultIgnoreLines = []string{ 28 ".cfignore", 29 ".DS_Store", 30 ".git", 31 ".gitignore", 32 ".hg", 33 ".svn", 34 "_darcs", 35 "manifest.yaml", 36 "manifest.yml", 37 } 38 39 type Resource struct { 40 Filename string `json:"fn"` 41 Mode os.FileMode `json:"mode"` 42 SHA1 string `json:"sha1"` 43 Size int64 `json:"size"` 44 } 45 46 type V3Resource ccv3.Resource 47 48 // ToV3Resource converts a sharedaction Resource to V3 Resource format 49 func (r Resource) ToV3Resource() V3Resource { 50 return V3Resource{ 51 FilePath: r.Filename, 52 Mode: r.Mode, 53 Checksum: ccv3.Checksum{Value: r.SHA1}, 54 SizeInBytes: r.Size, 55 } 56 } 57 58 // ToV2Resource converts a V3 Resource to V2 Resource format 59 func (r V3Resource) ToV2Resource() Resource { 60 return Resource{ 61 Filename: r.FilePath, 62 Mode: r.Mode, 63 SHA1: r.Checksum.Value, 64 Size: r.SizeInBytes, 65 } 66 } 67 68 // GatherArchiveResources returns a list of resources for an archive. 69 func (actor Actor) GatherArchiveResources(archivePath string) ([]Resource, error) { 70 var resources []Resource 71 72 archive, err := os.Open(archivePath) 73 if err != nil { 74 return nil, err 75 } 76 defer archive.Close() 77 78 reader, err := actor.newArchiveReader(archive) 79 if err != nil { 80 return nil, err 81 } 82 83 gitIgnore, err := actor.generateArchiveCFIgnoreMatcher(reader.File) 84 if err != nil { 85 log.Errorln("reading .cfignore file:", err) 86 return nil, err 87 } 88 89 for _, archivedFile := range reader.File { 90 filename := filepath.ToSlash(archivedFile.Name) 91 if gitIgnore.MatchesPath(filename) { 92 continue 93 } 94 95 resource := Resource{Filename: filename} 96 info := archivedFile.FileInfo() 97 98 switch { 99 case info.IsDir(): 100 resource.Mode = DefaultFolderPermissions 101 case info.Mode()&os.ModeSymlink == os.ModeSymlink: 102 resource.Mode = info.Mode() 103 default: 104 fileReader, err := archivedFile.Open() 105 if err != nil { 106 return nil, err 107 } 108 defer fileReader.Close() 109 110 hash := sha1.New() 111 112 _, err = io.Copy(hash, fileReader) 113 if err != nil { 114 return nil, err 115 } 116 117 resource.Mode = DefaultArchiveFilePermissions 118 resource.SHA1 = fmt.Sprintf("%x", hash.Sum(nil)) 119 resource.Size = archivedFile.FileInfo().Size() 120 } 121 122 resources = append(resources, resource) 123 } 124 if len(resources) <= 1 { 125 return nil, actionerror.EmptyArchiveError{Path: archivePath} 126 } 127 return resources, nil 128 } 129 130 // GatherDirectoryResources returns a list of resources for a directory. 131 func (actor Actor) GatherDirectoryResources(sourceDir string) ([]Resource, error) { 132 var ( 133 resources []Resource 134 gitIgnore *ignore.GitIgnore 135 ) 136 137 gitIgnore, err := actor.generateDirectoryCFIgnoreMatcher(sourceDir) 138 if err != nil { 139 log.Errorln("reading .cfignore file:", err) 140 return nil, err 141 } 142 143 evalDir, err := filepath.EvalSymlinks(sourceDir) 144 if err != nil { 145 log.Errorln("evaluating symlink:", err) 146 return nil, err 147 } 148 149 walkErr := filepath.Walk(evalDir, func(fullPath string, info os.FileInfo, err error) error { 150 if err != nil { 151 return err 152 } 153 154 relPath, err := filepath.Rel(evalDir, fullPath) 155 if err != nil { 156 return err 157 } 158 159 // if file ignored continue to the next file 160 if gitIgnore.MatchesPath(relPath) { 161 return nil 162 } 163 164 if relPath == "." { 165 return nil 166 } 167 168 resource := Resource{ 169 Filename: filepath.ToSlash(relPath), 170 } 171 172 switch { 173 case info.IsDir(): 174 // If the file is a directory 175 resource.Mode = DefaultFolderPermissions 176 case info.Mode()&os.ModeSymlink == os.ModeSymlink: 177 // If the file is a Symlink we just set the mode of the file 178 // We won't be using any sha information since we don't do 179 // any resource matching on symlinks. 180 resource.Mode = fixMode(info.Mode()) 181 default: 182 // If the file is regular we want to open 183 // and calculate the sha of the file 184 file, err := os.Open(fullPath) 185 if err != nil { 186 return err 187 } 188 defer file.Close() 189 190 sum := sha1.New() 191 _, err = io.Copy(sum, file) 192 if err != nil { 193 return err 194 } 195 196 resource.Mode = fixMode(info.Mode()) 197 resource.SHA1 = fmt.Sprintf("%x", sum.Sum(nil)) 198 resource.Size = info.Size() 199 } 200 201 resources = append(resources, resource) 202 return nil 203 }) 204 205 if len(resources) == 0 { 206 return nil, actionerror.EmptyDirectoryError{Path: sourceDir} 207 } 208 209 return resources, walkErr 210 } 211 212 // ZipArchiveResources zips an archive and a sorted (based on full 213 // path/filename) list of resources and returns the location. On Windows, the 214 // filemode for user is forced to be readable and executable. 215 func (actor Actor) ZipArchiveResources(sourceArchivePath string, filesToInclude []Resource) (string, error) { 216 log.WithField("sourceArchive", sourceArchivePath).Info("zipping source files from archive") 217 zipFile, err := ioutil.TempFile("", "cf-cli-") 218 if err != nil { 219 return "", err 220 } 221 defer zipFile.Close() 222 zipPath := zipFile.Name() 223 224 writer := zip.NewWriter(zipFile) 225 defer writer.Close() 226 227 source, err := os.Open(sourceArchivePath) 228 if err != nil { 229 return zipPath, err 230 } 231 defer source.Close() 232 233 reader, err := actor.newArchiveReader(source) 234 if err != nil { 235 return zipPath, err 236 } 237 238 for _, archiveFile := range reader.File { 239 resource, ok := actor.findInResources(archiveFile.Name, filesToInclude) 240 if !ok { 241 log.WithField("archiveFileName", archiveFile.Name).Debug("skipping file") 242 continue 243 } 244 245 log.WithField("archiveFileName", archiveFile.Name).Debug("zipping file") 246 // archiveFile.Open opens the symlink file, not the file it points too 247 reader, openErr := archiveFile.Open() 248 if openErr != nil { 249 log.WithField("archiveFile", archiveFile.Name).Errorln("opening path in dir:", openErr) 250 return zipPath, openErr 251 } 252 defer reader.Close() 253 254 err = actor.addFileToZipFromFileSystem( 255 resource.Filename, reader, archiveFile.FileInfo(), 256 resource, writer, 257 ) 258 if err != nil { 259 log.WithField("archiveFileName", archiveFile.Name).Errorln("zipping file:", err) 260 return zipPath, err 261 } 262 reader.Close() 263 } 264 265 log.WithFields(log.Fields{ 266 "zip_file_location": zipFile.Name(), 267 "zipped_file_count": len(filesToInclude), 268 }).Info("zip file created") 269 return zipPath, nil 270 } 271 272 // ZipDirectoryResources zips a directory and a sorted (based on full 273 // path/filename) list of resources and returns the location. On Windows, the 274 // filemode for user is forced to be readable and executable. 275 func (actor Actor) ZipDirectoryResources(sourceDir string, filesToInclude []Resource) (string, error) { 276 log.WithField("sourceDir", sourceDir).Info("zipping source files from directory") 277 zipFile, err := ioutil.TempFile("", "cf-cli-") 278 if err != nil { 279 return "", err 280 } 281 defer zipFile.Close() 282 zipPath := zipFile.Name() 283 284 writer := zip.NewWriter(zipFile) 285 defer writer.Close() 286 287 for _, resource := range filesToInclude { 288 fullPath := filepath.Join(sourceDir, resource.Filename) 289 log.WithField("fullPath", fullPath).Debug("zipping file") 290 291 fileInfo, err := os.Lstat(fullPath) 292 if err != nil { 293 log.WithField("fullPath", fullPath).Errorln("stat error in dir:", err) 294 return zipPath, err 295 } 296 297 log.WithField("file-mode", fileInfo.Mode().String()).Debug("resource file info") 298 if fileInfo.Mode()&os.ModeSymlink == os.ModeSymlink { 299 // we need to user os.Readlink to read a symlink file from a directory 300 err = actor.addLinkToZipFromFileSystem(fullPath, fileInfo, resource, writer) 301 if err != nil { 302 log.WithField("fullPath", fullPath).Errorln("zipping file:", err) 303 return zipPath, err 304 } 305 } else { 306 srcFile, err := os.Open(fullPath) 307 if err != nil { 308 log.WithField("fullPath", fullPath).Errorln("opening path in dir:", err) 309 return zipPath, err 310 } 311 defer srcFile.Close() 312 313 err = actor.addFileToZipFromFileSystem( 314 fullPath, srcFile, fileInfo, 315 resource, writer, 316 ) 317 srcFile.Close() 318 if err != nil { 319 log.WithField("fullPath", fullPath).Errorln("zipping file:", err) 320 return zipPath, err 321 } 322 } 323 } 324 325 log.WithFields(log.Fields{ 326 "zip_file_location": zipFile.Name(), 327 "zipped_file_count": len(filesToInclude), 328 }).Info("zip file created") 329 return zipPath, nil 330 } 331 332 func (Actor) addLinkToZipFromFileSystem(srcPath string, 333 fileInfo os.FileInfo, resource Resource, 334 zipFile *zip.Writer, 335 ) error { 336 header, err := zip.FileInfoHeader(fileInfo) 337 if err != nil { 338 log.WithField("srcPath", srcPath).Errorln("getting file info in dir:", err) 339 return err 340 } 341 342 header.Name = resource.Filename 343 header.Method = zip.Deflate 344 345 log.WithFields(log.Fields{ 346 "srcPath": srcPath, 347 "destPath": header.Name, 348 "mode": header.Mode().String(), 349 }).Debug("setting mode for file") 350 351 destFileWriter, err := zipFile.CreateHeader(header) 352 if err != nil { 353 log.Errorln("creating header:", err) 354 return err 355 } 356 357 pathInSymlink, err := os.Readlink(srcPath) 358 if err != nil { 359 return err 360 } 361 log.WithField("path", pathInSymlink).Debug("resolving symlink") 362 symLinkContents := strings.NewReader(pathInSymlink) 363 if _, err := io.Copy(destFileWriter, symLinkContents); err != nil { 364 log.WithField("srcPath", srcPath).Errorln("copying data in dir:", err) 365 return err 366 } 367 368 return nil 369 } 370 371 func (Actor) addFileToZipFromFileSystem(srcPath string, 372 srcFile io.Reader, fileInfo os.FileInfo, resource Resource, 373 zipFile *zip.Writer, 374 ) error { 375 header, err := zip.FileInfoHeader(fileInfo) 376 if err != nil { 377 log.WithField("srcPath", srcPath).Errorln("getting file info in dir:", err) 378 return err 379 } 380 381 header.Name = resource.Filename 382 383 // An extra '/' indicates that this file is a directory 384 if fileInfo.IsDir() && !strings.HasSuffix(resource.Filename, "/") { 385 header.Name += "/" 386 } 387 header.Method = zip.Deflate 388 header.SetMode(resource.Mode) 389 390 log.WithFields(log.Fields{ 391 "srcPath": srcPath, 392 "destPath": header.Name, 393 "mode": header.Mode().String(), 394 }).Debug("setting mode for file") 395 396 destFileWriter, err := zipFile.CreateHeader(header) 397 if err != nil { 398 log.Errorln("creating header:", err) 399 return err 400 } 401 402 if fileInfo.Mode().IsRegular() { 403 sum := sha1.New() 404 multi := io.MultiWriter(sum, destFileWriter) 405 406 if _, err := io.Copy(multi, srcFile); err != nil { 407 log.WithField("srcPath", srcPath).Errorln("copying data in dir:", err) 408 return err 409 } 410 411 if currentSum := fmt.Sprintf("%x", sum.Sum(nil)); resource.SHA1 != currentSum { 412 log.WithFields(log.Fields{ 413 "expected": resource.SHA1, 414 "currentSum": currentSum, 415 }).Error("setting mode for file") 416 return actionerror.FileChangedError{Filename: srcPath} 417 } 418 } else if fileInfo.Mode()&os.ModeSymlink == os.ModeSymlink { 419 _, err = io.Copy(destFileWriter, srcFile) 420 if err != nil { 421 return err 422 } 423 } 424 425 return nil 426 } 427 428 func (Actor) generateArchiveCFIgnoreMatcher(files []*zip.File) (*ignore.GitIgnore, error) { 429 for _, item := range files { 430 if strings.HasSuffix(item.Name, ".cfignore") { 431 fileReader, err := item.Open() 432 if err != nil { 433 return nil, err 434 } 435 defer fileReader.Close() 436 437 raw, err := ioutil.ReadAll(fileReader) 438 if err != nil { 439 return nil, err 440 } 441 s := append(DefaultIgnoreLines, strings.Split(string(raw), "\n")...) 442 return ignore.CompileIgnoreLines(s...) 443 } 444 } 445 return ignore.CompileIgnoreLines(DefaultIgnoreLines...) 446 } 447 448 func (actor Actor) generateDirectoryCFIgnoreMatcher(sourceDir string) (*ignore.GitIgnore, error) { 449 pathToCFIgnore := filepath.Join(sourceDir, ".cfignore") 450 log.WithFields(log.Fields{ 451 "pathToCFIgnore": pathToCFIgnore, 452 "sourceDir": sourceDir, 453 }).Debug("using ignore file") 454 455 additionalIgnoreLines := DefaultIgnoreLines 456 457 // If verbose logging has files in the current dir, ignore them 458 _, traceFiles := actor.Config.Verbose() 459 for _, traceFilePath := range traceFiles { 460 if relPath, err := filepath.Rel(sourceDir, traceFilePath); err == nil { 461 additionalIgnoreLines = append(additionalIgnoreLines, relPath) 462 } 463 } 464 465 log.Debugf("ignore rules: %v", additionalIgnoreLines) 466 467 if _, err := os.Stat(pathToCFIgnore); !os.IsNotExist(err) { 468 return ignore.CompileIgnoreFileAndLines(pathToCFIgnore, additionalIgnoreLines...) 469 } 470 return ignore.CompileIgnoreLines(additionalIgnoreLines...) 471 } 472 473 func (Actor) findInResources(path string, filesToInclude []Resource) (Resource, bool) { 474 for _, resource := range filesToInclude { 475 if resource.Filename == filepath.ToSlash(path) { 476 log.WithField("resource", resource.Filename).Debug("found resource in files to include") 477 return resource, true 478 } 479 } 480 481 log.WithField("path", path).Debug("did not find resource in files to include") 482 return Resource{}, false 483 } 484 485 func (Actor) newArchiveReader(archive *os.File) (*zip.Reader, error) { 486 info, err := archive.Stat() 487 if err != nil { 488 return nil, err 489 } 490 491 return ykk.NewReader(archive, info.Size()) 492 } 493 494 func (actor Actor) CreateArchive(bitsPath string, resources []Resource) (io.ReadCloser, int64, error) { 495 archivePath, err := actor.ZipDirectoryResources(bitsPath, resources) 496 _ = err 497 498 return actor.ReadArchive(archivePath) 499 } 500 501 func (Actor) ReadArchive(archivePath string) (io.ReadCloser, int64, error) { 502 archive, err := os.Open(archivePath) 503 if err != nil { 504 log.WithField("archivePath", archivePath).Errorln("opening temp archive:", err) 505 return nil, -1, err 506 } 507 508 archiveInfo, err := archive.Stat() 509 if err != nil { 510 archive.Close() 511 log.WithField("archivePath", archivePath).Errorln("stat temp archive:", err) 512 return nil, -1, err 513 } 514 515 return archive, archiveInfo.Size(), nil 516 }