github.com/treeverse/lakefs@v1.24.1-0.20240520134607-95648127bfb0/pkg/fileutil/fs_case.go (about)

     1  package fileutil
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"os"
     7  	"path/filepath"
     8  	"strings"
     9  
    10  	nanoid "github.com/matoous/go-nanoid/v2"
    11  )
    12  
    13  // caseSensitiveNamePrefix is a prefix used for testing case sensitivity.
    14  // It contains both lowercase and uppercase characters.
    15  const caseSensitiveNamePrefix = ".cAsE."
    16  
    17  // IsCaseInsensitiveLocation returns true if dirPath is a directory on a
    18  // case-insensitive filesystem.  dirPath must be writable.  If it fails to
    19  // delete the file, it uses warnFunc to emit a warning message.
    20  func IsCaseInsensitiveLocation(fs FS, dirPath string, warnFunc func(string)) (bool, error) {
    21  	// Random element in the filename to ensure uniqueness: the filename
    22  	// will not be in use from anything else.
    23  	id, err := nanoid.New()
    24  	if err != nil {
    25  		return false, fmt.Errorf("generate random name: %w", err)
    26  	}
    27  	path := filepath.Join(dirPath, caseSensitiveNamePrefix+id)
    28  	err = fs.Touch(path)
    29  	if err != nil {
    30  		return false, fmt.Errorf("touch %s: %w", path, err)
    31  	}
    32  	defer func() {
    33  		err := fs.Remove(path)
    34  		if err != nil {
    35  			warnFunc(fmt.Sprintf("Garbage file %s remains: %s; make sure to delete this file", path, err))
    36  		}
    37  	}()
    38  
    39  	lowercasePrefix := strings.ToLower(caseSensitiveNamePrefix)
    40  	lowercasePath := filepath.Join(dirPath, lowercasePrefix+id)
    41  	// If lowercasePath exists, fs is case-insensitive.  Random id
    42  	// ensures that it can be no other file!
    43  	return fs.Exists(lowercasePath)
    44  }
    45  
    46  // FS is a tiny filesystem abstraction.  The standard io/fs does not support
    47  // any write operations, see https://github.com/golang/go/issues/45757.
    48  type FS interface {
    49  	// Touch creates a file at path.
    50  	Touch(path string) error
    51  	// Exists returns true if there is a file at path.  It follows
    52  	// symbolic links.
    53  	Exists(path string) (bool, error)
    54  	// Remove deletes the file at path.
    55  	Remove(path string) error
    56  }
    57  
    58  // OSFS is the filesystem of the OS.
    59  type OSFS struct{}
    60  
    61  func NewOSFS() FS {
    62  	return OSFS{}
    63  }
    64  
    65  func (OSFS) Touch(path string) error {
    66  	file, err := os.Create(path)
    67  	if err != nil {
    68  		return err
    69  	}
    70  	return file.Close()
    71  }
    72  
    73  func (OSFS) Exists(path string) (bool, error) {
    74  	_, err := os.Stat(path)
    75  	if errors.Is(err, os.ErrNotExist) {
    76  		return false, nil
    77  	}
    78  	if err != nil {
    79  		return false, err
    80  	}
    81  	return true, nil
    82  }
    83  
    84  func (OSFS) Remove(path string) error {
    85  	return os.Remove(path)
    86  }