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 }