github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/libkb/home.go (about) 1 // Copyright 2015 Keybase, Inc. All rights reserved. Use of 2 // this source code is governed by the included BSD license. 3 4 package libkb 5 6 import ( 7 "fmt" 8 "os" 9 "os/user" 10 "path/filepath" 11 "regexp" 12 "runtime" 13 "strings" 14 "unicode" 15 "unicode/utf8" 16 17 "github.com/keybase/client/go/protocol/keybase1" 18 ) 19 20 type ConfigGetter func() string 21 type RunModeGetter func() RunMode 22 type EnvGetter func(s string) string 23 24 type Base struct { 25 appName string 26 getHomeFromCmd ConfigGetter 27 getHomeFromConfig ConfigGetter 28 getMobileSharedHome ConfigGetter 29 getRunMode RunModeGetter 30 getLog LogGetter 31 getenvFunc EnvGetter 32 } 33 34 type HomeFinder interface { 35 CacheDir() string 36 SharedCacheDir() string 37 ConfigDir() string 38 DownloadsDir() string 39 Home(emptyOk bool) string 40 MobileSharedHome(emptyOk bool) string 41 DataDir() string 42 SharedDataDir() string 43 RuntimeDir() string 44 Normalize(s string) string 45 LogDir() string 46 ServiceSpawnDir() (string, error) 47 SandboxCacheDir() string // For macOS 48 InfoDir() string 49 IsNonstandardHome() (bool, error) 50 } 51 52 func (b Base) getHome() string { 53 if b.getHomeFromCmd != nil { 54 ret := b.getHomeFromCmd() 55 if ret != "" { 56 return ret 57 } 58 } 59 if b.getHomeFromConfig != nil { 60 ret := b.getHomeFromConfig() 61 if ret != "" { 62 return ret 63 } 64 } 65 return "" 66 } 67 68 func (b Base) IsNonstandardHome() (bool, error) { 69 return false, fmt.Errorf("unsupported on %s", runtime.GOOS) 70 } 71 72 func (b Base) getenv(s string) string { 73 if b.getenvFunc != nil { 74 return b.getenvFunc(s) 75 } 76 return os.Getenv(s) 77 } 78 79 func (b Base) Join(elem ...string) string { return filepath.Join(elem...) } 80 81 type XdgPosix struct { 82 Base 83 } 84 85 func (x XdgPosix) Normalize(s string) string { return s } 86 87 func (x XdgPosix) Home(emptyOk bool) string { 88 ret := x.getHome() 89 if len(ret) == 0 && !emptyOk { 90 ret = x.getenv("HOME") 91 } 92 if ret == "" { 93 return "" 94 } 95 resolved, err := filepath.Abs(ret) 96 if err != nil { 97 return ret 98 } 99 return resolved 100 } 101 102 // IsNonstandardHome is true if the home directory gleaned via cmdline, 103 // env, or config is different from that in /etc/passwd. 104 func (x XdgPosix) IsNonstandardHome() (bool, error) { 105 passed := x.Home(false) 106 if passed == "" { 107 return false, nil 108 } 109 passwd, err := user.Current() 110 if err != nil { 111 return false, err 112 } 113 passwdAbs, err := filepath.Abs(passwd.HomeDir) 114 if err != nil { 115 return false, err 116 } 117 passedAbs, err := filepath.Abs(passed) 118 if err != nil { 119 return false, err 120 } 121 return passedAbs != passwdAbs, nil 122 } 123 124 func (x XdgPosix) MobileSharedHome(emptyOk bool) string { 125 return x.Home(emptyOk) 126 } 127 128 func (x XdgPosix) dirHelper(xdgEnvVar string, prefixDirs ...string) string { 129 appName := x.appName 130 if x.getRunMode() != ProductionRunMode { 131 appName = appName + "." + string(x.getRunMode()) 132 } 133 134 isNonstandard, isNonstandardErr := x.IsNonstandardHome() 135 xdgSpecified := x.getenv(xdgEnvVar) 136 137 // If the user specified a nonstandard home directory, or there's no XDG 138 // environment variable present, use the home directory from the 139 // commandline/environment/config. 140 if (isNonstandardErr == nil && isNonstandard) || xdgSpecified == "" { 141 alternateDir := x.Join(append([]string{x.Home(false)}, prefixDirs...)...) 142 return x.Join(alternateDir, appName) 143 } 144 145 // Otherwise, use the XDG standard. 146 return x.Join(xdgSpecified, appName) 147 } 148 149 func (x XdgPosix) ConfigDir() string { return x.dirHelper("XDG_CONFIG_HOME", ".config") } 150 func (x XdgPosix) CacheDir() string { return x.dirHelper("XDG_CACHE_HOME", ".cache") } 151 func (x XdgPosix) SharedCacheDir() string { return x.CacheDir() } 152 func (x XdgPosix) SandboxCacheDir() string { return "" } // Unsupported 153 func (x XdgPosix) DataDir() string { return x.dirHelper("XDG_DATA_HOME", ".local", "share") } 154 func (x XdgPosix) SharedDataDir() string { return x.DataDir() } 155 func (x XdgPosix) DownloadsDir() string { 156 xdgSpecified := x.getenv("XDG_DOWNLOAD_DIR") 157 if xdgSpecified != "" { 158 return xdgSpecified 159 } 160 return filepath.Join(x.Home(false), "Downloads") 161 } 162 func (x XdgPosix) RuntimeDir() string { return x.dirHelper("XDG_RUNTIME_DIR", ".config") } 163 func (x XdgPosix) InfoDir() string { return x.RuntimeDir() } 164 165 func (x XdgPosix) ServiceSpawnDir() (ret string, err error) { 166 ret = x.RuntimeDir() 167 if len(ret) == 0 { 168 ret, err = os.MkdirTemp("", "keybase_service") 169 } 170 return 171 } 172 173 func (x XdgPosix) LogDir() string { 174 // There doesn't seem to be an official place for logs in the XDG spec, but 175 // according to http://stackoverflow.com/a/27965014/823869 at least, this 176 // is the best compromise. 177 return x.CacheDir() 178 } 179 180 type Darwin struct { 181 Base 182 forceIOS bool // for testing 183 } 184 185 func toUpper(s string) string { 186 if s == "" { 187 return s 188 } 189 a := []rune(s) 190 a[0] = unicode.ToUpper(a[0]) 191 return string(a) 192 } 193 194 func (d Darwin) isIOS() bool { 195 return isIOS || d.forceIOS 196 } 197 198 func (d Darwin) appDir(dirs ...string) string { 199 appName := toUpper(d.appName) 200 runMode := d.getRunMode() 201 if runMode != ProductionRunMode { 202 appName += toUpper(string(runMode)) 203 } 204 dirs = append(dirs, appName) 205 return filepath.Join(dirs...) 206 } 207 208 func (d Darwin) sharedHome() string { 209 homeDir := d.Home(false) 210 if d.isIOS() { 211 // check if we have a shared container path, and if so, that is where the shared home is. 212 sharedHome := d.getMobileSharedHome() 213 if len(sharedHome) > 0 { 214 homeDir = sharedHome 215 } 216 } 217 return homeDir 218 } 219 220 func (d Darwin) CacheDir() string { 221 return d.appDir(d.Home(false), "Library", "Caches") 222 } 223 224 func (d Darwin) SharedCacheDir() string { 225 return d.appDir(d.sharedHome(), "Library", "Caches") 226 } 227 228 func (d Darwin) SandboxCacheDir() string { 229 if d.isIOS() { 230 return "" 231 } 232 return d.CacheDir() 233 // The container name "keybase" is the group name specified in the entitlement for sandboxed extensions 234 // Note: this was added for kbfs finder integration, which was never activated. 235 // keybased.sock and kbfsd.sock live in this directory. 236 // return d.appDir(d.Home(false), "Library", "keybase", "Library", "Caches") 237 } 238 func (d Darwin) ConfigDir() string { 239 return d.appDir(d.sharedHome(), "Library", "Application Support") 240 } 241 func (d Darwin) DataDir() string { 242 return d.appDir(d.Home(false), "Library", "Application Support") 243 } 244 func (d Darwin) SharedDataDir() string { 245 return d.appDir(d.sharedHome(), "Library", "Application Support") 246 } 247 func (d Darwin) RuntimeDir() string { return d.CacheDir() } 248 func (d Darwin) ServiceSpawnDir() (string, error) { return d.RuntimeDir(), nil } 249 func (d Darwin) LogDir() string { 250 appName := toUpper(d.appName) 251 runMode := d.getRunMode() 252 dirs := []string{d.Home(false), "Library", "Logs"} 253 if runMode != ProductionRunMode { 254 dirs = append(dirs, appName+toUpper(string(runMode))) 255 } 256 return filepath.Join(dirs...) 257 } 258 259 func (d Darwin) InfoDir() string { 260 // If the user is explicitly passing in a HomeDirectory, make the PID file directory 261 // local to that HomeDir. This way it's possible to have multiple keybases in parallel 262 // running for a given run mode, without having to explicitly specify a PID file. 263 if d.getHome() != "" { 264 return d.CacheDir() 265 } 266 return d.appDir(os.TempDir()) 267 } 268 269 func (d Darwin) DownloadsDir() string { 270 return filepath.Join(d.Home(false), "Downloads") 271 } 272 273 func (d Darwin) Home(emptyOk bool) string { 274 ret := d.getHome() 275 if len(ret) == 0 && !emptyOk { 276 ret = d.getenv("HOME") 277 } 278 return ret 279 } 280 281 func (d Darwin) MobileSharedHome(emptyOk bool) string { 282 var ret string 283 if d.getMobileSharedHome != nil { 284 ret = d.getMobileSharedHome() 285 } 286 if len(ret) == 0 && !emptyOk { 287 ret = d.getenv("MOBILE_SHARED_HOME") 288 } 289 return ret 290 } 291 292 func (d Darwin) Normalize(s string) string { return s } 293 294 type Win32 struct { 295 Base 296 } 297 298 var win32SplitRE = regexp.MustCompile(`[/\\]`) 299 300 func (w Win32) Split(s string) []string { 301 return win32SplitRE.Split(s, -1) 302 } 303 304 func (w Win32) Unsplit(v []string) string { 305 if len(v) > 0 && len(v[0]) == 0 { 306 v2 := make([]string, len(v)) 307 copy(v2, v) 308 v[0] = string(filepath.Separator) 309 } 310 result := filepath.Join(v...) 311 // filepath.Join doesn't add a separator on Windows after the drive 312 if len(v) > 0 && result[len(v[0])] != filepath.Separator { 313 v = append(v[:1], v...) 314 v[1] = string(filepath.Separator) 315 result = filepath.Join(v...) 316 } 317 return result 318 } 319 320 func (w Win32) Normalize(s string) string { 321 return w.Unsplit(w.Split(s)) 322 } 323 324 func (w Win32) CacheDir() string { return w.Home(false) } 325 func (w Win32) SharedCacheDir() string { return w.CacheDir() } 326 func (w Win32) SandboxCacheDir() string { return "" } // Unsupported 327 func (w Win32) ConfigDir() string { return w.Home(false) } 328 func (w Win32) DataDir() string { return w.Home(false) } 329 func (w Win32) SharedDataDir() string { return w.DataDir() } 330 func (w Win32) RuntimeDir() string { return w.Home(false) } 331 func (w Win32) InfoDir() string { return w.RuntimeDir() } 332 func (w Win32) ServiceSpawnDir() (string, error) { return w.RuntimeDir(), nil } 333 func (w Win32) LogDir() string { return w.Home(false) } 334 335 func (w Win32) deriveFromTemp() (ret string) { 336 tmp := w.getenv("TEMP") 337 if len(tmp) == 0 { 338 w.getLog().Info("No 'TEMP' environment variable found") 339 tmp = w.getenv("TMP") 340 if len(tmp) == 0 { 341 w.getLog().Fatalf("No 'TMP' environment variable found") 342 } 343 } 344 v := w.Split(tmp) 345 if len(v) < 2 { 346 w.getLog().Fatalf("Bad 'TEMP' variable found, no directory separators!") 347 } 348 last := strings.ToLower(v[len(v)-1]) 349 rest := v[0 : len(v)-1] 350 if last != "temp" && last != "tmp" { 351 w.getLog().Warning("TEMP directory didn't end in \\Temp: %s", last) 352 } 353 if strings.ToLower(rest[len(rest)-1]) == "local" { 354 rest[len(rest)-1] = "Roaming" 355 } 356 ret = w.Unsplit(rest) 357 return 358 } 359 360 func (w Win32) DownloadsDir() string { 361 // Prefer to use USERPROFILE instead of w.Home() because the latter goes 362 // into APPDATA. 363 user, err := user.Current() 364 if err != nil { 365 return filepath.Join(w.Home(false), "Downloads") 366 } 367 return filepath.Join(user.HomeDir, "Downloads") 368 } 369 370 func (w Win32) Home(emptyOk bool) string { 371 ret := w.getHome() 372 if len(ret) == 0 && !emptyOk { 373 ret, _ = LocalDataDir() 374 if len(ret) == 0 { 375 w.getLog().Info("APPDATA environment variable not found") 376 } 377 378 } 379 if len(ret) == 0 && !emptyOk { 380 ret = w.deriveFromTemp() 381 } 382 383 packageName := "Keybase" 384 385 if w.getRunMode() == DevelRunMode || w.getRunMode() == StagingRunMode { 386 runModeName := string(w.getRunMode()) 387 if runModeName != "" { 388 // Capitalize the first letter 389 r, n := utf8.DecodeRuneInString(runModeName) 390 runModeName = string(unicode.ToUpper(r)) + runModeName[n:] 391 packageName += runModeName 392 } 393 } 394 395 ret = filepath.Join(ret, packageName) 396 397 return ret 398 } 399 400 func (w Win32) MobileSharedHome(emptyOk bool) string { 401 return w.Home(emptyOk) 402 } 403 404 func NewHomeFinder(appName string, getHomeFromCmd ConfigGetter, getHomeFromConfig ConfigGetter, getMobileSharedHome ConfigGetter, 405 osname string, getRunMode RunModeGetter, getLog LogGetter, getenv EnvGetter) HomeFinder { 406 base := Base{appName, getHomeFromCmd, getHomeFromConfig, getMobileSharedHome, getRunMode, getLog, getenv} 407 switch runtimeGroup(osname) { 408 case keybase1.RuntimeGroup_WINDOWSLIKE: 409 return Win32{base} 410 case keybase1.RuntimeGroup_DARWINLIKE: 411 return Darwin{Base: base} 412 default: 413 return XdgPosix{base} 414 } 415 }