github.com/treeverse/lakefs@v1.24.1-0.20240520134607-95648127bfb0/pkg/fileutil/io.go (about) 1 package fileutil 2 3 import ( 4 "errors" 5 "fmt" 6 "io" 7 "io/fs" 8 "os" 9 "path/filepath" 10 "strings" 11 ) 12 13 const ( 14 DefaultDirectoryMask = 0o755 15 ) 16 17 var ( 18 ErrNotFile = errors.New("path is not a file") 19 ErrBadPath = errors.New("bad path traversal blocked") 20 ErrSymbolicLink = errors.New("symbolic links not supported") 21 ErrInvalidPath = errors.New("invalid path") 22 ) 23 24 // IsDir Returns true if p is a directory, otherwise false 25 func IsDir(p string) (bool, error) { 26 stat, err := os.Stat(p) 27 if err != nil { 28 return false, err 29 } 30 return stat.IsDir(), nil 31 } 32 33 // FindInParents Returns the first occurrence of filename going up the dir tree 34 func FindInParents(dir, filename string) (string, error) { 35 var lookup string 36 fullPath, err := filepath.Abs(dir) 37 if err != nil { 38 return "", err 39 } 40 volumeName := filepath.VolumeName(fullPath) 41 for fullPath != filepath.Join(volumeName, string(filepath.Separator)) { 42 info, err := os.Stat(fullPath) 43 if err != nil { 44 return "", fmt.Errorf("%s: %w", fullPath, err) 45 } 46 47 if !info.IsDir() { 48 // find filename here 49 lookup = filepath.Join(filepath.Dir(fullPath), filename) 50 } else { 51 lookup = filepath.Join(fullPath, filename) 52 } 53 _, err = os.Stat(lookup) 54 if err == nil { 55 return lookup, nil 56 } 57 if !errors.Is(err, fs.ErrNotExist) { 58 return "", err 59 } 60 // error == fs.ErrNotExist 61 fullPath = filepath.Dir(fullPath) 62 } 63 return "", nil 64 } 65 66 func IsDirEmpty(name string) (bool, error) { 67 f, err := os.Open(name) 68 if err != nil { 69 return false, err 70 } 71 defer func() { _ = f.Close() }() 72 73 _, err = f.Readdir(1) 74 if err == io.EOF { 75 return true, nil 76 } 77 return false, err 78 } 79 80 // PruneEmptyDirectories iterates through the directory tree, removing empty directories, and directories that only 81 // contain empty directories. 82 func PruneEmptyDirectories(dirPath string) ([]string, error) { 83 // Check if the directory exists 84 info, err := os.Stat(dirPath) 85 if err != nil { 86 return nil, err 87 } 88 89 // Skip if it's not a directory 90 if !info.IsDir() { 91 return nil, nil 92 } 93 94 // Read the directory contents 95 entries, err := os.ReadDir(dirPath) 96 if err != nil { 97 return nil, err 98 } 99 100 // Recurse through the directory entries 101 var pruned []string 102 for _, entry := range entries { 103 if !entry.IsDir() { 104 continue 105 } 106 107 subDirPath := filepath.Join(dirPath, entry.Name()) 108 prunedDirs, err := PruneEmptyDirectories(subDirPath) 109 if err != nil { 110 return nil, err 111 } 112 // Collect the pruned directories 113 pruned = append(pruned, prunedDirs...) 114 115 // Re-read the directory contents to check if it's empty now 116 empty, err := IsDirEmpty(subDirPath) 117 if err != nil { 118 return nil, err 119 } 120 if empty { 121 err = os.Remove(subDirPath) 122 if err != nil { 123 return nil, err 124 } 125 pruned = append(pruned, subDirPath) 126 } 127 } 128 129 return pruned, nil 130 } 131 132 func RemoveFile(p string) error { 133 fileExists, err := FileExists(p) 134 if err != nil { 135 return err 136 } 137 if !fileExists { 138 return nil // does not exist 139 } 140 return os.Remove(p) 141 } 142 143 func FileExists(p string) (bool, error) { 144 info, err := os.Stat(p) 145 if os.IsNotExist(err) { 146 return false, nil 147 } else if err != nil { 148 return false, err 149 } 150 if !info.IsDir() { 151 return true, nil 152 } 153 return false, fmt.Errorf("%s: %w", p, ErrNotFile) 154 } 155 156 func VerifyAbsPath(absPath, basePath string) error { 157 // check we have a valid abs path 158 if !filepath.IsAbs(absPath) || filepath.Clean(absPath) != absPath { 159 return ErrBadPath 160 } 161 // point to storage namespace 162 if !strings.HasPrefix(absPath, basePath) { 163 return ErrInvalidPath 164 } 165 return nil 166 } 167 168 func VerifyRelPath(relPath, basePath string) error { 169 abs := filepath.Join(basePath, relPath) 170 return VerifyAbsPath(abs, basePath) 171 } 172 173 // VerifySafeFilename checks that the given file name is not a symbolic link and that 174 // the file name does not contain path traversal 175 func VerifySafeFilename(absPath string) error { 176 if err := VerifyAbsPath(absPath, absPath); err != nil { 177 return err 178 } 179 if !filepath.IsAbs(absPath) { 180 return fmt.Errorf("relative path not allowed: %w", ErrInvalidPath) 181 } 182 filename, err := filepath.EvalSymlinks(absPath) 183 if err != nil { 184 return err 185 } 186 if filename != absPath { 187 return ErrSymbolicLink 188 } 189 return nil 190 }