github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/internal/uniqid/uniqid.go (about)

     1  package uniqid
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"path/filepath"
     9  	"runtime"
    10  
    11  	"github.com/ActiveState/cli/internal/errs"
    12  	"github.com/ActiveState/cli/internal/osutils/user"
    13  	"github.com/google/uuid"
    14  )
    15  
    16  // BaseDirLocation facilitates base directory location option enums.
    17  type BaseDirLocation int
    18  
    19  // BaseDirLocation enums define the base directory location options.
    20  const (
    21  	InHome BaseDirLocation = iota
    22  	InTmp
    23  )
    24  
    25  const (
    26  	fileName   = "uniqid"
    27  	persistDir = "activestate_persist"
    28  )
    29  
    30  // UniqID manages the storage and retrieval of a unique id.
    31  type UniqID struct {
    32  	ID uuid.UUID
    33  }
    34  
    35  // New retrieves or creates a new unique id.
    36  func New(base BaseDirLocation) (*UniqID, error) {
    37  	dir, err := storageDirectory(base)
    38  	if err != nil {
    39  		return nil, fmt.Errorf("cannot determine uniqid storage directory: %w", err)
    40  	}
    41  
    42  	id, err := uniqueID(filepath.Join(dir, fileName))
    43  	if err != nil {
    44  		return nil, fmt.Errorf("cannot obtain uniqid: %w", err)
    45  	}
    46  
    47  	return &UniqID{ID: id}, nil
    48  }
    49  
    50  // String implements fmt.Stringer.
    51  func (u *UniqID) String() string {
    52  	return u.ID.String()
    53  }
    54  
    55  func uniqueID(filepath string) (uuid.UUID, error) {
    56  	// For a transitionary period where the old persist directory may exist on
    57  	// Windows we want to move the uniqid file to a better location.
    58  	// This code should be removed after some time.
    59  	if !fileExists(filepath) {
    60  		err := moveUniqidFile(filepath)
    61  		if err != nil {
    62  			return uuid.UUID{}, fmt.Errorf("Could not move legacy uniqid file: %w", err)
    63  		}
    64  	}
    65  
    66  	data, err := readFile(filepath)
    67  	if errors.Is(err, os.ErrNotExist) {
    68  		id := uuid.New()
    69  
    70  		if err := writeFile(filepath, id[:]); err != nil {
    71  			return uuid.UUID{}, fmt.Errorf("cannot write uniqid file: %w", err)
    72  		}
    73  
    74  		return id, nil
    75  	}
    76  	if err != nil {
    77  		return uuid.UUID{}, fmt.Errorf("Could not read uniqid file: %w", err)
    78  	}
    79  
    80  	id, err := uuid.FromBytes(data)
    81  	if err != nil {
    82  		return uuid.UUID{}, fmt.Errorf("Could not create new UUID from uniqid file data: %w", err)
    83  	}
    84  
    85  	return id, nil
    86  }
    87  
    88  // ErrUnsupportedOS indicates that an unsupported OS tried to store a uniqid as
    89  // a file.
    90  var ErrUnsupportedOS = errors.New("unsupported uniqid os")
    91  
    92  func storageDirectory(base BaseDirLocation) (string, error) {
    93  	var dir string
    94  	switch base {
    95  	case InTmp:
    96  		dir = filepath.Join(os.TempDir(), persistDir)
    97  
    98  	default:
    99  		home, err := user.HomeDir()
   100  		if err != nil {
   101  			return "", fmt.Errorf("cannot get home dir for uniqid file: %w", err)
   102  		}
   103  		dir = home
   104  	}
   105  
   106  	var subdir string
   107  	switch runtime.GOOS {
   108  	case "darwin":
   109  		subdir = "Library/Application Support"
   110  	case "linux":
   111  		subdir = ".local/share"
   112  	case "windows":
   113  		subdir = "AppData/Local"
   114  	default:
   115  		return "", ErrUnsupportedOS
   116  	}
   117  
   118  	return filepath.Join(dir, subdir, persistDir), nil
   119  }
   120  
   121  // The following is copied from fileutils to avoid cyclical importing. As
   122  // global usage in the code is minimized, or logging is removed from fileutils,
   123  // this may be removed.
   124  
   125  // readFile reads the content of a file
   126  func readFile(filePath string) ([]byte, error) {
   127  	b, err := os.ReadFile(filePath)
   128  	if err != nil {
   129  		return nil, fmt.Errorf("os.ReadFile %s failed: %w", filePath, err)
   130  	}
   131  	return b, nil
   132  }
   133  
   134  // dirExists checks if the given directory exists
   135  func dirExists(path string) bool {
   136  	fi, err := os.Stat(path)
   137  	if err != nil {
   138  		return false
   139  	}
   140  
   141  	mode := fi.Mode()
   142  	return mode.IsDir()
   143  }
   144  
   145  // mkdir is a small helper function to create a directory if it doesnt already exist
   146  func mkdir(path string, subpath ...string) error {
   147  	if len(subpath) > 0 {
   148  		subpathStr := filepath.Join(subpath...)
   149  		path = filepath.Join(path, subpathStr)
   150  	}
   151  	if _, err := os.Stat(path); os.IsNotExist(err) {
   152  		err = os.MkdirAll(path, os.ModePerm)
   153  		if err != nil {
   154  			return fmt.Errorf("MkdirAll failed for path: %s: %w", path, err)
   155  		}
   156  	}
   157  	return nil
   158  }
   159  
   160  // mkdirUnlessExists will make the directory structure if it doesn't already exists
   161  func mkdirUnlessExists(path string) error {
   162  	if dirExists(path) {
   163  		return nil
   164  	}
   165  	return mkdir(path)
   166  }
   167  
   168  // fileExists checks if the given file (not folder) exists
   169  func fileExists(path string) bool {
   170  	fi, err := os.Stat(path)
   171  	if err != nil {
   172  		return false
   173  	}
   174  
   175  	mode := fi.Mode()
   176  	return mode.IsRegular()
   177  }
   178  
   179  // writeFile writes data to a file, if it exists it is overwritten, if it doesn't exist it is created and data is written
   180  func writeFile(filePath string, data []byte) (rerr error) {
   181  	err := mkdirUnlessExists(filepath.Dir(filePath))
   182  	if err != nil {
   183  		return err
   184  	}
   185  
   186  	// make the target file temporarily writable
   187  	fileExists := fileExists(filePath)
   188  	if fileExists {
   189  		stat, _ := os.Stat(filePath)
   190  		if err := os.Chmod(filePath, 0644); err != nil {
   191  			return fmt.Errorf("os.Chmod %s failed: %w", filePath, err)
   192  		}
   193  		defer func() {
   194  			err = os.Chmod(filePath, stat.Mode().Perm())
   195  			if err != nil {
   196  				rerr = errs.Pack(rerr, errs.Wrap(err, "os.Chmod %s failed", filePath))
   197  			}
   198  		}()
   199  	}
   200  
   201  	f, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
   202  	if err != nil {
   203  		if !fileExists {
   204  			target := filepath.Dir(filePath)
   205  			err = fmt.Errorf("access to target %q is denied", target)
   206  		}
   207  		return fmt.Errorf("os.OpenFile %s failed: %w", filePath, err)
   208  	}
   209  	defer f.Close()
   210  
   211  	_, err = f.Write(data)
   212  	if err != nil {
   213  		return fmt.Errorf("file.Write %s failed: %w", filePath, err)
   214  	}
   215  	return nil
   216  }
   217  
   218  // copyFile copies a file from one location to another
   219  func copyFile(src, target string) error {
   220  	in, err := os.Open(src)
   221  	if err != nil {
   222  		return fmt.Errorf("os.Open %s failed: %w", src, err)
   223  	}
   224  	defer in.Close()
   225  
   226  	// Create target directory if it doesn't exist
   227  	dir := filepath.Dir(target)
   228  	err = mkdirUnlessExists(dir)
   229  	if err != nil {
   230  		return err
   231  	}
   232  
   233  	// Create target file
   234  	out, err := os.Create(target)
   235  	if err != nil {
   236  		return fmt.Errorf("os.Create %s failed: %w", target, err)
   237  	}
   238  	defer out.Close()
   239  
   240  	// Copy bytes to target file
   241  	_, err = io.Copy(out, in)
   242  	if err != nil {
   243  		return fmt.Errorf("io.Copy failed: %w", err)
   244  	}
   245  	err = out.Close()
   246  	if err != nil {
   247  		return fmt.Errorf("out.Close failed: %w", err)
   248  	}
   249  	return nil
   250  }