github.com/cloudfoundry-attic/cli-with-i18n@v6.32.1-0.20171002233121-7401370d3b85+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/ykk" 14 ignore "github.com/sabhiram/go-gitignore" 15 log "github.com/sirupsen/logrus" 16 ) 17 18 const ( 19 DefaultFolderPermissions = 0755 20 DefaultArchiveFilePermissions = 0744 21 MaxResourceMatchChunkSize = 1000 22 ) 23 24 var DefaultIgnoreLines = []string{ 25 ".cfignore", 26 ".DS_Store", 27 ".git", 28 ".gitignore", 29 ".hg", 30 ".svn", 31 "_darcs", 32 "manifest.yaml", 33 "manifest.yml", 34 } 35 36 type FileChangedError struct { 37 Filename string 38 } 39 40 func (e FileChangedError) Error() string { 41 return fmt.Sprint("SHA1 mismatch for:", e.Filename) 42 } 43 44 type EmptyDirectoryError struct { 45 Path string 46 } 47 48 func (e EmptyDirectoryError) Error() string { 49 return fmt.Sprint(e.Path, "is empty") 50 } 51 52 type Resource struct { 53 Filename string `json:"fn"` 54 Mode os.FileMode `json:"mode"` 55 SHA1 string `json:"sha1"` 56 Size int64 `json:"size"` 57 } 58 59 // GatherArchiveResources returns a list of resources for an archive. 60 func (actor Actor) GatherArchiveResources(archivePath string) ([]Resource, error) { 61 var resources []Resource 62 63 archive, err := os.Open(archivePath) 64 if err != nil { 65 return nil, err 66 } 67 defer archive.Close() 68 69 reader, err := actor.newArchiveReader(archive) 70 if err != nil { 71 return nil, err 72 } 73 74 gitIgnore, err := actor.generateArchiveCFIgnoreMatcher(reader.File) 75 if err != nil { 76 log.Errorln("reading .cfignore file:", err) 77 return nil, err 78 } 79 80 for _, archivedFile := range reader.File { 81 filename := filepath.ToSlash(archivedFile.Name) 82 if gitIgnore.MatchesPath(filename) { 83 continue 84 } 85 86 resource := Resource{Filename: filename} 87 if archivedFile.FileInfo().IsDir() { 88 resource.Mode = DefaultFolderPermissions 89 } else { 90 fileReader, err := archivedFile.Open() 91 if err != nil { 92 return nil, err 93 } 94 defer fileReader.Close() 95 96 hash := sha1.New() 97 98 _, err = io.Copy(hash, fileReader) 99 if err != nil { 100 return nil, err 101 } 102 103 resource.Mode = DefaultArchiveFilePermissions 104 resource.SHA1 = fmt.Sprintf("%x", hash.Sum(nil)) 105 resource.Size = archivedFile.FileInfo().Size() 106 } 107 resources = append(resources, resource) 108 } 109 return resources, nil 110 } 111 112 // GatherDirectoryResources returns a list of resources for a directory. 113 func (actor Actor) GatherDirectoryResources(sourceDir string) ([]Resource, error) { 114 var ( 115 resources []Resource 116 gitIgnore *ignore.GitIgnore 117 ) 118 119 gitIgnore, err := actor.generateDirectoryCFIgnoreMatcher(sourceDir) 120 if err != nil { 121 log.Errorln("reading .cfignore file:", err) 122 return nil, err 123 } 124 125 walkErr := filepath.Walk(sourceDir, func(path string, info os.FileInfo, err error) error { 126 if err != nil { 127 return err 128 } 129 130 // if file ignored contine to the next file 131 if gitIgnore.MatchesPath(path) { 132 return nil 133 } 134 135 relPath, err := filepath.Rel(sourceDir, path) 136 if err != nil { 137 return err 138 } 139 140 if relPath == "." { 141 return nil 142 } 143 144 resource := Resource{ 145 Filename: filepath.ToSlash(relPath), 146 } 147 148 if info.IsDir() { 149 resource.Mode = DefaultFolderPermissions 150 } else { 151 file, err := os.Open(path) 152 if err != nil { 153 return err 154 } 155 defer file.Close() 156 157 sum := sha1.New() 158 _, err = io.Copy(sum, file) 159 if err != nil { 160 return err 161 } 162 163 resource.Mode = fixMode(info.Mode()) 164 resource.SHA1 = fmt.Sprintf("%x", sum.Sum(nil)) 165 resource.Size = info.Size() 166 } 167 resources = append(resources, resource) 168 return nil 169 }) 170 171 if len(resources) == 0 { 172 return nil, EmptyDirectoryError{Path: sourceDir} 173 } 174 175 return resources, walkErr 176 } 177 178 // ZipArchiveResources zips an archive and a sorted (based on full 179 // path/filename) list of resources and returns the location. On Windows, the 180 // filemode for user is forced to be readable and executable. 181 func (actor Actor) ZipArchiveResources(sourceArchivePath string, filesToInclude []Resource) (string, error) { 182 log.WithField("sourceArchive", sourceArchivePath).Info("zipping source files from archive") 183 zipFile, err := ioutil.TempFile("", "cf-cli-") 184 if err != nil { 185 return "", err 186 } 187 defer zipFile.Close() 188 189 writer := zip.NewWriter(zipFile) 190 defer writer.Close() 191 192 source, err := os.Open(sourceArchivePath) 193 if err != nil { 194 return "", err 195 } 196 defer source.Close() 197 198 reader, err := actor.newArchiveReader(source) 199 if err != nil { 200 return "", err 201 } 202 203 for _, archiveFile := range reader.File { 204 resource, ok := actor.findInResources(archiveFile.Name, filesToInclude) 205 if !ok { 206 log.WithField("archiveFileName", archiveFile.Name).Debug("skipping file") 207 continue 208 } 209 210 log.WithField("archiveFileName", archiveFile.Name).Debug("zipping file") 211 reader, openErr := archiveFile.Open() 212 if openErr != nil { 213 log.WithField("archiveFile", archiveFile.Name).Errorln("opening path in dir:", openErr) 214 return "", openErr 215 } 216 217 err = actor.addFileToZipFromFileSystem( 218 resource.Filename, reader, archiveFile.FileInfo(), 219 resource.Filename, resource.SHA1, resource.Mode, writer, 220 ) 221 if err != nil { 222 log.WithField("archiveFileName", archiveFile.Name).Errorln("zipping file:", err) 223 return "", err 224 } 225 } 226 227 log.WithFields(log.Fields{ 228 "zip_file_location": zipFile.Name(), 229 "zipped_file_count": len(filesToInclude), 230 }).Info("zip file created") 231 return zipFile.Name(), nil 232 } 233 234 // ZipDirectoryResources zips a directory and a sorted (based on full 235 // path/filename) list of resources and returns the location. On Windows, the 236 // filemode for user is forced to be readable and executable. 237 func (actor Actor) ZipDirectoryResources(sourceDir string, filesToInclude []Resource) (string, error) { 238 log.WithField("sourceDir", sourceDir).Info("zipping source files from directory") 239 zipFile, err := ioutil.TempFile("", "cf-cli-") 240 if err != nil { 241 return "", err 242 } 243 defer zipFile.Close() 244 245 writer := zip.NewWriter(zipFile) 246 defer writer.Close() 247 248 for _, resource := range filesToInclude { 249 fullPath := filepath.Join(sourceDir, resource.Filename) 250 log.WithField("fullPath", fullPath).Debug("zipping file") 251 252 srcFile, err := os.Open(fullPath) 253 if err != nil { 254 log.WithField("fullPath", fullPath).Errorln("opening path in dir:", err) 255 return "", err 256 } 257 258 fileInfo, err := srcFile.Stat() 259 if err != nil { 260 log.WithField("fullPath", fullPath).Errorln("stat error in dir:", err) 261 return "", err 262 } 263 264 err = actor.addFileToZipFromFileSystem( 265 fullPath, srcFile, fileInfo, 266 resource.Filename, resource.SHA1, resource.Mode, writer, 267 ) 268 if err != nil { 269 log.WithField("fullPath", fullPath).Errorln("zipping file:", err) 270 return "", err 271 } 272 } 273 274 log.WithFields(log.Fields{ 275 "zip_file_location": zipFile.Name(), 276 "zipped_file_count": len(filesToInclude), 277 }).Info("zip file created") 278 return zipFile.Name(), nil 279 } 280 281 func (Actor) addFileToZipFromFileSystem( 282 srcPath string, srcFile io.ReadCloser, fileInfo os.FileInfo, 283 destPath string, sha1Sum string, mode os.FileMode, zipFile *zip.Writer, 284 ) error { 285 defer srcFile.Close() 286 287 header, err := zip.FileInfoHeader(fileInfo) 288 if err != nil { 289 log.WithField("srcPath", srcPath).Errorln("getting file info in dir:", err) 290 return err 291 } 292 293 // An extra '/' indicates that this file is a directory 294 if fileInfo.IsDir() && !strings.HasSuffix(destPath, "/") { 295 destPath += "/" 296 } 297 298 header.Name = destPath 299 header.Method = zip.Deflate 300 301 header.SetMode(mode) 302 log.WithFields(log.Fields{ 303 "srcPath": srcPath, 304 "destPath": destPath, 305 "mode": mode, 306 }).Debug("setting mode for file") 307 308 destFileWriter, err := zipFile.CreateHeader(header) 309 if err != nil { 310 log.Errorln("creating header:", err) 311 return err 312 } 313 314 if !fileInfo.IsDir() { 315 sum := sha1.New() 316 317 multi := io.MultiWriter(sum, destFileWriter) 318 if _, err := io.Copy(multi, srcFile); err != nil { 319 log.WithField("srcPath", srcPath).Errorln("copying data in dir:", err) 320 return err 321 } 322 323 if currentSum := fmt.Sprintf("%x", sum.Sum(nil)); sha1Sum != currentSum { 324 log.WithFields(log.Fields{ 325 "expected": sha1Sum, 326 "currentSum": currentSum, 327 }).Error("setting mode for file") 328 return FileChangedError{Filename: srcPath} 329 } 330 } 331 332 return nil 333 } 334 335 func (Actor) generateArchiveCFIgnoreMatcher(files []*zip.File) (*ignore.GitIgnore, error) { 336 for _, item := range files { 337 if strings.HasSuffix(item.Name, ".cfignore") { 338 fileReader, err := item.Open() 339 if err != nil { 340 return nil, err 341 } 342 defer fileReader.Close() 343 344 raw, err := ioutil.ReadAll(fileReader) 345 if err != nil { 346 return nil, err 347 } 348 s := append(DefaultIgnoreLines, strings.Split(string(raw), "\n")...) 349 return ignore.CompileIgnoreLines(s...) 350 } 351 } 352 return ignore.CompileIgnoreLines(DefaultIgnoreLines...) 353 } 354 355 func (actor Actor) generateDirectoryCFIgnoreMatcher(sourceDir string) (*ignore.GitIgnore, error) { 356 pathToCFIgnore := filepath.Join(sourceDir, ".cfignore") 357 358 additionalIgnoreLines := DefaultIgnoreLines 359 360 // If verbose logging has files in the current dir, ignore them 361 _, traceFiles := actor.Config.Verbose() 362 for _, traceFilePath := range traceFiles { 363 if relPath, err := filepath.Rel(sourceDir, traceFilePath); err == nil { 364 additionalIgnoreLines = append(additionalIgnoreLines, relPath) 365 } 366 } 367 368 if _, err := os.Stat(pathToCFIgnore); !os.IsNotExist(err) { 369 return ignore.CompileIgnoreFileAndLines(pathToCFIgnore, additionalIgnoreLines...) 370 } else { 371 return ignore.CompileIgnoreLines(additionalIgnoreLines...) 372 } 373 } 374 375 func (Actor) findInResources(path string, filesToInclude []Resource) (Resource, bool) { 376 for _, resource := range filesToInclude { 377 if resource.Filename == filepath.ToSlash(path) { 378 log.WithField("resource", resource.Filename).Debug("found resource in files to include") 379 return resource, true 380 } 381 } 382 383 log.WithField("path", path).Debug("did not find resource in files to include") 384 return Resource{}, false 385 } 386 387 func (Actor) newArchiveReader(archive *os.File) (*zip.Reader, error) { 388 info, err := archive.Stat() 389 if err != nil { 390 return nil, err 391 } 392 393 return ykk.NewReader(archive, info.Size()) 394 }