go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/system/filesystem/filesystem.go (about) 1 // Copyright 2017 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package filesystem 16 17 import ( 18 "io" 19 "io/ioutil" 20 "os" 21 "path/filepath" 22 "runtime" 23 "sort" 24 "strings" 25 "syscall" 26 "time" 27 28 "go.chromium.org/luci/common/data/sortby" 29 "go.chromium.org/luci/common/errors" 30 ) 31 32 // IsNotExist calls os.IsNotExist on the unwrapped err. 33 func IsNotExist(err error) bool { return os.IsNotExist(errors.Unwrap(err)) } 34 35 // MakeDirs is a convenience wrapper around os.MkdirAll that applies a 0755 36 // mask to all created directories. 37 func MakeDirs(path string) error { 38 if err := os.MkdirAll(path, 0755); err != nil { 39 return errors.Annotate(err, "").Err() 40 } 41 return nil 42 } 43 44 // AbsPath is a convenience wrapper around filepath.Abs that accepts a string 45 // pointer, base, and updates it on successful resolution. 46 func AbsPath(base *string) error { 47 v, err := filepath.Abs(*base) 48 if err != nil { 49 return errors.Annotate(err, "unable to resolve absolute path"). 50 InternalReason("base(%q)", *base).Err() 51 } 52 *base = v 53 return nil 54 } 55 56 // Touch creates a new, empty file at the specified path. 57 // 58 // If when is zero-value, time.Now will be used. 59 func Touch(path string, when time.Time, mode os.FileMode) error { 60 // Try and create a file at the target path. 61 fd, err := os.OpenFile(path, (os.O_CREATE | os.O_RDWR | os.O_EXCL), mode) 62 if err == nil { 63 if err := fd.Close(); err != nil { 64 return errors.Annotate(err, "failed to close new file").Err() 65 } 66 if when.IsZero() { 67 // If "now" was specified, and we created a new file, then its times will 68 // be now by default. 69 return nil 70 } 71 } 72 73 // Couldn't create a new file. Either it exists already, it is a directory, 74 // or there was an OS-level failure. Since we can't really distinguish 75 // between these cases, try Chtimes (update timestamp) and error 76 // if this fails. 77 if when.IsZero() { 78 when = time.Now() 79 } 80 if err := os.Chtimes(path, when, when); err != nil { 81 return errors.Annotate(err, "failed to Chtimes").InternalReason("path(%q)", path).Err() 82 } 83 84 return nil 85 } 86 87 // RemoveAll is a fork of os.RemoveAll that attempts to deal with read only 88 // files and directories by modifying permissions as necessary. 89 // 90 // If the specified path does not exist, RemoveAll will return nil. 91 // 92 // Note that RemoveAll will not modify permissions on parent directory of the 93 // provided path, even if it is read only and preventing deletion of the path on 94 // POSIX system. 95 // 96 // Copied from 97 // https://go.googlesource.com/go/+/b86e76681366447798c94abb959bb60875bcc856/src/os/path.go#63 98 func RemoveAll(path string) error { 99 const isWin = runtime.GOOS == "windows" 100 // Simple case: try removing as if it was a file or empty directory. 101 var err error 102 if isWin { 103 // In theory this call should not be necessary. os.Remove() already 104 // tries to remove the FILE_ATTRIBUTE_READONLY attribute at 105 // https://go.googlesource.com/go/+blame/go1.13/src/os/file_windows.go#296. 106 // In practice this doesn't work in one case, when it is a symlink that 107 // points to a missing file. In this case, ErrNotExist is returned, but 108 // the function call is still needed for the os.Remove() to work below. 109 err = MakePathUserWritable(path, nil) 110 } 111 if err == nil || IsNotExist(err) { 112 // On Windows, invalid symlink is treated as not exist error, but need to 113 // remove that. 114 err = os.Remove(path) 115 } 116 if err == nil || IsNotExist(err) { 117 return nil 118 } 119 120 // Otherwise, is this a directory we need to recurse into? 121 dir, serr := os.Lstat(path) 122 if serr != nil { 123 if serr, ok := serr.(*os.PathError); ok && (IsNotExist(serr.Err) || serr.Err == syscall.ENOTDIR) { 124 return nil 125 } 126 return serr 127 } 128 if !dir.IsDir() { 129 // Not a directory; return the error from Remove. 130 return err 131 } 132 // Directory. 133 if !isWin { 134 // On POSIX systems, the directory must have write access for its files to 135 // be deleted. Best effort attempt to make it writable. 136 _ = MakePathUserWritable(path, dir) 137 } 138 fd, err := os.Open(path) 139 if err != nil { 140 if IsNotExist(err) { 141 // Race. It was deleted between the Lstat and Open. 142 // Return nil per RemoveAll's docs. 143 return nil 144 } 145 return err 146 } 147 // Remove contents & return first error. 148 err = nil 149 for { 150 if err == nil && (runtime.GOOS == "plan9" || runtime.GOOS == "nacl") { 151 // Reset read offset after removing directory entries. 152 // See golang.org/issue/22572. 153 fd.Seek(0, 0) 154 } 155 names, err1 := fd.Readdirnames(100) 156 for _, name := range names { 157 err1 := RemoveAll(path + string(os.PathSeparator) + name) 158 if err == nil { 159 err = err1 160 } 161 } 162 if err1 == io.EOF { 163 break 164 } 165 // If Readdirnames returned an error, use it. 166 if err == nil { 167 err = err1 168 } 169 if len(names) == 0 { 170 break 171 } 172 } 173 // Close directory, because windows won't remove opened directory. 174 fd.Close() 175 // Remove directory. 176 err1 := os.Remove(path) 177 if err1 == nil || IsNotExist(err1) { 178 return nil 179 } 180 if err == nil { 181 err = err1 182 } 183 return err 184 } 185 186 // RenamingRemoveAll opportunistically renames a path first, and then removes it. 187 // 188 // The advantage over RemoveAll is, if renaming succeeds, lower chance of 189 // interference from other writers/readers of the filesystem. 190 // If renaming fails, removes the original path via RemoveAll. 191 // 192 // If renameToDir is given, a new temp directory will be created in it. 193 // Else, a new temp directory is placed within the path's parent dir. 194 // After this, a file/dir represented by the path is moved into the temp dir. 195 // 196 // In case of any failures during the temp dir creation or the move, 197 // default to RemoveAll of path in place. 198 // 199 // Returned renamedToPath is the renamed path if renaming succeeded and "" 200 // otherwise. 201 // Returned error is the one from RemoveAll call. 202 func RenamingRemoveAll(path, renameToDir string) (renamedToPath string, err error) { 203 pathParentDir, pathFileOrDir := filepath.Split(filepath.Clean(path)) 204 if renameToDir == "" { 205 renameToDir = pathParentDir 206 } 207 renameToDir, err = ioutil.TempDir(renameToDir, ".trash-") 208 if err != nil { 209 err = RemoveAll(path) 210 return 211 } 212 213 renamedToPath = filepath.Join(renameToDir, pathFileOrDir) 214 if err = os.Rename(path, renamedToPath); err != nil { 215 // delete temp dir we just created and ignore errors -- there is not much we can do. 216 _ = os.Remove(renameToDir) 217 renamedToPath = "" 218 err = RemoveAll(path) 219 return 220 } 221 err = RemoveAll(renamedToPath) 222 return 223 } 224 225 // MakeReadOnly recursively iterates through all of the files and directories 226 // starting at path and marks them read-only. 227 func MakeReadOnly(path string, filter func(string) bool) error { 228 return recursiveChmod(path, filter, func(mode os.FileMode) os.FileMode { 229 return mode & (^os.FileMode(0222)) 230 }) 231 } 232 233 // MakePathUserWritable updates the filesystem metadata on a single file or 234 // directory to make it user-writable. 235 // 236 // fi is optional. If nil, os.Stat will be called on path. Otherwise, fi will 237 // be regarded as the results of calling os.Stat on path. This is provided as 238 // an optimization, since some filesystem operations automatically yield a 239 // FileInfo. 240 func MakePathUserWritable(path string, fi os.FileInfo) error { 241 if fi == nil { 242 var err error 243 if fi, err = os.Stat(path); err != nil { 244 return errors.Annotate(err, "failed to Stat path").InternalReason("path(%q)", path).Err() 245 } 246 } 247 248 // Make user-writable, if it's not already. 249 mode := fi.Mode() 250 if (mode & 0200) == 0 { 251 mode |= 0200 252 if err := os.Chmod(path, mode); err != nil { 253 return errors.Annotate(err, "could not Chmod path").InternalReason("mode(%#o)/path(%q)", mode, path).Err() 254 } 255 } 256 return nil 257 } 258 259 func recursiveChmod(path string, filter func(string) bool, chmod func(mode os.FileMode) os.FileMode) error { 260 if filter == nil { 261 filter = func(string) bool { return true } 262 } 263 264 err := filepath.Walk(path, func(path string, info os.FileInfo, err error) error { 265 if err != nil { 266 return errors.Annotate(err, "").Err() 267 } 268 269 mode := info.Mode() 270 if (mode.IsRegular() || mode.IsDir()) && filter(path) { 271 if newMode := chmod(mode); newMode != mode { 272 if err := os.Chmod(path, newMode); err != nil { 273 return errors.Annotate(err, "failed to Chmod").InternalReason("path(%q)", path).Err() 274 } 275 } 276 } 277 return nil 278 }) 279 if err != nil { 280 return errors.Annotate(err, "").Err() 281 } 282 return nil 283 } 284 285 // Copy makes a copy of the file. 286 func Copy(outfile, infile string, mode os.FileMode) (err error) { 287 in, err := os.Open(infile) 288 if err != nil { 289 return err 290 } 291 defer func() { 292 if cerr := in.Close(); err == nil { 293 err = cerr 294 } 295 }() 296 297 out, err := os.OpenFile(outfile, os.O_CREATE|os.O_EXCL|os.O_WRONLY, mode) 298 if err != nil { 299 return err 300 } 301 defer func() { 302 if cerr := out.Close(); err == nil { 303 err = cerr 304 } 305 }() 306 307 _, err = io.Copy(out, in) 308 return err 309 } 310 311 // ReadableCopy makes a copy of the file that is readable by everyone. 312 func ReadableCopy(outfile, infile string) error { 313 istat, err := os.Stat(infile) 314 if err != nil { 315 return err 316 } 317 318 return Copy(outfile, infile, addReadMode(istat.Mode())) 319 } 320 321 func hardlinkWithFallback(outfile, infile string) error { 322 if err := os.Link(infile, outfile); err == nil { 323 return nil 324 } 325 326 return ReadableCopy(outfile, infile) 327 } 328 329 // HardlinkRecursively efficiently copies a file or directory from src to dst. 330 // 331 // `src` may be a file, directory, or a symlink to a file or directory. 332 // All symlinks are replaced with their targets, so the resulting 333 // directory structure in `dst` will never have any symlinks. 334 // 335 // To increase speed, HardlinkRecursively hardlinks individual files into the 336 // (newly created) directory structure if possible. 337 func HardlinkRecursively(src, dst string) error { 338 src, stat, err := ResolveSymlink(src) 339 if err != nil { 340 return errors.Annotate(err, "failed to call ResolveSymlink(%s)", src).Err() 341 } 342 343 if stat.Mode().IsRegular() { 344 return hardlinkWithFallback(dst, src) 345 } 346 347 if !stat.Mode().IsDir() { 348 return errors.Reason("%s is not a directory: %v", src, stat).Err() 349 } 350 351 if err := os.MkdirAll(dst, 0775); err != nil { 352 return errors.Annotate(err, "failed to call MkdirAll for %s", dst).Err() 353 } 354 355 file, err := os.Open(src) 356 if err != nil { 357 return errors.Annotate(err, "failed to Open %s", src).Err() 358 } 359 defer file.Close() 360 361 for { 362 names, err := file.Readdirnames(100) 363 if err == io.EOF { 364 break 365 } 366 if err != nil { 367 return errors.Annotate(err, "failed to call Readdirnames for %s", src).Err() 368 } 369 370 for _, name := range names { 371 if err := HardlinkRecursively(filepath.Join(src, name), filepath.Join(dst, name)); err != nil { 372 return errors.Annotate(err, "failed to call HardlinkRecursively(%s, %s)", filepath.Join(src, name), filepath.Join(dst, name)).Err() 373 } 374 375 } 376 } 377 378 return nil 379 } 380 381 // CreateDirectories creates the directory structure needed by the given list of files. 382 func CreateDirectories(baseDirectory string, files []string) error { 383 dirs := make([]string, len(files)) 384 for i, file := range files { 385 if filepath.IsAbs(file) { 386 return errors.Reason("file should be relative path: %s", file).Err() 387 } 388 dirs[i] = filepath.Dir(file) 389 } 390 391 sort.Strings(dirs) 392 393 for i, dir := range dirs { 394 if dir == "" { 395 continue 396 } 397 if i+1 < len(dirs) && filepath.HasPrefix(dirs[i+1], dir) { 398 continue 399 } 400 dir = filepath.Join(baseDirectory, dir) 401 402 if err := os.MkdirAll(dir, 0755); err != nil { 403 return errors.Annotate(err, "failed to create directory for %s", dir).Err() 404 } 405 } 406 407 return nil 408 } 409 410 // IsEmptyDir returns whether |dir| is empty or not. 411 // This returns error if |dir| is not directory, or find some error during checking. 412 func IsEmptyDir(dir string) (bool, error) { 413 d, err := os.Open(dir) 414 if err != nil { 415 return false, errors.Annotate(err, "failed to Open(%s)", dir).Err() 416 } 417 defer d.Close() 418 419 names, err := d.Readdirnames(1) 420 if len(names) > 0 || err == io.EOF { 421 return len(names) == 0, nil 422 } 423 424 return false, errors.Annotate(err, "failed to call Readdirnames(1) for %s", dir).Err() 425 } 426 427 // IsDir to see whether |path| is a directory. 428 // This is just a thin wrapper around os.Stat(...). 429 // If this returns True, |path| is a directory. 430 // If this returns False with nil err, |path| is not a directory. 431 // If this returns non-nil error, failed to determine |path| is a drectory. 432 func IsDir(path string) (bool, error) { 433 stat, err := os.Stat(path) 434 if err != nil { 435 if os.IsNotExist(err) { 436 return false, nil 437 } 438 return false, err 439 } 440 return stat.IsDir(), nil 441 } 442 443 // GetFreeSpace returns the number of free bytes. 444 // 445 // On POSIX platforms, this returns the free space as visible by the current 446 // user. The returned value is what is usable, and it can be lower than the 447 // actual free disk space. For example on linux there's by default a 5% that is 448 // reserved to the root user. 449 func GetFreeSpace(path string) (uint64, error) { 450 return getFreeSpace(path) 451 } 452 453 // findPathSeparators finds the index of all PathSeparators in `path` which 454 // don't split the Volume. 455 // 456 // This function is only defined for clean, absolute, paths which end with 457 // a path separator. 458 // 459 // For unix paths, the first returned index will always be 0. 460 func findPathSeparators(path string) []int { 461 offset := len(filepath.VolumeName(path)) 462 path = path[offset:] 463 464 ret := make([]int, 0, 10) // 10 is a guess, could be more or less. 465 for { 466 idx := strings.IndexByte(path, os.PathSeparator) 467 if idx == -1 { 468 break 469 } 470 ret = append(ret, offset+idx) 471 offset += idx + 1 472 path = path[idx+1:] 473 } 474 475 return ret 476 } 477 478 // ErrRootSentinel is wrapped and then returned from GetCommonAncestor when it 479 // encounters one of the provided rootSentinels. 480 var ErrRootSentinel = errors.New("hit root sentinel") 481 482 // GetCommonAncestor returns the smallest path which is the ancestor of all 483 // provided paths (which must actually exist on the filesystem). 484 // 485 // All paths here are converted to absolute paths before calculating the 486 // ancestor, and the returned path will also be an absolute path. Note that this 487 // doesn't do anything special with symlinks; the caller can resolve them if 488 // necessary. 489 // 490 // This function works correctly on case-insensitive filesystems, or on 491 // filesystems with a mix of case-sensitive and case-insensitive paths, but you 492 // can get some wild filesystems out there, so this will probably break on 493 // exotic setups. Note that the case of the path you get back will be derived 494 // from the shortest input path (after making them absolute); this function 495 // makes no attempt to "canonicalize" the case of any paths (but may do so 496 // accidentally, depending on the operating system). This function does not 497 // attempt to resolve symlinks. 498 // 499 // If a given path points to a file, the file's containing directory will be 500 // considered instead (i.e. GetCommonAncestor("a/b.ext") will return the 501 // absolute path of "a"). 502 // 503 // `rootSentinels` is a list of sub paths to look for to stop walking up the 504 // directory hierarchy. A typical value would be something like 505 // []string{".git"}. If one of these is found, this function returns "" with 506 // a wrapped ErrRootSentinel. Use errors.Is to identify this. 507 // 508 // Returns an error if any of the provided paths does not exist. 509 // If successful, will return a path ending with PathSeparator. 510 // 511 // If no paths are prodvided, returns ("", nil) 512 func GetCommonAncestor(paths []string, rootSentinels []string) (string, error) { 513 if len(paths) == 0 { 514 return "", nil 515 } 516 517 const sep = string(os.PathSeparator) 518 519 type cleanPath struct { 520 // The cleaned, absolute path to a directory which exists. 521 path string 522 // Indexes in `path` for each os.PathSeparator which is a valid split point. 523 // Note that UNC roots on windows may 'skip' PathSeparators (i.e. slashes[0] 524 // may contain multiple PathSeparators). 525 slashes []int 526 } 527 cleanPaths := make([]cleanPath, len(paths)) 528 529 var commonVolume *string 530 531 // Clean all the paths, make them absolute. 532 // Find all the slashes in the paths. 533 // 534 // Note that a UNC path like '\\host\share\something' would have its first 535 // slash at index 12. 536 // A Unix path like '/host/share/something' would have its first 537 // slash at index 0. 538 for i, path := range paths { 539 if err := AbsPath(&path); err != nil { 540 return "", err 541 } 542 vol := filepath.VolumeName(path) 543 544 if commonVolume != nil { 545 // note: we check this first to allow testing; otherwise we would need to 546 // run tests on a machine with multiple volumes. 547 if vol != *commonVolume { 548 return "", errors.Reason("provided paths originate on different volumes: path[0]:%q, path[%d]:%q", *commonVolume, i, vol).Err() 549 } 550 } else { 551 commonVolume = &vol 552 } 553 554 fi, err := os.Lstat(path) 555 if err != nil { 556 return "", errors.Annotate(err, "reading path[%d]: %q", i, path).Err() 557 } 558 if !fi.IsDir() { 559 path = filepath.Dir(path) 560 fi, err := os.Lstat(path) 561 if err != nil { 562 // given that we know that the original `path` exists, this SHOULD be 563 // impossible, but FUSE exists so... idk. 564 return "", errors.Annotate(err, "reading Dir(path[%d]): %q", i, path).Err() 565 } 566 if !fi.IsDir() { 567 // this SHOULD ALSO be impossible... 568 return "", errors.Annotate(err, "path %q could not be resolved to parent dir", path).Err() 569 } 570 } 571 572 if !strings.HasSuffix(path, sep) { 573 path = path + sep 574 } 575 cleanPaths[i] = cleanPath{ 576 path: path, 577 slashes: findPathSeparators(path), 578 } 579 } 580 581 // sort by length and then alphabetically 582 sort.Slice(cleanPaths, sortby.Chain{ 583 func(i, j int) bool { return len(cleanPaths[i].path) < len(cleanPaths[j].path) }, 584 func(i, j int) bool { return cleanPaths[i].path < cleanPaths[j].path }, 585 }.Use) 586 587 candidate := cleanPaths[0] 588 589 // We want to see if all the slashes in all other candidates line up with the 590 // slashes in `candidate`. 591 // 592 // We are already making some lexical assumptions about the paths here; If we 593 // wanted to discard lexical assumptions we would need to do all permutations 594 // of SameFile checks for every directory combination in all paths, and pick 595 // the lowest one which matched (due to the possibility of e.g. bind mounts). 596 // 597 // However, ain't nobody got time for that. 598 slashesMatch := func(whichSlash int) bool { 599 for _, other := range cleanPaths[1:] { 600 // whichSlash+1 because we want to include everything up to, and 601 // including, whichSlash. 602 for i, slashIdx := range candidate.slashes[:whichSlash+1] { 603 if other.slashes[i] != slashIdx { 604 return false 605 } 606 } 607 } 608 return true 609 } 610 611 // for each slash in the candidate, see if all cleanPaths at this slash return 612 // true from os.SameFile vs candidate. 613 // 614 // Calling this function implies that all other paths have already been 615 // verified with slashesMatch. 616 // 617 // The first check would avoid comparing "/long/f" vs "/s/long"; Although they 618 // both have a slash at 7, the prefix leading to that doesn't match. See 619 // comment on slashesMatch. 620 trySlash := func(curPath string) (bool, error) { 621 var curFi os.FileInfo 622 for _, other := range cleanPaths[1:] { 623 otherPath := other.path[:len(curPath)] 624 if otherPath == curPath { 625 // exact match, try the next one 626 continue 627 } 628 629 // ok, try SameFile 630 if curFi == nil { 631 var err error 632 if curFi, err = os.Lstat(curPath); err != nil { 633 return false, err 634 } 635 } 636 otherFi, err := os.Lstat(otherPath) 637 if err != nil { 638 return false, err 639 } 640 if !os.SameFile(curFi, otherFi) { 641 return false, nil 642 } 643 } 644 return true, nil 645 } 646 647 for whichSlash := len(candidate.slashes) - 1; whichSlash >= 0; whichSlash-- { 648 // curPath includes trailing slash 649 curPath := candidate.path[:candidate.slashes[whichSlash]+1] 650 651 // check if it's out of bounds 652 for _, sentinel := range rootSentinels { 653 sentinelPath := filepath.Join(curPath, sentinel) 654 if _, err := os.Lstat(sentinelPath); err == nil { 655 return "", errors.Annotate(ErrRootSentinel, "%q", sentinelPath).Err() 656 } else if !os.IsNotExist(err) { 657 return "", errors.Annotate(err, "failed to read root sentinel %q", sentinelPath).Err() 658 } 659 } 660 661 // Check to see if all other candidates have the same slash structure; i.e. 662 // the first `whichSlash` number of slashes line up across all paths. This 663 // avoids doing stats to see if "/a/path/" and "/path/a/" are the same file. 664 if !slashesMatch(whichSlash) { 665 continue 666 } 667 668 // all slashes match, let's see if the targeted files are the same (either 669 // lexically equivalent or are os.SameFile) 670 ok, err := trySlash(curPath) 671 if err != nil { 672 return "", err 673 } 674 if ok { 675 return curPath, nil 676 } 677 } 678 679 // Should never get here: 680 // * We are on unix and so whichSlash==0 is always the path "/". Either we 681 // found the root sentinel above, or returned "/" successfully. 682 // * We are on !unix and so whichSlash==0 is always a Volume (e.g. "C:\\"). 683 // However we already checked when resolving `paths` at the top that all 684 // the cleanPaths share the SAME volume. Thus we should have either found 685 // the root sentinel or returned this same volume (i.e. `commonVolume`). 686 panic("impossible") 687 }