go4.org@v0.0.0-20230225012048-214862532bf5/xdgdir/xdgdir.go (about) 1 /* 2 Copyright 2017 The go4 Authors 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 // Package xdgdir implements the Free Desktop Base Directory 18 // specification for locating directories. 19 // 20 // The specification is at 21 // http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html 22 package xdgdir // import "go4.org/xdgdir" 23 24 import ( 25 "errors" 26 "fmt" 27 "os" 28 "os/user" 29 "path/filepath" 30 "syscall" 31 ) 32 33 // Directories defined by the specification. 34 var ( 35 Data Dir 36 Config Dir 37 Cache Dir 38 Runtime Dir 39 ) 40 41 func init() { 42 // Placed in init for the sake of readable docs. 43 Data = Dir{ 44 env: "XDG_DATA_HOME", 45 dirsEnv: "XDG_DATA_DIRS", 46 fallback: ".local/share", 47 dirsFallback: []string{"/usr/local/share", "/usr/share"}, 48 } 49 Config = Dir{ 50 env: "XDG_CONFIG_HOME", 51 dirsEnv: "XDG_CONFIG_DIRS", 52 fallback: ".config", 53 dirsFallback: []string{"/etc/xdg"}, 54 } 55 Cache = Dir{ 56 env: "XDG_CACHE_HOME", 57 fallback: ".cache", 58 } 59 Runtime = Dir{ 60 env: "XDG_RUNTIME_DIR", 61 userOwned: true, 62 } 63 } 64 65 // A Dir is a logical base directory along with additional search 66 // directories. 67 type Dir struct { 68 // env is the name of the environment variable for the base directory 69 // relative to which files should be written. 70 env string 71 72 // dirsEnv is the name of the environment variable containing 73 // preference-ordered base directories to search for files. 74 dirsEnv string 75 76 // fallback is the home-relative path to use if the variable named by 77 // env is not set. 78 fallback string 79 80 // dirsFallback is the list of paths to use if the variable named by 81 // dirsEnv is not set. 82 dirsFallback []string 83 84 // If userOwned is true, then for the directory to be considered 85 // valid, it must be owned by the user with the mode 700. This is 86 // only used for XDG_RUNTIME_DIR. 87 userOwned bool 88 } 89 90 // String returns the name of the primary environment variable for the 91 // directory. 92 func (d Dir) String() string { 93 if d.env == "" { 94 panic("xdgdir.Dir.String() on zero Dir") 95 } 96 return d.env 97 } 98 99 // Path returns the absolute path of the primary directory, or an empty 100 // string if there's no suitable directory present. This is the path 101 // that should be used for writing files. 102 func (d Dir) Path() string { 103 if d.env == "" { 104 panic("xdgdir.Dir.Path() on zero Dir") 105 } 106 p := d.path() 107 if p != "" && d.userOwned { 108 info, err := os.Stat(p) 109 if err != nil { 110 return "" 111 } 112 if !info.IsDir() || info.Mode().Perm() != 0700 { 113 return "" 114 } 115 st, ok := info.Sys().(*syscall.Stat_t) 116 if !ok || int(st.Uid) != geteuid() { 117 return "" 118 } 119 } 120 return p 121 } 122 123 func (d Dir) path() string { 124 if e := getenv(d.env); isValidPath(e) { 125 return e 126 } 127 if d.fallback == "" { 128 return "" 129 } 130 home := findHome() 131 if home == "" { 132 return "" 133 } 134 p := filepath.Join(home, d.fallback) 135 if !isValidPath(p) { 136 return "" 137 } 138 return p 139 } 140 141 // SearchPaths returns the list of paths (in descending order of 142 // preference) to search for files. 143 func (d Dir) SearchPaths() []string { 144 if d.env == "" { 145 panic("xdgdir.Dir.SearchPaths() on zero Dir") 146 } 147 var paths []string 148 if p := d.Path(); p != "" { 149 paths = append(paths, p) 150 } 151 if d.dirsEnv == "" { 152 return paths 153 } 154 e := getenv(d.dirsEnv) 155 if e == "" { 156 paths = append(paths, d.dirsFallback...) 157 return paths 158 } 159 epaths := filepath.SplitList(e) 160 n := 0 161 for _, p := range epaths { 162 if isValidPath(p) { 163 epaths[n] = p 164 n++ 165 } 166 } 167 paths = append(paths, epaths[:n]...) 168 return paths 169 } 170 171 // Open opens the named file inside the directory for reading. If the 172 // directory has multiple search paths, each path is checked in order 173 // for the file and the first one found is opened. 174 func (d Dir) Open(name string) (*os.File, error) { 175 if d.env == "" { 176 return nil, errors.New("xdgdir: Open on zero Dir") 177 } 178 paths := d.SearchPaths() 179 if len(paths) == 0 { 180 return nil, fmt.Errorf("xdgdir: open %s: %s is invalid or not set", name, d.env) 181 } 182 var firstErr error 183 for _, p := range paths { 184 f, err := os.Open(filepath.Join(p, name)) 185 if err == nil { 186 return f, nil 187 } else if !os.IsNotExist(err) { 188 firstErr = err 189 } 190 } 191 if firstErr != nil { 192 return nil, firstErr 193 } 194 return nil, &os.PathError{ 195 Op: "Open", 196 Path: filepath.Join("$"+d.env, name), 197 Err: os.ErrNotExist, 198 } 199 } 200 201 // Create creates the named file inside the directory mode 0666 (before 202 // umask), truncating it if it already exists. Parent directories of 203 // the file will be created with mode 0700. 204 func (d Dir) Create(name string) (*os.File, error) { 205 if d.env == "" { 206 return nil, errors.New("xdgdir: Create on zero Dir") 207 } 208 p := d.Path() 209 if p == "" { 210 return nil, fmt.Errorf("xdgdir: create %s: %s is invalid or not set", name, d.env) 211 } 212 fp := filepath.Join(p, name) 213 if err := os.MkdirAll(filepath.Dir(fp), 0700); err != nil { 214 return nil, err 215 } 216 return os.Create(fp) 217 } 218 219 func isValidPath(path string) bool { 220 return path != "" && filepath.IsAbs(path) 221 } 222 223 // findHome returns the user's home directory or the empty string if it 224 // can't be found. It can be faked for testing. 225 var findHome = func() string { 226 if h := getenv("HOME"); h != "" { 227 return h 228 } 229 u, err := user.Current() 230 if err != nil { 231 return "" 232 } 233 return u.HomeDir 234 } 235 236 // getenv retrieves an environment variable. It can be faked for testing. 237 var getenv = os.Getenv 238 239 // geteuid retrieves the effective user ID of the process. It can be faked for testing. 240 var geteuid = os.Geteuid