github.com/loggregator/cli@v6.33.1-0.20180224010324-82334f081791+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 196 writer := zip.NewWriter(zipFile) 197 defer writer.Close() 198 199 source, err := os.Open(sourceArchivePath) 200 if err != nil { 201 return "", err 202 } 203 defer source.Close() 204 205 reader, err := actor.newArchiveReader(source) 206 if err != nil { 207 return "", err 208 } 209 210 for _, archiveFile := range reader.File { 211 resource, ok := actor.findInResources(archiveFile.Name, filesToInclude) 212 if !ok { 213 log.WithField("archiveFileName", archiveFile.Name).Debug("skipping file") 214 continue 215 } 216 217 log.WithField("archiveFileName", archiveFile.Name).Debug("zipping file") 218 // archiveFile.Open opens the symlink file, not the file it points too 219 reader, openErr := archiveFile.Open() 220 if openErr != nil { 221 log.WithField("archiveFile", archiveFile.Name).Errorln("opening path in dir:", openErr) 222 return "", openErr 223 } 224 defer reader.Close() 225 226 err = actor.addFileToZipFromFileSystem( 227 resource.Filename, reader, archiveFile.FileInfo(), 228 resource, writer, 229 ) 230 if err != nil { 231 log.WithField("archiveFileName", archiveFile.Name).Errorln("zipping file:", err) 232 return "", err 233 } 234 reader.Close() 235 } 236 237 log.WithFields(log.Fields{ 238 "zip_file_location": zipFile.Name(), 239 "zipped_file_count": len(filesToInclude), 240 }).Info("zip file created") 241 return zipFile.Name(), nil 242 } 243 244 // ZipDirectoryResources zips a directory and a sorted (based on full 245 // path/filename) list of resources and returns the location. On Windows, the 246 // filemode for user is forced to be readable and executable. 247 func (actor Actor) ZipDirectoryResources(sourceDir string, filesToInclude []Resource) (string, error) { 248 log.WithField("sourceDir", sourceDir).Info("zipping source files from directory") 249 zipFile, err := ioutil.TempFile("", "cf-cli-") 250 if err != nil { 251 return "", err 252 } 253 defer zipFile.Close() 254 255 writer := zip.NewWriter(zipFile) 256 defer writer.Close() 257 258 for _, resource := range filesToInclude { 259 fullPath := filepath.Join(sourceDir, resource.Filename) 260 log.WithField("fullPath", fullPath).Debug("zipping file") 261 262 fileInfo, err := os.Lstat(fullPath) 263 if err != nil { 264 log.WithField("fullPath", fullPath).Errorln("stat error in dir:", err) 265 return "", err 266 } 267 268 log.WithField("file-mode", fileInfo.Mode().String()).Debug("resource file info") 269 if fileInfo.Mode()&os.ModeSymlink == os.ModeSymlink { 270 // we need to user os.Readlink to read a symlink file from a directory 271 err = actor.addLinkToZipFromFileSystem(fullPath, fileInfo, resource, writer) 272 if err != nil { 273 log.WithField("fullPath", fullPath).Errorln("zipping file:", err) 274 return "", err 275 } 276 } else { 277 srcFile, err := os.Open(fullPath) 278 defer srcFile.Close() 279 if err != nil { 280 log.WithField("fullPath", fullPath).Errorln("opening path in dir:", err) 281 return "", err 282 } 283 284 err = actor.addFileToZipFromFileSystem( 285 fullPath, srcFile, fileInfo, 286 resource, writer, 287 ) 288 srcFile.Close() 289 if err != nil { 290 log.WithField("fullPath", fullPath).Errorln("zipping file:", err) 291 return "", err 292 } 293 } 294 } 295 296 log.WithFields(log.Fields{ 297 "zip_file_location": zipFile.Name(), 298 "zipped_file_count": len(filesToInclude), 299 }).Info("zip file created") 300 return zipFile.Name(), nil 301 } 302 303 func (Actor) addLinkToZipFromFileSystem(srcPath string, 304 fileInfo os.FileInfo, resource Resource, 305 zipFile *zip.Writer, 306 ) error { 307 header, err := zip.FileInfoHeader(fileInfo) 308 if err != nil { 309 log.WithField("srcPath", srcPath).Errorln("getting file info in dir:", err) 310 return err 311 } 312 313 header.Name = resource.Filename 314 header.Method = zip.Deflate 315 316 log.WithFields(log.Fields{ 317 "srcPath": srcPath, 318 "destPath": header.Name, 319 "mode": header.Mode().String(), 320 }).Debug("setting mode for file") 321 322 destFileWriter, err := zipFile.CreateHeader(header) 323 if err != nil { 324 log.Errorln("creating header:", err) 325 return err 326 } 327 328 pathInSymlink, err := os.Readlink(srcPath) 329 if err != nil { 330 return err 331 } 332 log.WithField("path", pathInSymlink).Debug("resolving symlink") 333 symLinkContents := strings.NewReader(pathInSymlink) 334 if _, err := io.Copy(destFileWriter, symLinkContents); err != nil { 335 log.WithField("srcPath", srcPath).Errorln("copying data in dir:", err) 336 return err 337 } 338 339 return nil 340 } 341 342 func (Actor) addFileToZipFromFileSystem(srcPath string, 343 srcFile io.Reader, fileInfo os.FileInfo, resource Resource, 344 zipFile *zip.Writer, 345 ) error { 346 header, err := zip.FileInfoHeader(fileInfo) 347 if err != nil { 348 log.WithField("srcPath", srcPath).Errorln("getting file info in dir:", err) 349 return err 350 } 351 352 header.Name = resource.Filename 353 354 // An extra '/' indicates that this file is a directory 355 if fileInfo.IsDir() && !strings.HasSuffix(resource.Filename, "/") { 356 header.Name += "/" 357 } 358 header.Method = zip.Deflate 359 header.SetMode(resource.Mode) 360 361 log.WithFields(log.Fields{ 362 "srcPath": srcPath, 363 "destPath": header.Name, 364 "mode": header.Mode().String(), 365 }).Debug("setting mode for file") 366 367 destFileWriter, err := zipFile.CreateHeader(header) 368 if err != nil { 369 log.Errorln("creating header:", err) 370 return err 371 } 372 373 if fileInfo.Mode().IsRegular() { 374 sum := sha1.New() 375 multi := io.MultiWriter(sum, destFileWriter) 376 377 if _, err := io.Copy(multi, srcFile); err != nil { 378 log.WithField("srcPath", srcPath).Errorln("copying data in dir:", err) 379 return err 380 } 381 382 if currentSum := fmt.Sprintf("%x", sum.Sum(nil)); resource.SHA1 != currentSum { 383 log.WithFields(log.Fields{ 384 "expected": resource.SHA1, 385 "currentSum": currentSum, 386 }).Error("setting mode for file") 387 return actionerror.FileChangedError{Filename: srcPath} 388 } 389 } else if fileInfo.Mode()&os.ModeSymlink == os.ModeSymlink { 390 io.Copy(destFileWriter, srcFile) 391 } 392 393 return nil 394 } 395 396 func (Actor) generateArchiveCFIgnoreMatcher(files []*zip.File) (*ignore.GitIgnore, error) { 397 for _, item := range files { 398 if strings.HasSuffix(item.Name, ".cfignore") { 399 fileReader, err := item.Open() 400 if err != nil { 401 return nil, err 402 } 403 defer fileReader.Close() 404 405 raw, err := ioutil.ReadAll(fileReader) 406 if err != nil { 407 return nil, err 408 } 409 s := append(DefaultIgnoreLines, strings.Split(string(raw), "\n")...) 410 return ignore.CompileIgnoreLines(s...) 411 } 412 } 413 return ignore.CompileIgnoreLines(DefaultIgnoreLines...) 414 } 415 416 func (actor Actor) generateDirectoryCFIgnoreMatcher(sourceDir string) (*ignore.GitIgnore, error) { 417 pathToCFIgnore := filepath.Join(sourceDir, ".cfignore") 418 419 additionalIgnoreLines := DefaultIgnoreLines 420 421 // If verbose logging has files in the current dir, ignore them 422 _, traceFiles := actor.Config.Verbose() 423 for _, traceFilePath := range traceFiles { 424 if relPath, err := filepath.Rel(sourceDir, traceFilePath); err == nil { 425 additionalIgnoreLines = append(additionalIgnoreLines, relPath) 426 } 427 } 428 429 if _, err := os.Stat(pathToCFIgnore); !os.IsNotExist(err) { 430 return ignore.CompileIgnoreFileAndLines(pathToCFIgnore, additionalIgnoreLines...) 431 } else { 432 return ignore.CompileIgnoreLines(additionalIgnoreLines...) 433 } 434 } 435 436 func (Actor) findInResources(path string, filesToInclude []Resource) (Resource, bool) { 437 for _, resource := range filesToInclude { 438 if resource.Filename == filepath.ToSlash(path) { 439 log.WithField("resource", resource.Filename).Debug("found resource in files to include") 440 return resource, true 441 } 442 } 443 444 log.WithField("path", path).Debug("did not find resource in files to include") 445 return Resource{}, false 446 } 447 448 func (Actor) newArchiveReader(archive *os.File) (*zip.Reader, error) { 449 info, err := archive.Stat() 450 if err != nil { 451 return nil, err 452 } 453 454 return ykk.NewReader(archive, info.Size()) 455 }