1/* 2 * xdg.ts 3 * 4 * Copyright (C) 2021 by RStudio, PBC 5 * 6 * Unless you have received this program directly from RStudio pursuant 7 * to the terms of a commercial license agreement with RStudio, then 8 * this program is licensed to you under the terms of version 3 of the 9 * GNU Affero General Public License. This program is distributed WITHOUT 10 * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT, 11 * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the 12 * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details. 13 * 14 */ 15 16/* 17 * These routines return system and user paths for RStudio configuration and data, roughly in 18 * accordance with the FreeDesktop XDG Base Directory Specification. 19 * 20 * https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html 21 * 22 * All of these can be configured with environment variables as described below. 23 * 24 * The values of the environment variables can include the following special variables: 25 * 26 * $USER The user's name 27 * $HOME The user's home directory 28 * ~ The user's home directory 29 * 30 * These values will be resolved against the current user by default. If you wish to 31 * resolve them against a different user, supply their name and home directory using 32 * the boost::optional arguments. 33 */ 34 35import os from 'os'; 36 37import { logger } from './logger'; 38import { Environment, expandEnvVars, getenv } from './environment'; 39import { username, userHomePath } from './user'; 40import { FilePath } from './file-path'; 41 42export enum WinFolderID { 43 FOLDERID_RoamingAppData, 44 FOLDERID_LocalAppData, 45 FOLDERID_ProgramData 46} 47 48/** 49 * Simplified implementation of Windows `SHGetKnownFolderPath` API using environment 50 * variables. 51 */ 52export function SHGetKnownFolderPath(folderId: WinFolderID): string { 53 let envVar = ''; 54 switch (folderId) { 55 case WinFolderID.FOLDERID_RoamingAppData: 56 envVar = 'APPDATA'; 57 break; 58 case WinFolderID.FOLDERID_LocalAppData: 59 envVar = 'LOCALAPPDATA'; 60 break; 61 case WinFolderID.FOLDERID_ProgramData: 62 envVar = 'ProgramData'; 63 break; 64 } 65 return getenv(envVar); 66} 67 68// Store the hostname so we don't have to look it up multiple times 69let hostname = ''; 70 71/** 72 * Returns the hostname from the operating system 73 */ 74function getHostname(): string { 75 if (!hostname) 76 hostname = os.hostname(); 77 return hostname; 78} 79 80/** 81 * Resolves an XDG directory based on the user and environment. 82 * 83 * `rstudioEnvVer` The RStudio-specific environment variable specifying 84 * the directory (given precedence) 85 * 86 * `xdgEnvVar` The XDG standard environment variable 87 * 88 * `defaultDir` Fallback default directory if neither environment variable 89 * is present 90 * 91 * `windowsFolderId` The ID of the Windows folder to resolve against 92 * 93 * `user` Optionally, the user to return a directory for; if omitted the 94 * current user is used 95 * 96 * `homeDir` Optionally, the home directory to resolve against; if omitted 97 * the current user's home directory is used 98 */ 99function resolveXdgDir( 100 rstudioEnvVar: string, 101 xdgEnvVar: string, 102 windowsFolderId: WinFolderID, 103 defaultDir: string, 104 user?: string, 105 homeDir?: FilePath 106): FilePath { 107 let xdgHome = new FilePath(); 108 let finalPath = true; 109 110 // Look for the RStudio-specific environment variable 111 let env = getenv(rstudioEnvVar); 112 if (!env) { 113 // The RStudio environment variable specifies the final path; if it isn't 114 // set we will need to append "rstudio" to the path later. 115 finalPath = false; 116 env = getenv(xdgEnvVar); 117 } 118 119 if (!env) { 120 // No root specified for xdg home; we will need to generate one. 121 if (process.platform === 'win32') { 122 // On Windows, the default path is in Application Data/Roaming. 123 const path = SHGetKnownFolderPath(windowsFolderId); 124 if (path) { 125 xdgHome = new FilePath(path); 126 } else { 127 logger().logError(new Error(`Unable to retrieve app settings path (${windowsFolderId}).`)); 128 } 129 } 130 if (xdgHome.isEmpty()) { 131 // Use the default subdir for POSIX. We also use this folder as a fallback on Windows 132 //if we couldn't read the app settings path. 133 xdgHome = new FilePath(defaultDir); 134 } 135 } else { 136 // We have a manually specified xdg directory from an environment variable. 137 xdgHome = new FilePath(env); 138 } 139 140 // expand HOME, USER, and HOSTNAME if given 141 const environment: Environment = { 142 'HOME': homeDir ? homeDir.getAbsolutePath() : userHomePath().getAbsolutePath(), 143 'USER': user ? user : username() 144 }; 145 146 // check for manually specified hostname in environment variable 147 let hostname = getenv('HOSTNAME'); 148 149 // when omitted, look up the hostname using a system call 150 if (!hostname) 151 hostname = getHostname(); 152 153 environment.HOSTNAME = hostname; 154 155 const expanded = expandEnvVars(environment, xdgHome.getAbsolutePath()); 156 157 // resolve aliases in the path 158 xdgHome = FilePath.resolveAliasedPathSync(expanded, homeDir ? homeDir : userHomePath()); 159 160 // If this is the final path, we can return it as-is 161 if (finalPath) 162 return xdgHome; 163 164 // Otherwise, it's a root folder in which we need to create our own subfolder 165 const folderName = process.platform === 'win32' ? 'RStudio' : 'rstudio'; 166 return xdgHome.completePath(folderName); 167} 168 169export class Xdg { 170 171 /** 172 * Returns the RStudio XDG user config directory. 173 * 174 * On Unix-alikes, this is `~/.config/rstudio`, or `XDG_CONFIG_HOME`. 175 * On Windows, this is `FOLDERID_RoamingAppData` (typically `AppData/Roaming`). 176 */ 177 static userConfigDir(user?: string, homeDir?: FilePath): FilePath { 178 return resolveXdgDir( 179 'RSTUDIO_CONFIG_HOME', 180 'XDG_CONFIG_HOME', 181 WinFolderID.FOLDERID_RoamingAppData, 182 '~/.config', 183 user, 184 homeDir 185 ); 186 } 187 188 /** 189 * Returns the RStudio XDG user data directory. 190 * 191 * On Unix-alikes, this is `~/.local/share/rstudio`, or `XDG_DATA_HOME`. 192 * On Windows, this is `FOLDERID_LocalAppData` (typically `AppData/Local`). 193 */ 194 static userDataDir(user?: string, homeDir?: FilePath): FilePath { 195 return resolveXdgDir( 196 'RSTUDIO_DATA_HOME', 197 'XDG_DATA_HOME', 198 WinFolderID.FOLDERID_LocalAppData, 199 '~/.local/share', 200 user, 201 homeDir 202 ); 203 } 204 205 /** 206 * This function verifies that the `userConfigDir()` and `userDataDir()` exist and are 207 * owned by the running user. 208 * 209 * It should be invoked once. Any issues with these directories will be emitted to the 210 * session log. 211 */ 212 static verifyUserDirs( 213 // eslint-disable-next-line @typescript-eslint/no-unused-vars 214 user?: string, 215 // eslint-disable-next-line @typescript-eslint/no-unused-vars 216 homeDir?: FilePath 217 ): void { 218 throw Error('Xdg.verifyUserDirs is NYI'); 219 } 220 221 222 /** 223 * Returns the RStudio XDG system config directory. 224 * 225 * On Unix-alikes, this is `/etc/rstudio`, `XDG_CONFIG_DIRS`. 226 * On Windows, this is `FOLDERID_ProgramData` (typically `C:/ProgramData`). 227 */ 228 static systemConfigDir(): FilePath { 229 if (process.platform !== 'win32') { 230 if (!getenv('RSTUDIO_CONFIG_DIR')) { 231 // On POSIX operating systems, it's possible to specify multiple config 232 // directories. We have to select one, so read the list and take the first 233 // one that contains an "rstudio" folder. 234 const env = getenv('XDG_CONFIG_DIRS'); 235 if (env.indexOf(':') >= 0) { 236 const dirs = env.split(':'); 237 for (const dir of dirs) { 238 const resolved = new FilePath(dir).completePath('rstudio'); 239 if (resolved.existsSync()) { 240 return resolved; 241 } 242 } 243 } 244 } 245 } 246 return resolveXdgDir( 247 'RSTUDIO_CONFIG_DIR', 248 'XDG_CONFIG_DIRS', 249 WinFolderID.FOLDERID_ProgramData, 250 '/etc', 251 undefined, // no specific user 252 undefined // no home folder resolution 253 ); 254 } 255 256 /** 257 * Convenience method for finding a configuration file. Checks all the 258 * directories in `XDG_CONFIG_DIRS` for the file. If it doesn't find it, 259 * the path where we expected to find it is returned instead. 260 */ 261 static systemConfigFile(filename: string): FilePath { 262 if (process.platform === 'win32') { 263 // Passthrough on Windows 264 return Xdg.systemConfigDir().completeChildPath(filename); 265 } 266 if (!getenv('RSTUDIO_CONFIG_DIR')) { 267 // On POSIX, check for a search path. 268 const env = getenv('XDG_CONFIG_DIRS'); 269 if (env.indexOf(':') >= 0) { 270 // This is a search path; check each element for the file. 271 const dirs = env.split(':'); 272 for (const dir of dirs) { 273 const resolved = new FilePath(dir).completePath('rstudio').completeChildPath(filename); 274 if (resolved.existsSync()) { 275 return resolved; 276 } 277 } 278 } 279 } 280 281 // We didn't find the file on the search path, so return the location where 282 // we expected to find it. 283 return Xdg.systemConfigDir().completeChildPath(filename); 284 } 285 286 /** 287 * Sets relevant XDG environment variables 288 */ 289 // eslint-disable-next-line @typescript-eslint/no-unused-vars 290 static forwardXdgEnvVars(pEnvironment: Environment): void { 291 throw Error('forwardXdgEnvVars is NYI'); 292 } 293}