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  }