github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/internal/fileutils/fileutils.go (about) 1 package fileutils 2 3 import ( 4 "bytes" 5 "crypto/sha256" 6 "encoding/hex" 7 "errors" 8 "fmt" 9 "io" 10 "io/fs" 11 "os" 12 "path/filepath" 13 "regexp" 14 "runtime" 15 "strings" 16 "time" 17 "unicode" 18 19 "github.com/gofrs/flock" 20 "github.com/thoas/go-funk" 21 22 "github.com/ActiveState/cli/internal/assets" 23 "github.com/ActiveState/cli/internal/errs" 24 "github.com/ActiveState/cli/internal/locale" 25 "github.com/ActiveState/cli/internal/logging" 26 "github.com/ActiveState/cli/internal/multilog" 27 "github.com/ActiveState/cli/internal/rollbar" 28 ) 29 30 // nullByte represents the null-terminator byte 31 const nullByte byte = 0 32 33 // FileMode is the mode used for created files 34 const FileMode = 0o644 35 36 // DirMode is the mode used for created dirs 37 const DirMode = os.ModePerm 38 39 // AmendOptions used to specify write actions for WriteFile 40 type AmendOptions uint8 41 42 const ( 43 // AmendByAppend content to end of file 44 AmendByAppend AmendOptions = iota 45 // WriteOverwrite file with contents 46 WriteOverwrite 47 // AmendByPrepend - add content start of file 48 AmendByPrepend 49 ) 50 51 var ErrorFileNotFound = errs.New("File could not be found") 52 53 // ReplaceAll replaces all instances of search text with replacement text in a 54 // file, which may be a binary file. 55 func ReplaceAll(filename, find, replace string) error { 56 // Read the file's bytes and create find and replace byte arrays for search 57 // and replace. 58 fileBytes, err := os.ReadFile(filename) 59 if err != nil { 60 return err 61 } 62 63 changed, byts, err := replaceInFile(fileBytes, find, replace) 64 if err != nil { 65 return err 66 } 67 68 // skip writing file, if we did not change anything 69 if !changed { 70 return nil 71 } 72 73 if err := WriteFile(filename, byts); err != nil { 74 return errs.Wrap(err, "WriteFile %s failed", filename) 75 } 76 77 return nil 78 } 79 80 // replaceInFile replaces all occurrences of oldpath with newpath 81 // For binary files with nul-terminated strings, it ensures that the replaces strings are still valid nul-terminated strings and the returned buffer has the same size as the input buffer buf. 82 // The first return argument denotes whether at least one file has been replaced 83 func replaceInFile(buf []byte, oldpath, newpath string) (bool, []byte, error) { 84 findBytes := []byte(oldpath) 85 replaceBytes := []byte(newpath) 86 replaceBytesLen := len(replaceBytes) 87 88 // Check if the file is a binary file. If so, the search and replace byte 89 // arrays must be of equal length (replacement being NUL-padded as necessary). 90 var replaceRegex *regexp.Regexp 91 quoteEscapeFind := regexp.QuoteMeta(oldpath) 92 93 // Ensure we replace both types of backslashes on Windows 94 if runtime.GOOS == "windows" { 95 quoteEscapeFind = strings.ReplaceAll(quoteEscapeFind, `\\`, `(\\|\\\\)`) 96 } 97 if IsBinary(buf) { 98 // logging.Debug("Assuming file '%s' is a binary file", filename) 99 100 regexExpandBytes := []byte("${1}") 101 // Must account for the expand characters (ie. '${1}') in the 102 // replacement bytes in order for the binary paddding to be correct 103 replaceBytes = append(replaceBytes, regexExpandBytes...) 104 105 // Replacement regex for binary files must account for null characters 106 replaceRegex = regexp.MustCompile(fmt.Sprintf(`%s([^\x00]*)`, quoteEscapeFind)) 107 if replaceBytesLen > len(findBytes) { 108 multilog.Log(logging.ErrorNoStacktrace, rollbar.Error)("Replacement text too long: %s, original text: %s", string(replaceBytes), string(findBytes)) 109 return false, nil, errors.New("replacement text cannot be longer than search text in a binary file") 110 } else if len(findBytes) > replaceBytesLen { 111 // Pad replacement with NUL bytes. 112 // logging.Debug("Padding replacement text by %d byte(s)", len(findBytes)-len(replaceBytes)) 113 paddedReplaceBytes := make([]byte, len(findBytes)+len(regexExpandBytes)) 114 copy(paddedReplaceBytes, replaceBytes) 115 replaceBytes = paddedReplaceBytes 116 } 117 } else { 118 replaceRegex = regexp.MustCompile(quoteEscapeFind) 119 // logging.Debug("Assuming file '%s' is a text file", filename) 120 } 121 122 replaced := replaceRegex.ReplaceAll(buf, replaceBytes) 123 124 return !bytes.Equal(replaced, buf), replaced, nil 125 } 126 127 // IsSymlink checks if a path is a symlink 128 func IsSymlink(path string) bool { 129 fi, err := os.Lstat(path) 130 if err != nil { 131 return false 132 } 133 return (fi.Mode() & os.ModeSymlink) == os.ModeSymlink 134 } 135 136 // IsBinary checks if the given bytes are for a binary file 137 func IsBinary(fileBytes []byte) bool { 138 return bytes.IndexByte(fileBytes, nullByte) != -1 139 } 140 141 // TargetExists checks if the given file or folder exists 142 func TargetExists(path string) bool { 143 if FileExists(path) || DirExists(path) { 144 return true 145 } 146 147 _, err1 := os.Stat(path) 148 _, err2 := os.Readlink(path) // os.Stat returns false on Symlinks that don't point to a valid file 149 _, err3 := os.Lstat(path) // for links where os.Stat and os.Readlink fail (e.g. Windows socket files) 150 return err1 == nil || err2 == nil || err3 == nil 151 } 152 153 // FileExists checks if the given file (not folder) exists 154 func FileExists(path string) bool { 155 fi, err := os.Stat(path) 156 if err != nil { 157 return false 158 } 159 160 mode := fi.Mode() 161 return mode.IsRegular() 162 } 163 164 // DirExists checks if the given directory exists 165 func DirExists(path string) bool { 166 fi, err := os.Stat(path) 167 if err != nil { 168 return false 169 } 170 171 mode := fi.Mode() 172 return mode.IsDir() 173 } 174 175 // Sha256Hash will sha256 hash the given file 176 func Sha256Hash(path string) (string, error) { 177 hasher := sha256.New() 178 b, err := os.ReadFile(path) 179 if err != nil { 180 return "", errs.Wrap(err, fmt.Sprintf("Cannot read file: %s", path)) 181 } 182 hasher.Write(b) 183 return hex.EncodeToString(hasher.Sum(nil)), nil 184 } 185 186 // HashDirectory will sha256 hash the given directory 187 func HashDirectory(path string) (string, error) { 188 hasher := sha256.New() 189 err := filepath.Walk(path, func(path string, f os.FileInfo, err error) error { 190 if err != nil { 191 return err 192 } 193 194 if f.IsDir() { 195 return nil 196 } 197 198 b, err := os.ReadFile(path) 199 if err != nil { 200 return err 201 } 202 hasher.Write(b) 203 204 return nil 205 }) 206 if err != nil { 207 return "", errs.Wrap(err, fmt.Sprintf("Cannot hash directory: %s", path)) 208 } 209 210 return hex.EncodeToString(hasher.Sum(nil)), nil 211 } 212 213 // Mkdir is a small helper function to create a directory if it doesnt already exist 214 func Mkdir(path string, subpath ...string) error { 215 if len(subpath) > 0 { 216 subpathStr := filepath.Join(subpath...) 217 path = filepath.Join(path, subpathStr) 218 } 219 if _, err := os.Stat(path); os.IsNotExist(err) { 220 err = os.MkdirAll(path, DirMode) 221 if err != nil { 222 return errs.Wrap(err, fmt.Sprintf("MkdirAll failed for path: %s", path)) 223 } 224 } 225 return nil 226 } 227 228 // MkdirUnlessExists will make the directory structure if it doesn't already exists 229 func MkdirUnlessExists(path string) error { 230 if DirExists(path) { 231 return nil 232 } 233 return Mkdir(path) 234 } 235 236 // CopyFile copies a file from one location to another 237 func CopyFile(src, target string) error { 238 in, err := os.Open(src) 239 if err != nil { 240 return errs.Wrap(err, "os.Open %s failed", src) 241 } 242 defer in.Close() 243 244 inInfo, err := in.Stat() 245 if err != nil { 246 return errs.Wrap(err, "get file info failed") 247 } 248 249 // Create target directory if it doesn't exist 250 dir := filepath.Dir(target) 251 err = MkdirUnlessExists(dir) 252 if err != nil { 253 return err 254 } 255 256 // Create target file 257 out, err := os.Create(target) 258 if err != nil { 259 return errs.Wrap(err, "os.Create %s failed", target) 260 } 261 defer out.Close() 262 263 // Copy bytes to target file 264 _, err = io.Copy(out, in) 265 if err != nil { 266 return errs.Wrap(err, "io.Copy failed") 267 } 268 err = out.Close() 269 if err != nil { 270 return errs.Wrap(err, "out.Close failed") 271 } 272 273 if err := os.Chmod(out.Name(), inInfo.Mode().Perm()); err != nil { 274 return errs.Wrap(err, "chmod failed") 275 } 276 277 return nil 278 } 279 280 func CopyAsset(assetName, dest string) error { 281 asset, err := assets.ReadFileBytes(assetName) 282 if err != nil { 283 return errs.Wrap(err, "Asset %s failed", assetName) 284 } 285 286 err = os.WriteFile(dest, asset, 0o644) 287 if err != nil { 288 return errs.Wrap(err, "os.WriteFile %s failed", dest) 289 } 290 291 return nil 292 } 293 294 func CopyMultipleFiles(files map[string]string) error { 295 for src, target := range files { 296 err := CopyFile(src, target) 297 if err != nil { 298 return err 299 } 300 } 301 return nil 302 } 303 304 // ReadFileUnsafe is an unsafe version of os.ReadFile, DO NOT USE THIS OUTSIDE OF TESTS 305 func ReadFileUnsafe(src string) []byte { 306 b, err := os.ReadFile(src) 307 if err != nil { 308 panic(fmt.Sprintf("Cannot read file: %s, error: %s", src, err.Error())) 309 } 310 return b 311 } 312 313 // ReadFile reads the content of a file 314 func ReadFile(filePath string) ([]byte, error) { 315 b, err := os.ReadFile(filePath) 316 if err != nil { 317 return nil, errs.Wrap(err, "os.ReadFile %s failed", filePath) 318 } 319 return b, nil 320 } 321 322 // WriteFile writes data to a file, if it exists it is overwritten, if it doesn't exist it is created and data is written 323 func WriteFile(filePath string, data []byte) (rerr error) { 324 err := MkdirUnlessExists(filepath.Dir(filePath)) 325 if err != nil { 326 return err 327 } 328 329 // make the target file temporarily writable 330 fileExists := FileExists(filePath) 331 if fileExists { 332 stat, _ := os.Stat(filePath) 333 if err := os.Chmod(filePath, FileMode); err != nil { 334 return errs.Wrap(err, "os.Chmod %s failed", filePath) 335 } 336 defer func() { 337 err = os.Chmod(filePath, stat.Mode().Perm()) 338 if err != nil { 339 rerr = errs.Pack(rerr, errs.Wrap(err, "os.Chmod %s failed", filePath)) 340 } 341 }() 342 } 343 344 f, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, FileMode) 345 if err != nil { 346 if !fileExists { 347 target := filepath.Dir(filePath) 348 err = errs.Pack(err, fmt.Errorf("access to target %q is denied", target)) 349 } 350 return errs.Wrap(err, "os.OpenFile %s failed", filePath) 351 } 352 defer f.Close() 353 354 _, err = f.Write(data) 355 if err != nil { 356 return errs.Wrap(err, "file.Write %s failed", filePath) 357 } 358 return nil 359 } 360 361 func AmendFileLocked(filePath string, data []byte, flag AmendOptions) error { 362 locker := flock.New(filePath + ".lock") 363 364 if err := locker.Lock(); err != nil { 365 return errs.Wrap(err, "Could not acquire file lock") 366 } 367 368 if err := AmendFile(filePath, data, flag); err != nil { 369 return errs.Wrap(err, "Could not write to file") 370 } 371 372 return locker.Unlock() 373 } 374 375 // AppendToFile appends the data to the file (if it exists) with the given data, if the file doesn't exist 376 // it is created and the data is written 377 func AppendToFile(filepath string, data []byte) error { 378 return AmendFile(filepath, data, AmendByAppend) 379 } 380 381 // PrependToFile prepends the data to the file (if it exists) with the given data, if the file doesn't exist 382 // it is created and the data is written 383 func PrependToFile(filepath string, data []byte) error { 384 return AmendFile(filepath, data, AmendByPrepend) 385 } 386 387 // AmendFile amends data to a file, supports append, or prepend 388 func AmendFile(filePath string, data []byte, flag AmendOptions) error { 389 switch flag { 390 case AmendByAppend, AmendByPrepend: 391 392 default: 393 return locale.NewInputError("fileutils_err_amend_file", "", filePath) 394 } 395 396 err := Touch(filePath) 397 if err != nil { 398 return errs.Wrap(err, "Touch %s failed", filePath) 399 } 400 401 b, err := ReadFile(filePath) 402 if err != nil { 403 return errs.Wrap(err, "ReadFile %s failed", filePath) 404 } 405 406 if flag == AmendByPrepend { 407 data = append(data, b...) 408 } else if flag == AmendByAppend { 409 data = append(b, data...) 410 } 411 412 f, err := os.OpenFile(filePath, os.O_WRONLY, FileMode) 413 if err != nil { 414 return errs.Wrap(err, "os.OpenFile %s failed", filePath) 415 } 416 defer f.Close() 417 418 _, err = f.Write(data) 419 if err != nil { 420 return errs.Wrap(err, "file.Write %s failed", filePath) 421 } 422 return nil 423 } 424 425 // FindFileInPath will find a file by the given file-name in the directory provided or in 426 // one of the parent directories of that path by walking up the tree. If the file is found, 427 // the path to that file is returned, otherwise an failure is returned. 428 func FindFileInPath(dir, filename string) (string, error) { 429 absDir, err := filepath.Abs(dir) 430 if err != nil { 431 return "", errs.Wrap(err, "filepath.Abs %s failed", dir) 432 } else if filepath := walkPathAndFindFile(absDir, filename); filepath != "" { 433 return filepath, nil 434 } 435 return "", locale.WrapError(ErrorFileNotFound, "err_file_not_found_in_path", "", filename, absDir) 436 } 437 438 // walkPathAndFindFile finds a file in the provided directory or one of its parent directories. 439 // walkPathAndFindFile prefers an absolute directory path. 440 func walkPathAndFindFile(dir, filename string) string { 441 if file := filepath.Join(dir, filename); FileExists(file) { 442 return file 443 } else if parentDir := filepath.Dir(dir); parentDir != dir { 444 return walkPathAndFindFile(parentDir, filename) 445 } 446 return "" 447 } 448 449 // Touch will attempt to "touch" a given filename by trying to open it read-only or create 450 // the file with 0644 perms if it does not exist. 451 func Touch(path string) error { 452 err := MkdirUnlessExists(filepath.Dir(path)) 453 if err != nil { 454 return err 455 } 456 file, err := os.OpenFile(path, os.O_CREATE, FileMode) 457 if err != nil { 458 return errs.Wrap(err, "os.OpenFile %s failed", path) 459 } 460 if err := file.Close(); err != nil { 461 return errs.Wrap(err, "file.Close %s failed", path) 462 } 463 return nil 464 } 465 466 // TouchFileUnlessExists will attempt to "touch" a given filename if it doesn't already exists 467 func TouchFileUnlessExists(path string) error { 468 if TargetExists(path) { 469 return nil 470 } 471 return Touch(path) 472 } 473 474 // IsEmptyDir returns true if the directory at the provided path has no files (including dirs) within it. 475 func IsEmptyDir(path string) (bool, error) { 476 dir, err := os.Open(path) 477 if err != nil { 478 return false, errs.Wrap(err, "os.Open %s failed", path) 479 } 480 481 files, err := dir.Readdir(1) 482 dir.Close() 483 if err != nil && err != io.EOF { 484 return false, errs.Wrap(err, "dir.Readdir %s failed", path) 485 } 486 487 return (len(files) == 0), nil 488 } 489 490 // MoveAllFilesCallback is invoked for every file that we move 491 type MoveAllFilesCallback func(fromPath, toPath string) 492 493 // MoveAllFilesRecursively moves files and directories from one directory to another. 494 // Unlike in MoveAllFiles, the destination directory does not need to be empty, and 495 // may include directories that are moved from the source directory. 496 // It also counts the moved files for use in a progress bar. 497 // Warnings are printed if 498 // - a source file overwrites an existing destination file 499 // - a sub-directory exists in both the source and and the destination and their permissions do not match 500 func MoveAllFilesRecursively(fromPath, toPath string, cb MoveAllFilesCallback) error { 501 if !DirExists(fromPath) { 502 return locale.NewError("err_os_not_a_directory", "", fromPath) 503 } else if !DirExists(toPath) { 504 return locale.NewError("err_os_not_a_directory", "", toPath) 505 } 506 507 // read all child files and dirs 508 dir, err := os.Open(fromPath) 509 if err != nil { 510 return errs.Wrap(err, "os.Open %s failed", fromPath) 511 } 512 fileInfos, err := dir.Readdir(-1) 513 dir.Close() 514 if err != nil { 515 return errs.Wrap(err, "dir.Readdir %s failed", fromPath) 516 } 517 518 // any found files and dirs 519 for _, fileInfo := range fileInfos { 520 subFromPath := filepath.Join(fromPath, fileInfo.Name()) 521 subToPath := filepath.Join(toPath, fileInfo.Name()) 522 toInfo, err := os.Lstat(subToPath) 523 // if stat returns, the destination path exists (either file or directory) 524 toPathExists := toInfo != nil && err == nil 525 // handle case where destination exists 526 if toPathExists { 527 if fileInfo.IsDir() != toInfo.IsDir() { 528 return locale.NewError("err_incompatible_move_file_dir", "", subFromPath, subToPath) 529 } 530 if fileInfo.Mode() != toInfo.Mode() { 531 logging.Warning(locale.Tr("warn_move_incompatible_modes", "", subFromPath, subToPath)) 532 } 533 534 if !toInfo.IsDir() { 535 // If the subToPath file exists, we remove it first - in order to ensure compatibility between platforms: 536 // On Windows, the following renaming step can otherwise fail if subToPath is read-only (file removal is allowed) 537 err = os.Remove(subToPath) 538 if err != nil { 539 multilog.Error("Failed to remove file scheduled to be overwritten: %s (file mode: %#o): %v", subToPath, toInfo.Mode(), err) 540 } 541 } 542 } 543 544 // If we are moving to a directory, call function recursively to overwrite and add files in that directory 545 if fileInfo.IsDir() { 546 // create target directories that don't exist yet 547 if !toPathExists { 548 err = Mkdir(subToPath) 549 if err != nil { 550 return locale.WrapError(err, "err_move_create_directory", "Failed to create directory {{.V0}}", subToPath) 551 } 552 err = os.Chmod(subToPath, fileInfo.Mode()) 553 if err != nil { 554 return locale.WrapError(err, "err_move_set_dir_permissions", "Failed to set file mode for directory {{.V0}}", subToPath) 555 } 556 } 557 err := MoveAllFilesRecursively(subFromPath, subToPath, cb) 558 if err != nil { 559 return err 560 } 561 // source path should be empty now 562 err = os.Remove(subFromPath) 563 if err != nil { 564 return errs.Wrap(err, "os.Remove %s failed", subFromPath) 565 } 566 567 cb(subFromPath, subToPath) 568 continue 569 } 570 571 err = os.Rename(subFromPath, subToPath) 572 if err != nil { 573 var mode fs.FileMode 574 if toPathExists { 575 mode = toInfo.Mode() 576 } 577 return errs.Wrap(err, "os.Rename %s:%s failed (file mode: %#o)", subFromPath, subToPath, mode) 578 } 579 cb(subFromPath, subToPath) 580 } 581 return nil 582 } 583 584 // CopyAndRenameFiles copies files from fromDir to toDir. 585 // If the target file exists already, the source file is first copied next to the target file, and then overwrites the target by renaming the source. 586 // This method is more robust and than copying directly, in case the target file is opened or executed. 587 func CopyAndRenameFiles(fromPath, toPath string, exclude ...string) error { 588 logging.Debug("Copying files from %s to %s", fromPath, toPath) 589 590 if !DirExists(fromPath) { 591 return locale.NewError("err_os_not_a_directory", "", fromPath) 592 } else if !DirExists(toPath) { 593 return locale.NewError("err_os_not_a_directory", "", toPath) 594 } 595 596 // read all child files and dirs 597 files, err := ListDir(fromPath, true) 598 if err != nil { 599 return errs.Wrap(err, "Could not ListDir %s", fromPath) 600 } 601 602 // any found files and dirs 603 for _, file := range files { 604 if funk.Contains(exclude, file.Name()) { 605 continue 606 } 607 608 rpath := file.RelativePath() 609 fromPath := filepath.Join(fromPath, rpath) 610 toPath := filepath.Join(toPath, rpath) 611 612 if file.IsDir() { 613 if err := MkdirUnlessExists(toPath); err != nil { 614 return errs.Wrap(err, "Could not create dir: %s", toPath) 615 } 616 continue 617 } 618 619 finfo, err := file.Info() 620 if err != nil { 621 return errs.Wrap(err, "Could not get file info for %s", file.RelativePath()) 622 } 623 624 if TargetExists(toPath) { 625 tmpToPath := fmt.Sprintf("%s.new", toPath) 626 err := CopyFile(fromPath, tmpToPath) 627 if err != nil { 628 return errs.Wrap(err, "failed to copy %s -> %s", fromPath, tmpToPath) 629 } 630 err = os.Chmod(tmpToPath, finfo.Mode()) 631 if err != nil { 632 return errs.Wrap(err, "failed to set file permissions for %s", tmpToPath) 633 } 634 err = os.Rename(tmpToPath, toPath) 635 if err != nil { 636 // cleanup 637 _ = os.Remove(tmpToPath) 638 return errs.Wrap(err, "os.Rename %s -> %s failed", tmpToPath, toPath) 639 } 640 } else { 641 err := CopyFile(fromPath, toPath) 642 if err != nil { 643 return errs.Wrap(err, "Copy %s -> %s failed", fromPath, toPath) 644 } 645 err = os.Chmod(toPath, finfo.Mode()) 646 if err != nil { 647 return errs.Wrap(err, "failed to set file permissions for %s", toPath) 648 } 649 } 650 } 651 return nil 652 } 653 654 // MoveAllFiles will move all of the files/dirs within one directory to another directory. Both directories 655 // must already exist. 656 func MoveAllFiles(fromPath, toPath string) error { 657 if !DirExists(fromPath) { 658 return locale.NewError("err_os_not_a_directory", "", fromPath) 659 } else if !DirExists(toPath) { 660 return locale.NewError("err_os_not_a_directory", "", toPath) 661 } 662 663 // read all child files and dirs 664 dir, err := os.Open(fromPath) 665 if err != nil { 666 return errs.Wrap(err, "os.Open %s failed", fromPath) 667 } 668 fileInfos, err := dir.Readdir(-1) 669 dir.Close() 670 if err != nil { 671 return errs.Wrap(err, "dir.Readdir %s failed", fromPath) 672 } 673 674 // any found files and dirs 675 for _, fileInfo := range fileInfos { 676 fromPath := filepath.Join(fromPath, fileInfo.Name()) 677 toPath := filepath.Join(toPath, fileInfo.Name()) 678 err := os.Rename(fromPath, toPath) 679 if err != nil { 680 return errs.Wrap(err, "os.Rename %s:%s failed", fromPath, toPath) 681 } 682 } 683 return nil 684 } 685 686 // WriteTempFile writes data to a temp file. 687 func WriteTempFile(pattern string, data []byte) (string, error) { 688 tempDir := os.TempDir() 689 return WriteTempFileToDir(tempDir, pattern, data, os.ModePerm) 690 } 691 692 // WriteTempFileToDir writes data to a temp file in the given dir 693 func WriteTempFileToDir(dir, pattern string, data []byte, perm os.FileMode) (string, error) { 694 f, err := os.CreateTemp(dir, pattern) 695 if err != nil { 696 return "", errs.Wrap(err, "os.CreateTemp %s (%s) failed", dir, pattern) 697 } 698 699 if _, err = f.Write(data); err != nil { 700 os.Remove(f.Name()) 701 return "", errs.Wrap(err, "f.Write %s failed", f.Name()) 702 } 703 704 if err = f.Close(); err != nil { 705 os.Remove(f.Name()) 706 return "", errs.Wrap(err, "f.Close %s failed", f.Name()) 707 } 708 709 if err := os.Chmod(f.Name(), perm); err != nil { 710 os.Remove(f.Name()) 711 return "", errs.Wrap(err, "os.Chmod %s failed", f.Name()) 712 } 713 714 return f.Name(), nil 715 } 716 717 type DirReader interface { 718 ReadDir(string) ([]os.DirEntry, error) 719 } 720 721 func CopyFilesDirReader(reader DirReader, src, dst, placeholderFileName string) error { 722 entries, err := reader.ReadDir(src) 723 if err != nil { 724 return errs.Wrap(err, "reader.ReadDir %s failed", src) 725 } 726 727 for _, entry := range entries { 728 srcPath := filepath.Join(src, entry.Name()) 729 destPath := filepath.Join(dst, entry.Name()) 730 731 switch entry.Type() & os.ModeType { 732 case os.ModeDir: 733 err := MkdirUnlessExists(destPath) 734 if err != nil { 735 return errs.Wrap(err, "MkdirUnlessExists %s failed", destPath) 736 } 737 738 err = CopyFilesDirReader(reader, srcPath, destPath, placeholderFileName) 739 if err != nil { 740 return errs.Wrap(err, "CopyFiles %s:%s failed", srcPath, destPath) 741 } 742 case os.ModeSymlink: 743 err := CopySymlink(srcPath, destPath) 744 if err != nil { 745 return errs.Wrap(err, "CopySymlink %s:%s failed", srcPath, destPath) 746 } 747 default: 748 if entry.Name() == placeholderFileName { 749 continue 750 } 751 752 err := CopyAsset(srcPath, destPath) 753 if err != nil { 754 return errs.Wrap(err, "CopyFile %s:%s failed", srcPath, destPath) 755 } 756 } 757 } 758 759 return nil 760 } 761 762 // CopyFiles will copy all of the files/dirs within one directory to another. 763 // Both directories must already exist 764 func CopyFiles(src, dst string) error { 765 return copyFiles(src, dst, false) 766 } 767 768 func copyFiles(src, dest string, remove bool) error { 769 if !DirExists(src) { 770 return locale.NewError("err_os_not_a_directory", "", src) 771 } 772 if !DirExists(dest) { 773 return locale.NewError("err_os_not_a_directory", "", dest) 774 } 775 776 entries, err := os.ReadDir(src) 777 if err != nil { 778 return errs.Wrap(err, "os.ReadDir %s failed", src) 779 } 780 781 for _, entry := range entries { 782 srcPath := filepath.Join(src, entry.Name()) 783 destPath := filepath.Join(dest, entry.Name()) 784 785 fileInfo, err := os.Lstat(srcPath) 786 if err != nil { 787 return errs.Wrap(err, "os.Lstat %s failed", srcPath) 788 } 789 790 switch fileInfo.Mode() & os.ModeType { 791 case os.ModeDir: 792 err := MkdirUnlessExists(destPath) 793 if err != nil { 794 return errs.Wrap(err, "MkdirUnlessExists %s failed", destPath) 795 } 796 err = CopyFiles(srcPath, destPath) 797 if err != nil { 798 return errs.Wrap(err, "CopyFiles %s:%s failed", srcPath, destPath) 799 } 800 case os.ModeSymlink: 801 err := CopySymlink(srcPath, destPath) 802 if err != nil { 803 return errs.Wrap(err, "CopySymlink %s:%s failed", srcPath, destPath) 804 } 805 default: 806 err := CopyFile(srcPath, destPath) 807 if err != nil { 808 return errs.Wrap(err, "CopyFile %s:%s failed", srcPath, destPath) 809 } 810 } 811 } 812 813 if remove { 814 if err := os.RemoveAll(src); err != nil { 815 return errs.Wrap(err, "os.RemovaAll %s failed", src) 816 } 817 } 818 819 return nil 820 } 821 822 // CopySymlink reads the symlink at src and creates a new 823 // link at dest 824 func CopySymlink(src, dest string) error { 825 link, err := os.Readlink(src) 826 if err != nil { 827 return errs.Wrap(err, "os.Readlink %s failed", src) 828 } 829 830 err = os.Symlink(link, dest) 831 if err != nil { 832 return errs.Wrap(err, "os.Symlink %s:%s failed", link, dest) 833 } 834 835 return nil 836 } 837 838 // TempFileUnsafe returns a tempfile handler or panics if it cannot be created 839 // This is for use in tests, do not use it outside tests! 840 func TempFileUnsafe(dir, pattern string) *os.File { 841 f, err := os.CreateTemp(dir, pattern) 842 if err != nil { 843 panic(fmt.Sprintf("Could not create tempFile: %v", err)) 844 } 845 return f 846 } 847 848 func TempFilePathUnsafe(dir, pattern string) string { 849 f := TempFileUnsafe(dir, pattern) 850 defer f.Close() 851 return f.Name() 852 } 853 854 // TempDirUnsafe returns a temp path or panics if it cannot be created 855 // This is for use in tests, do not use it outside tests! 856 func TempDirUnsafe() string { 857 f, err := os.MkdirTemp("", "") 858 if err != nil { 859 panic(fmt.Sprintf("Could not create tempDir: %v", err)) 860 } 861 return f 862 } 863 864 func TempDirFromBaseDirUnsafe(baseDir string) string { 865 f, err := os.MkdirTemp(baseDir, "") 866 if err != nil { 867 panic(fmt.Sprintf("Could not create tempDir: %v", err)) 868 } 869 return f 870 } 871 872 // MoveAllFilesCrossDisk will move all of the files/dirs within one directory 873 // to another directory even across disks. Both directories must already exist. 874 func MoveAllFilesCrossDisk(src, dst string) error { 875 err := MoveAllFiles(src, dst) 876 if err != nil { 877 multilog.Error("Move all files failed with error: %s. Falling back to copy files", err) 878 } 879 880 return copyFiles(src, dst, true) 881 } 882 883 // Join is identical to filepath.Join except that it doesn't clean the input, allowing for 884 // more consistent behavior 885 func Join(elem ...string) string { 886 for i, e := range elem { 887 if e != "" { 888 return strings.Join(elem[i:], string(filepath.Separator)) 889 } 890 } 891 return "" 892 } 893 894 // PrepareDir prepares a path by ensuring it exists and the path is consistent 895 func PrepareDir(path string) (string, error) { 896 err := MkdirUnlessExists(path) 897 if err != nil { 898 return "", err 899 } 900 901 path, err = filepath.Abs(path) 902 if err != nil { 903 return "", err 904 } 905 906 path, err = filepath.EvalSymlinks(path) 907 if err != nil { 908 return "", err 909 } 910 911 return path, nil 912 } 913 914 // LogPath will walk the given file path and log the name, permissions, mod 915 // time, and file size of all files it encounters 916 func LogPath(path string) error { 917 return filepath.Walk(path, func(path string, info os.FileInfo, err error) error { 918 if err != nil { 919 multilog.Error("Error walking filepath at: %s", path) 920 return err 921 } 922 923 logging.Debug(strings.Join([]string{ 924 fmt.Sprintf("File name: %s", info.Name()), 925 fmt.Sprintf("File permissions: %s", info.Mode()), 926 fmt.Sprintf("File mod time: %s", info.ModTime()), 927 fmt.Sprintf("File size: %d", info.Size()), 928 }, "\n")) 929 return nil 930 }) 931 } 932 933 // IsDir returns true if the given path is a directory 934 func IsDir(path string) bool { 935 info, err := os.Stat(path) 936 if err != nil { 937 return false 938 } 939 return info.IsDir() 940 } 941 942 // ResolvePath gets the absolute location of the provided path and 943 // fully evaluates the result if it is a symlink. 944 func ResolvePath(path string) (string, error) { 945 absPath, err := filepath.Abs(path) 946 if err != nil { 947 return path, errs.Wrap(err, "cannot get absolute filepath of %q", path) 948 } 949 950 if !TargetExists(path) { 951 return absPath, nil 952 } 953 954 evalPath, err := filepath.EvalSymlinks(absPath) 955 if err != nil { 956 return absPath, errs.Wrap(err, "cannot evaluate symlink %q", absPath) 957 } 958 959 return evalPath, nil 960 } 961 962 // PathsEqual checks whether the paths given all resolve to the same path 963 func PathsEqual(paths ...string) (bool, error) { 964 if len(paths) < 2 { 965 return false, errs.New("Must supply at least two paths") 966 } 967 968 var equalTo string 969 for _, path := range paths { 970 resolvedPath, err := ResolvePath(path) 971 if err != nil { 972 return false, errs.Wrap(err, "Could not resolve path: %s", path) 973 } 974 if equalTo == "" { 975 equalTo = resolvedPath 976 continue 977 } 978 if resolvedPath != equalTo { 979 return false, nil 980 } 981 } 982 983 return true, nil 984 } 985 986 // PathContainsParent checks if the directory path is equal to or a child directory 987 // of the targeted directory. Symlinks are evaluated for this comparison. 988 func PathContainsParent(path, parentPath string) (bool, error) { 989 if path == parentPath { 990 return true, nil 991 } 992 993 efmt := "cannot resolve %q" 994 995 resPath, err := ResolvePath(path) 996 if err != nil { 997 return false, errs.Wrap(err, efmt, path) 998 } 999 1000 resParent, err := ResolvePath(parentPath) 1001 if err != nil { 1002 return false, errs.Wrap(err, efmt, parentPath) 1003 } 1004 1005 return resolvedPathContainsParent(resPath, resParent), nil 1006 } 1007 1008 func resolvedPathContainsParent(path, parentPath string) bool { 1009 if !strings.HasSuffix(path, string(os.PathSeparator)) { 1010 path += string(os.PathSeparator) 1011 } 1012 1013 if !strings.HasSuffix(parentPath, string(os.PathSeparator)) { 1014 parentPath += string(os.PathSeparator) 1015 } 1016 1017 return path == parentPath || strings.HasPrefix(path, parentPath) 1018 } 1019 1020 // SymlinkTarget retrieves the target of the given symlink 1021 func SymlinkTarget(symlink string) (string, error) { 1022 fileInfo, err := os.Lstat(symlink) 1023 if err != nil { 1024 return "", errs.Wrap(err, "Could not lstat symlink") 1025 } 1026 1027 if fileInfo.Mode()&os.ModeSymlink != os.ModeSymlink { 1028 return "", errs.New("%s is not a symlink", symlink) 1029 } 1030 1031 evalDest, err := os.Readlink(symlink) 1032 if err != nil { 1033 return "", errs.Wrap(err, "Could not resolve symlink: %s", symlink) 1034 } 1035 1036 return evalDest, nil 1037 } 1038 1039 // ListDirSimple recursively lists filepaths under the given sourcePath 1040 // This does not follow symlinks 1041 func ListDirSimple(sourcePath string, includeDirs bool) ([]string, error) { 1042 result := []string{} 1043 err := filepath.WalkDir(sourcePath, func(path string, f fs.DirEntry, err error) error { 1044 if err != nil { 1045 return errs.Wrap(err, "Could not walk path: %s", path) 1046 } 1047 if !includeDirs && f.IsDir() { 1048 return nil 1049 } 1050 result = append(result, path) 1051 return nil 1052 }) 1053 if err != nil { 1054 return result, errs.Wrap(err, "Could not walk dir: %s", sourcePath) 1055 } 1056 return result, nil 1057 } 1058 1059 // ListFilesUnsafe lists filepaths under the given sourcePath non-recursively 1060 func ListFilesUnsafe(sourcePath string) []string { 1061 result := []string{} 1062 files, err := os.ReadDir(sourcePath) 1063 if err != nil { 1064 panic(fmt.Sprintf("Could not read dir: %s, error: %s", sourcePath, errs.JoinMessage(err))) 1065 } 1066 for _, file := range files { 1067 result = append(result, filepath.Join(sourcePath, file.Name())) 1068 } 1069 return result 1070 } 1071 1072 type DirEntry struct { 1073 fs.DirEntry 1074 absolutePath string 1075 rootPath string 1076 } 1077 1078 func (d DirEntry) Path() string { 1079 return d.absolutePath 1080 } 1081 1082 func (d DirEntry) RelativePath() string { 1083 // This is a bit awkward, but fs.DirEntry does not give us a relative path to the originally queried dir 1084 return strings.TrimPrefix(d.absolutePath, d.rootPath) 1085 } 1086 1087 // ListDir recursively lists filepaths under the given sourcePath 1088 // This does not follow symlinks 1089 func ListDir(sourcePath string, includeDirs bool) ([]DirEntry, error) { 1090 result := []DirEntry{} 1091 sourcePath = filepath.Clean(sourcePath) 1092 if err := filepath.WalkDir(sourcePath, func(path string, f fs.DirEntry, err error) error { 1093 if path == sourcePath { 1094 return nil // I don't know why WalkDir feels the need to include the very dir I queried.. 1095 } 1096 if err != nil { 1097 return errs.Wrap(err, "Could not walk path: %s", path) 1098 } 1099 if !includeDirs && f.IsDir() { 1100 return nil 1101 } 1102 result = append(result, DirEntry{f, path, sourcePath + string(filepath.Separator)}) 1103 return nil 1104 }); err != nil { 1105 return result, errs.Wrap(err, "Could not walk dir: %s", sourcePath) 1106 } 1107 return result, nil 1108 } 1109 1110 // PathInList returns whether the provided path list contains the provided 1111 // path. 1112 func PathInList(listSep, pathList, path string) (bool, error) { 1113 paths := strings.Split(pathList, listSep) 1114 for _, p := range paths { 1115 equal, err := PathsEqual(p, path) 1116 if err != nil { 1117 return false, err 1118 } 1119 if equal { 1120 return true, nil 1121 } 1122 } 1123 return false, nil 1124 } 1125 1126 func FileContains(path string, searchText []byte) (bool, error) { 1127 if !TargetExists(path) { 1128 return false, nil 1129 } 1130 b, err := ReadFile(path) 1131 if err != nil { 1132 return false, errs.Wrap(err, "Could not read file") 1133 } 1134 return bytes.Contains(b, searchText), nil 1135 } 1136 1137 func ModTime(path string) (time.Time, error) { 1138 stat, err := os.Stat(path) 1139 if err != nil { 1140 return time.Now(), errs.Wrap(err, "Could not stat file %s", path) 1141 } 1142 return stat.ModTime(), nil 1143 } 1144 1145 func CaseSensitivePath(path string) (string, error) { 1146 // On Windows Glob may not work with the short path (ie., DOS 8.3 notation) 1147 path, err := GetLongPathName(path) 1148 if err != nil { 1149 return "", errs.Wrap(err, "Failed to get long path name") 1150 } 1151 1152 var searchPath string 1153 if runtime.GOOS != "windows" { 1154 searchPath = globPath(path) 1155 } else { 1156 volume := filepath.VolumeName(path) 1157 remainder := strings.TrimLeft(path, volume) 1158 searchPath = filepath.Join(volume, globPath(remainder)) 1159 } 1160 1161 matches, err := filepath.Glob(searchPath) 1162 if err != nil { 1163 return "", errs.Wrap(err, "Failed to search for path") 1164 } 1165 1166 if len(matches) == 0 { 1167 return "", errs.New("Could not find path: %s", path) 1168 } 1169 1170 return matches[0], nil 1171 } 1172 1173 // PathsMatch checks if all the given paths resolve to the same value 1174 func PathsMatch(paths ...string) (bool, error) { 1175 for _, path := range paths[1:] { 1176 p1, err := ResolvePath(path) 1177 if err != nil { 1178 return false, errs.Wrap(err, "Could not resolve path %s", path) 1179 } 1180 p2, err := ResolvePath(paths[0]) 1181 if err != nil { 1182 return false, errs.Wrap(err, "Could not resolve path %s", paths[0]) 1183 } 1184 if p1 != p2 { 1185 logging.Debug("Path %s does not match %s", p1, p2) 1186 return false, nil 1187 } 1188 } 1189 return true, nil 1190 } 1191 1192 func globPath(path string) string { 1193 var result string 1194 for _, r := range path { 1195 if unicode.IsLetter(r) { 1196 result += fmt.Sprintf("[%c%c]", unicode.ToUpper(r), unicode.ToLower(r)) 1197 } else { 1198 result += string(r) 1199 } 1200 } 1201 return result 1202 }