github.com/ouraigua/jenkins-library@v0.0.0-20231028010029-fbeaf2f3aa9b/pkg/piperutils/fileUtils.go (about) 1 package piperutils 2 3 import ( 4 "archive/tar" 5 "archive/zip" 6 "compress/gzip" 7 "crypto/sha256" 8 "errors" 9 "fmt" 10 "io" 11 "io/fs" 12 "os" 13 "path/filepath" 14 "strings" 15 "time" 16 17 "github.com/bmatcuk/doublestar" 18 ) 19 20 // FileUtils ... 21 type FileUtils interface { 22 Abs(path string) (string, error) 23 DirExists(path string) (bool, error) 24 FileExists(filename string) (bool, error) 25 Copy(src, dest string) (int64, error) 26 Move(src, dest string) error 27 FileRead(path string) ([]byte, error) 28 ReadFile(path string) ([]byte, error) 29 FileWrite(path string, content []byte, perm os.FileMode) error 30 WriteFile(path string, content []byte, perm os.FileMode) error 31 FileRemove(path string) error 32 MkdirAll(path string, perm os.FileMode) error 33 Chmod(path string, mode os.FileMode) error 34 Glob(pattern string) (matches []string, err error) 35 Chdir(path string) error 36 TempDir(string, string) (string, error) 37 RemoveAll(string) error 38 FileRename(string, string) error 39 Getwd() (string, error) 40 Symlink(oldname string, newname string) error 41 SHA256(path string) (string, error) 42 CurrentTime(format string) string 43 Open(name string) (io.ReadWriteCloser, error) 44 Create(name string) (io.ReadWriteCloser, error) 45 } 46 47 // Files ... 48 type Files struct{} 49 50 // TempDir creates a temporary directory 51 func (f Files) TempDir(dir, pattern string) (name string, err error) { 52 if len(dir) == 0 { 53 // lazy init system temp dir in case it doesn't exist 54 if exists, _ := f.DirExists(os.TempDir()); !exists { 55 f.MkdirAll(os.TempDir(), 0o666) 56 } 57 } 58 59 return os.MkdirTemp(dir, pattern) 60 } 61 62 // FileExists returns true if the file system entry for the given path exists and is not a directory. 63 func (f Files) FileExists(filename string) (bool, error) { 64 info, err := os.Stat(filename) 65 66 if os.IsNotExist(err) { 67 return false, nil 68 } 69 if err != nil { 70 return false, err 71 } 72 73 return !info.IsDir(), nil 74 } 75 76 // FileExists returns true if the file system entry for the given path exists and is not a directory. 77 func FileExists(filename string) (bool, error) { 78 return Files{}.FileExists(filename) 79 } 80 81 // DirExists returns true if the file system entry for the given path exists and is a directory. 82 func (f Files) DirExists(path string) (bool, error) { 83 info, err := os.Stat(path) 84 85 if os.IsNotExist(err) { 86 return false, nil 87 } 88 if err != nil { 89 return false, err 90 } 91 92 return info.IsDir(), nil 93 } 94 95 // Copy ... 96 func (f Files) Copy(src, dst string) (int64, error) { 97 exists, err := f.FileExists(src) 98 if err != nil { 99 return 0, err 100 } 101 102 if !exists { 103 return 0, errors.New("Source file '" + src + "' does not exist") 104 } 105 106 source, err := os.Open(src) 107 if err != nil { 108 return 0, err 109 } 110 defer func() { _ = source.Close() }() 111 112 destination, err := os.Create(dst) 113 if err != nil { 114 return 0, err 115 } 116 stats, err := os.Stat(src) 117 if err != nil { 118 return 0, err 119 } 120 121 os.Chmod(dst, stats.Mode()) 122 defer func() { _ = destination.Close() }() 123 nBytes, err := CopyData(destination, source) 124 return nBytes, err 125 } 126 127 // Move will move files from src to dst 128 func (f Files) Move(src, dst string) error { 129 if exists, err := f.FileExists(src); err != nil { 130 return err 131 } else if !exists { 132 return fmt.Errorf("file doesn't exist: %s", src) 133 } 134 135 if _, err := f.Copy(src, dst); err != nil { 136 return err 137 } 138 139 return f.FileRemove(src) 140 } 141 142 // Chmod is a wrapper for os.Chmod(). 143 func (f Files) Chmod(path string, mode os.FileMode) error { 144 return os.Chmod(path, mode) 145 } 146 147 // Unzip will decompress a zip archive, moving all files and folders 148 // within the zip file (parameter 1) to an output directory (parameter 2). 149 // from https://golangcode.com/unzip-files-in-go/ with the following license: 150 // MIT License 151 // 152 // # Copyright (c) 2017 Edd Turtle 153 // 154 // Permission is hereby granted, free of charge, to any person obtaining a copy 155 // of this software and associated documentation files (the "Software"), to deal 156 // in the Software without restriction, including without limitation the rights 157 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 158 // copies of the Software, and to permit persons to whom the Software is 159 // furnished to do so, subject to the following conditions: 160 // 161 // The above copyright notice and this permission notice shall be included in all 162 // copies or substantial portions of the Software. 163 // 164 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 165 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 166 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 167 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 168 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 169 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 170 // SOFTWARE. 171 func Unzip(src, dest string) ([]string, error) { 172 var filenames []string 173 174 r, err := zip.OpenReader(src) 175 if err != nil { 176 return filenames, err 177 } 178 defer func() { _ = r.Close() }() 179 180 for _, f := range r.File { 181 182 // Store filename/path for returning and using later on 183 fpath := filepath.Join(dest, f.Name) 184 185 // Check for ZipSlip. More Info: http://bit.ly/2MsjAWE 186 if !strings.HasPrefix(fpath, filepath.Clean(dest)+string(os.PathSeparator)) { 187 return filenames, fmt.Errorf("%s: illegal file path", fpath) 188 } 189 190 filenames = append(filenames, fpath) 191 192 if f.FileInfo().IsDir() { 193 // Make Folder 194 err := os.MkdirAll(fpath, os.ModePerm) 195 if err != nil { 196 return filenames, fmt.Errorf("failed to create directory: %w", err) 197 } 198 continue 199 } 200 201 // Make File 202 if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil { 203 return filenames, err 204 } 205 206 outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) 207 if err != nil { 208 return filenames, err 209 } 210 211 rc, err := f.Open() 212 if err != nil { 213 return filenames, err 214 } 215 216 _, err = CopyData(outFile, rc) 217 218 // Close the file without defer to close before next iteration of loop 219 _ = outFile.Close() 220 _ = rc.Close() 221 222 if err != nil { 223 return filenames, err 224 } 225 } 226 return filenames, nil 227 } 228 229 // Untar will decompress a gzipped archive and then untar it, moving all files and folders 230 // within the tgz file (parameter 1) to an output directory (parameter 2). 231 // some tar like the one created from npm have an addtional package folder which need to be removed during untar 232 // stripComponent level acts the same like in the tar cli with level 1 corresponding to elimination of parent folder 233 // stripComponentLevel = 1 -> parentFolder/someFile.Txt -> someFile.Txt 234 // stripComponentLevel = 2 -> parentFolder/childFolder/someFile.Txt -> someFile.Txt 235 // when stripCompenent in 0 the untar will retain the original tar folder structure 236 // when stripCompmenet is greater than 0 the expectation is all files must be under that level folder and if not there is a hard check and failure condition 237 func Untar(src string, dest string, stripComponentLevel int) error { 238 file, err := os.Open(src) 239 if err != nil { 240 return fmt.Errorf("unable to open src: %v", err) 241 } 242 defer file.Close() 243 244 if b, err := isFileGzipped(src); err == nil && b { 245 zr, err := gzip.NewReader(file) 246 if err != nil { 247 return fmt.Errorf("requires gzip-compressed body: %v", err) 248 } 249 250 return untar(zr, dest, stripComponentLevel) 251 } 252 253 return untar(file, dest, stripComponentLevel) 254 } 255 256 func untar(r io.Reader, dir string, level int) (err error) { 257 madeDir := map[string]bool{} 258 259 tr := tar.NewReader(r) 260 for { 261 f, err := tr.Next() 262 if err == io.EOF { 263 break 264 } 265 if err != nil { 266 return fmt.Errorf("tar error: %v", err) 267 } 268 if strings.HasPrefix(f.Name, "/") { 269 f.Name = fmt.Sprintf(".%s", f.Name) 270 } 271 if !validRelPath(f.Name) { // blocks path traversal attacks 272 return fmt.Errorf("tar contained invalid name error %q", f.Name) 273 } 274 rel := filepath.FromSlash(f.Name) 275 276 // when level X folder(s) needs to be removed we first check that the rel path must have atleast X or greater than X pathseperatorserr 277 // or else we might end in index out of range 278 if level > 0 { 279 if strings.Count(rel, string(os.PathSeparator)) >= level { 280 relSplit := strings.SplitN(rel, string(os.PathSeparator), level+1) 281 rel = relSplit[level] 282 } else { 283 return fmt.Errorf("files %q in tarball archive not under level %v", f.Name, level) 284 } 285 } 286 287 abs := filepath.Join(dir, rel) 288 289 fi := f.FileInfo() 290 mode := fi.Mode() 291 switch { 292 case mode.IsRegular(): 293 // Make the directory. This is redundant because it should 294 // already be made by a directory entry in the tar 295 // beforehand. Thus, don't check for errors; the next 296 // write will fail with the same error. 297 dir := filepath.Dir(abs) 298 if !madeDir[dir] { 299 if err := os.MkdirAll(filepath.Dir(abs), 0o755); err != nil { 300 return err 301 } 302 madeDir[dir] = true 303 } 304 wf, err := os.OpenFile(abs, os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode.Perm()) 305 if err != nil { 306 return err 307 } 308 n, err := io.Copy(wf, tr) 309 if closeErr := wf.Close(); closeErr != nil && err == nil { 310 err = closeErr 311 } 312 if err != nil { 313 return fmt.Errorf("error writing to %s: %v", abs, err) 314 } 315 if n != f.Size { 316 return fmt.Errorf("only wrote %d bytes to %s; expected %d", n, abs, f.Size) 317 } 318 case mode.IsDir(): 319 if err := os.MkdirAll(abs, 0o755); err != nil { 320 return err 321 } 322 madeDir[abs] = true 323 case mode&fs.ModeSymlink != 0: 324 if err := os.Symlink(f.Linkname, abs); err != nil { 325 return err 326 } 327 default: 328 return fmt.Errorf("tar file entry %s contained unsupported file type %v", f.Name, mode) 329 } 330 } 331 return nil 332 } 333 334 // isFileGzipped checks the first 3 bytes of the given file to determine if it is gzipped or not. Returns `true` if the file is gzipped. 335 func isFileGzipped(file string) (bool, error) { 336 f, err := os.Open(file) 337 defer f.Close() 338 339 if err != nil { 340 return false, err 341 } 342 343 b := make([]byte, 3) 344 _, err = io.ReadFull(f, b) 345 346 if err != nil { 347 return false, err 348 } 349 350 return b[0] == 0x1f && b[1] == 0x8b && b[2] == 8, nil 351 } 352 353 func validRelPath(p string) bool { 354 if p == "" || strings.Contains(p, `\`) || strings.HasPrefix(p, "/") || strings.Contains(p, "../") { 355 return false 356 } 357 return true 358 } 359 360 // Copy ... 361 func Copy(src, dst string) (int64, error) { 362 return Files{}.Copy(src, dst) 363 } 364 365 // FileRead is a wrapper for os.ReadFile(). 366 func (f Files) FileRead(path string) ([]byte, error) { 367 return os.ReadFile(path) 368 } 369 370 // ReadFile is a wrapper for os.ReadFile() using the same name and syntax. 371 func (f Files) ReadFile(path string) ([]byte, error) { 372 return f.FileRead(path) 373 } 374 375 // FileWrite is a wrapper for os.WriteFile(). 376 func (f Files) FileWrite(path string, content []byte, perm os.FileMode) error { 377 return os.WriteFile(path, content, perm) 378 } 379 380 // WriteFile is a wrapper for os.ReadFile() using the same name and syntax. 381 func (f Files) WriteFile(path string, content []byte, perm os.FileMode) error { 382 return f.FileWrite(path, content, perm) 383 } 384 385 // FileRemove is a wrapper for os.Remove(). 386 func (f Files) FileRemove(path string) error { 387 return os.Remove(path) 388 } 389 390 // FileRename is a wrapper for os.Rename(). 391 func (f Files) FileRename(oldPath, newPath string) error { 392 return os.Rename(oldPath, newPath) 393 } 394 395 // FileOpen is a wrapper for os.OpenFile(). 396 func (f *Files) FileOpen(name string, flag int, perm os.FileMode) (*os.File, error) { 397 return os.OpenFile(name, flag, perm) 398 } 399 400 // MkdirAll is a wrapper for os.MkdirAll(). 401 func (f Files) MkdirAll(path string, perm os.FileMode) error { 402 return os.MkdirAll(path, perm) 403 } 404 405 // RemoveAll is a wrapper for os.RemoveAll(). 406 func (f Files) RemoveAll(path string) error { 407 return os.RemoveAll(path) 408 } 409 410 // Glob is a wrapper for doublestar.Glob(). 411 func (f Files) Glob(pattern string) (matches []string, err error) { 412 return doublestar.Glob(pattern) 413 } 414 415 // ExcludeFiles returns a slice of files, which contains only the sub-set of files that matched none 416 // of the glob patterns in the provided excludes list. 417 func ExcludeFiles(files, excludes []string) ([]string, error) { 418 if len(excludes) == 0 { 419 return files, nil 420 } 421 422 var filteredFiles []string 423 for _, file := range files { 424 includeFile := true 425 file = filepath.FromSlash(file) 426 for _, exclude := range excludes { 427 matched, err := doublestar.PathMatch(exclude, file) 428 if err != nil { 429 return nil, fmt.Errorf("failed to match file %s to pattern %s: %w", file, exclude, err) 430 } 431 if matched { 432 includeFile = false 433 break 434 } 435 } 436 if includeFile { 437 filteredFiles = append(filteredFiles, file) 438 } 439 } 440 441 return filteredFiles, nil 442 } 443 444 // Getwd is a wrapper for os.Getwd(). 445 func (f Files) Getwd() (string, error) { 446 return os.Getwd() 447 } 448 449 // Chdir is a wrapper for os.Chdir(). 450 func (f Files) Chdir(path string) error { 451 return os.Chdir(path) 452 } 453 454 // Stat is a wrapper for os.Stat() 455 func (f Files) Stat(path string) (os.FileInfo, error) { 456 return os.Stat(path) 457 } 458 459 // Abs is a wrapper for filepath.Abs() 460 func (f Files) Abs(path string) (string, error) { 461 return filepath.Abs(path) 462 } 463 464 // Symlink is a wrapper for os.Symlink 465 func (f Files) Symlink(oldname, newname string) error { 466 return os.Symlink(oldname, newname) 467 } 468 469 // SHA256 computes a SHA256 for a given file 470 func (f Files) SHA256(path string) (string, error) { 471 file, err := os.Open(path) 472 if err != nil { 473 return "", err 474 } 475 defer file.Close() 476 477 hash := sha256.New() 478 _, err = io.Copy(hash, file) 479 if err != nil { 480 return "", err 481 } 482 483 return fmt.Sprintf("%x", string(hash.Sum(nil))), nil 484 } 485 486 // CurrentTime returns the current time in the specified format 487 func (f Files) CurrentTime(format string) string { 488 fString := format 489 if len(format) == 0 { 490 fString = "20060102-150405" 491 } 492 return fmt.Sprint(time.Now().Format(fString)) 493 } 494 495 // Open is a wrapper for os.Open 496 func (f Files) Open(name string) (io.ReadWriteCloser, error) { 497 return os.Open(name) 498 } 499 500 // Create is a wrapper for os.Create 501 func (f Files) Create(name string) (io.ReadWriteCloser, error) { 502 return os.Create(name) 503 }