github.com/ablease/cli@v6.37.1-0.20180613014814-3adbb7d7fb19+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 421 additionalIgnoreLines := DefaultIgnoreLines 422 423 // If verbose logging has files in the current dir, ignore them 424 _, traceFiles := actor.Config.Verbose() 425 for _, traceFilePath := range traceFiles { 426 if relPath, err := filepath.Rel(sourceDir, traceFilePath); err == nil { 427 additionalIgnoreLines = append(additionalIgnoreLines, relPath) 428 } 429 } 430 431 if _, err := os.Stat(pathToCFIgnore); !os.IsNotExist(err) { 432 return ignore.CompileIgnoreFileAndLines(pathToCFIgnore, additionalIgnoreLines...) 433 } else { 434 return ignore.CompileIgnoreLines(additionalIgnoreLines...) 435 } 436 } 437 438 func (Actor) findInResources(path string, filesToInclude []Resource) (Resource, bool) { 439 for _, resource := range filesToInclude { 440 if resource.Filename == filepath.ToSlash(path) { 441 log.WithField("resource", resource.Filename).Debug("found resource in files to include") 442 return resource, true 443 } 444 } 445 446 log.WithField("path", path).Debug("did not find resource in files to include") 447 return Resource{}, false 448 } 449 450 func (Actor) newArchiveReader(archive *os.File) (*zip.Reader, error) { 451 info, err := archive.Stat() 452 if err != nil { 453 return nil, err 454 } 455 456 return ykk.NewReader(archive, info.Size()) 457 }