github.com/haraldrudell/parl@v0.4.176/pos/appdir.go (about) 1 /* 2 © 2021–present Harald Rudell <harald.rudell@gmail.com> (https://haraldrudell.github.io/haraldrudell/) 3 ISC License 4 */ 5 6 package pos 7 8 import ( 9 "os" 10 "path/filepath" 11 "sync/atomic" 12 "unicode" 13 14 "github.com/haraldrudell/parl/perrors" 15 "github.com/haraldrudell/parl/pfs" 16 "github.com/haraldrudell/parl/punix" 17 ) 18 19 const ( 20 // path segment “.local”. 21 // “~/.local/share” is a standardized directory on Linux 22 dotLocalDir = ".local" 23 // path segement “share”. 24 // “~/.local/share” is a standardized directory on Linux 25 shareDir = "share" 26 // mode for created directories 27 urwx os.FileMode = 0700 28 ) 29 30 // for testing 31 var homeDirHook string 32 33 // AppDirectory manages a per-user writable app-specific directory 34 type AppDirectory struct { 35 // app name like “myapp” 36 App string 37 // absolute clean symlink-free path if app-directory exists 38 // - macOS: “/Users/user/.local/share/myapp” 39 // - Linux: “/home/user/.local/share/myapp” 40 abs atomic.Pointer[string] 41 } 42 43 // NewAppDir returns a writable directory object in the user’s home directory 44 // - appName: application name like “myapp” 45 // Unicode letters and digits 46 // - directory is “~/.local/share/[appName]” 47 // - parent directory is based on the running process’ owner 48 // - does not rely on environment variables 49 // 50 // Usage: 51 // 52 // var appDir = NewAppDir("myapp") 53 // if err = appDir.EnsureDir(); err != nil {… 54 // var knownToExistAbsCleanNoSymlinksNeverErrors = appDir.Directory() 55 func NewAppDir(appName string) (appd *AppDirectory) { return &AppDirectory{App: appName} } 56 57 // best-effort single-value absolute clean possibly symlink-free directory 58 // - returns an absolute path whether the directory exists or not 59 // - if directory exists, absolute clean symlink-free, otherwise absolute clean 60 // - Directory may panic from errors that are returned by [AppDirectory.EnsureDir] or 61 // [AppDirectory.Path]. 62 // To avoid panics, invoke those methods first. 63 // 64 // Usage: 65 // 66 // var dir = NewAppDir("myapp").Directory() 67 func (d *AppDirectory) Directory() (abs string) { 68 var isNotExist bool 69 var err error 70 if abs, isNotExist, err = d.Path(); err != nil && !isNotExist { 71 panic(err) // some error 72 } 73 return 74 } 75 76 // EnsureDir ensures the directory exists 77 func (d *AppDirectory) EnsureDir() (err error) { 78 79 // get path while checking if already exists 80 var abs string 81 var isNotExist bool 82 if abs, isNotExist, err = d.Path(); err == nil { 83 return // directory already exists return 84 } else if !isNotExist { 85 return // some error 86 } 87 88 // MkDirAll begins with stat to see if path exists 89 if err = os.MkdirAll(abs, urwx); perrors.IsPF(&err, "os.MkdirAll: %w", err) { 90 return 91 } 92 // update d.abs 93 _, _, err = d.eval(abs) 94 95 return 96 } 97 98 // Path returns best-effort absolute clean path 99 // - if the app-directory exists, abs is also symlink-free 100 // - outcomes: 101 // - — err: nil: abs is absolute clean symlink-free, app directory exists 102 // - — isNotExist: true, err: non-nil: app directory does not eixst. 103 // abs is absolute clean. 104 // err is errno ENOENT 105 // - — err: non-nil, isNotExist: false: some error 106 // - — 107 // - macOS: “/Users/user/.local/share/myapp” 108 // - Linux: “/home/user/.local/share/myapp” 109 // - note: symlinks can only be evaled if a path exists 110 func (d *AppDirectory) Path() (abs string, isNotExist bool, err error) { 111 112 // if already present 113 if ap := d.abs.Load(); ap != nil { 114 abs = *ap 115 return // success: already has abs, directory exists return 116 } 117 118 // check appName 119 var appName string 120 if appName, err = d.checkAppName(); err != nil { 121 return // bad appName return 122 } 123 124 // get user’s home directory 125 var homeDir string 126 if h := homeDirHook; h == "" { 127 if homeDir, err = UserHome(); err != nil { 128 return // failure to obtain home directory return 129 } 130 } else { 131 homeDir = h 132 } 133 134 // get app directory’s parent 135 // - absolute, maybe unclean, maybe symlinks 136 var parentDir = filepath.Join(homeDir, dotLocalDir, shareDir) 137 138 // get app directory 139 // - absolute, maybe unclean, maybe symlinks 140 var a = filepath.Join(parentDir, appName) 141 142 // try to unsymlink app directory 143 if abs, isNotExist, err = d.eval(a); err == nil { 144 return // app directory exists success return 145 } else if !isNotExist { 146 return // some error return 147 } 148 // err is non-nil, isNotExist true 149 150 // try to unsymlink parent directory 151 if p, e := pfs.AbsEval(parentDir); e != nil { 152 if punix.IsENOENT(e) { 153 abs = a 154 return // parent no exist either, return isNotExist result 155 } 156 err = e // some new error 157 isNotExist = false 158 return // return error from parent directory 159 } else { 160 // use th evealed parent directory 161 abs = filepath.Clean(filepath.Join(p, appName)) 162 } 163 164 return // parent exists, app dir does not isNotExist return 165 } 166 167 // checks that appName is usable 168 func (d *AppDirectory) checkAppName() (appName string, err error) { 169 170 if appName = d.App; appName == "" { 171 err = perrors.NewPF("appName cannot be empty") 172 return // empty error return 173 } 174 175 for i, c := range appName { 176 if !unicode.IsDigit(c) && !unicode.IsLetter(c) { 177 err = perrors.ErrorfPF( 178 "appName can only contain Unicode letters or digits: #%d: %q", 179 i, c, 180 ) 181 return // bad character error return 182 } 183 } 184 185 return // good return 186 } 187 188 // eval evaluates the full app directory path 189 // - on success, updates d.abs 190 func (d *AppDirectory) eval(path string) (abs string, isNotExist bool, err error) { 191 var a string 192 193 if a, err = pfs.AbsEval(path); err != nil { 194 isNotExist = punix.IsENOENT(err) 195 return // some error including does not exist 196 } 197 198 // success, app directory exists and is evaled 199 d.abs.CompareAndSwap(nil, &a) 200 abs = a 201 202 return // success, directory exists 203 }