1"""Unix."""
2
3from __future__ import annotations
4
5import os
6import re
7import stat
8import sys
9import warnings
10from contextlib import suppress
11from pathlib import Path
12from tempfile import gettempdir
13from typing import Final, NoReturn
14
15from ._xdg import XDGMixin, _expand_user, _xdg_dir
16from .api import PlatformDirsABC, RuntimeDirWarning
17
18if sys.platform == "win32":
19
20 def getuid() -> NoReturn:
21 msg = "should only be used on Unix"
22 raise RuntimeError(msg)
23
24else:
25 from os import getuid
26
27
28class _UnixDefaults(PlatformDirsABC): # ruff:ignore[too-many-public-methods]
29 """Default directories for Unix/Linux without XDG environment variable overrides.
30
31 The XDG env var handling is in :class:`~platformdirs._xdg.XDGMixin`.
32
33 """
34
35 @property
36 def _use_site(self) -> bool:
37 return self.use_site_for_root and getuid() == 0
38
39 @property
40 def user_data_dir(self) -> str:
41 """Data directory tied to the user, e.g. ``~/.local/share/$appname/$version`` or ``$XDG_DATA_HOME/$appname/$version``."""
42 return self._append_app_name_and_version(_expand_user("~/.local/share"), private=True)
43
44 @property
45 def _site_data_dirs(self) -> list[str]:
46 return [self._join_app_name_and_version("/usr/local/share"), self._join_app_name_and_version("/usr/share")]
47
48 @property
49 def user_config_dir(self) -> str:
50 """Config directory tied to the user, e.g. ``~/.config/$appname/$version`` or ``$XDG_CONFIG_HOME/$appname/$version``."""
51 return self._append_app_name_and_version(_expand_user("~/.config"), private=True)
52
53 @property
54 def _site_config_dirs(self) -> list[str]:
55 return [self._join_app_name_and_version("/etc/xdg")]
56
57 @property
58 def user_cache_dir(self) -> str:
59 """Cache directory tied to the user, e.g. ``~/.cache/$appname/$version`` or ``$XDG_CACHE_HOME/$appname/$version``."""
60 return self._append_app_name_and_version(_expand_user("~/.cache"), private=True)
61
62 @property
63 def site_cache_dir(self) -> str:
64 """Cache directory shared by users, e.g. ``/var/cache/$appname/$version``."""
65 return self._append_app_name_and_version("/var/cache", private=False)
66
67 @property
68 def user_state_dir(self) -> str:
69 """State directory tied to the user, e.g. ``~/.local/state/$appname/$version`` or ``$XDG_STATE_HOME/$appname/$version``."""
70 return self._append_app_name_and_version(_expand_user("~/.local/state"), private=True)
71
72 @property
73 def site_state_dir(self) -> str:
74 """State directory shared by users, e.g. ``/var/lib/$appname/$version``."""
75 return self._append_app_name_and_version("/var/lib", private=False)
76
77 @property
78 def user_log_dir(self) -> str:
79 """Log directory tied to the user, same as `user_state_dir` if not opinionated else ``log`` in it."""
80 path = self.user_state_dir
81 if self.opinion:
82 path = os.path.join(path, "log") # ruff:ignore[os-path-join]
83 self._optionally_create_directory(path, private=True)
84 return path
85
86 @property
87 def site_log_dir(self) -> str:
88 """Log directory shared by users, e.g. ``/var/log/$appname/$version``.
89
90 Unlike `user_log_dir`, ``opinion`` has no effect since ``/var/log`` is inherently a log directory.
91
92 """
93 return self._append_app_name_and_version("/var/log", private=False)
94
95 @property
96 def user_documents_dir(self) -> str:
97 """Documents directory tied to the user, e.g. ``~/Documents``."""
98 return self._user_media_dir("XDG_DOCUMENTS_DIR", "~/Documents")
99
100 @property
101 def user_downloads_dir(self) -> str:
102 """Downloads directory tied to the user, e.g. ``~/Downloads``."""
103 return self._user_media_dir("XDG_DOWNLOAD_DIR", "~/Downloads")
104
105 @property
106 def user_pictures_dir(self) -> str:
107 """Pictures directory tied to the user, e.g. ``~/Pictures``."""
108 return self._user_media_dir("XDG_PICTURES_DIR", "~/Pictures")
109
110 @property
111 def user_videos_dir(self) -> str:
112 """Videos directory tied to the user, e.g. ``~/Videos``."""
113 return self._user_media_dir("XDG_VIDEOS_DIR", "~/Videos")
114
115 @property
116 def user_music_dir(self) -> str:
117 """Music directory tied to the user, e.g. ``~/Music``."""
118 return self._user_media_dir("XDG_MUSIC_DIR", "~/Music")
119
120 @property
121 def user_desktop_dir(self) -> str:
122 """Desktop directory tied to the user, e.g. ``~/Desktop``."""
123 return self._user_media_dir("XDG_DESKTOP_DIR", "~/Desktop")
124
125 @property
126 def user_projects_dir(self) -> str:
127 """Projects directory tied to the user, e.g. ``~/Projects``."""
128 return self._user_media_dir("XDG_PROJECTS_DIR", "~/Projects")
129
130 @property
131 def user_publicshare_dir(self) -> str:
132 """Public share directory tied to the user, e.g. ``~/Public``."""
133 return self._user_media_dir("XDG_PUBLICSHARE_DIR", "~/Public")
134
135 @property
136 def user_templates_dir(self) -> str:
137 """Templates directory tied to the user, e.g. ``~/Templates``."""
138 return self._user_media_dir("XDG_TEMPLATES_DIR", "~/Templates")
139
140 def _user_media_dir(self, key: str, default: str) -> str:
141 if path := _get_user_dirs_folder(key):
142 self._optionally_create_media_directory(path)
143 return path
144 return _expand_user(default)
145
146 @property
147 def user_fonts_dir(self) -> str:
148 """Fonts directory tied to the user, e.g. ``~/.local/share/fonts``."""
149 return f"{_expand_user('~/.local/share')}/fonts"
150
151 @property
152 def user_preference_dir(self) -> str:
153 """Preference directory tied to the user, same as ``user_config_dir``."""
154 return self.user_config_dir
155
156 @property
157 def user_bin_dir(self) -> str:
158 """Bin directory tied to the user, e.g. ``~/.local/bin``."""
159 return _expand_user("~/.local/bin")
160
161 @property
162 def site_bin_dir(self) -> str:
163 """Bin directory shared by users, e.g. ``/usr/local/bin``."""
164 return "/usr/local/bin"
165
166 @property
167 def user_applications_dir(self) -> str:
168 """Applications directory tied to the user, e.g. ``~/.local/share/applications``."""
169 return f"{_expand_user('~/.local/share')}{os.sep}applications"
170
171 @property
172 def _site_applications_dirs(self) -> list[str]:
173 return [os.path.join(p, "applications") for p in ["/usr/local/share", "/usr/share"]] # ruff:ignore[os-path-join]
174
175 def _default_runtime_dir(self) -> str:
176 if sys.platform.startswith("openbsd"):
177 path = f"/tmp/run/user/{getuid()}" # ruff:ignore[hardcoded-temp-file]
178 elif sys.platform.startswith(("freebsd", "netbsd")):
179 path = f"/var/run/user/{getuid()}"
180 else:
181 path = f"/run/user/{getuid()}"
182 return path if os.path.lexists(path) and not _runtime_dir_problem(path) else self._temp_runtime_dir()
183
184 def _temp_runtime_dir(self) -> str:
185 # Another user can pre-create this predictable name, and XDG requires an owned runtime dir with mode 0700.
186 path = f"{gettempdir()}/runtime-{getuid()}"
187 with suppress(FileExistsError):
188 self._optionally_create_directory(path, private=True)
189 if problem := _runtime_dir_problem(path, check_mode=False):
190 msg = f"runtime directory {problem}; set XDG_RUNTIME_DIR instead"
191 raise PermissionError(msg)
192 # Earlier releases created this directory with the default 0755.
193 if self.ensure_exists:
194 Path(path).chmod(_RUNTIME_DIR_MODE)
195 return path
196
197 @property
198 def site_runtime_dir(self) -> str:
199 """Runtime directory shared by users, e.g. ``/run/$appname/$version`` or ``$XDG_RUNTIME_DIR/$appname/$version``.
200
201 Note that this behaves almost exactly like `user_runtime_dir` if ``$XDG_RUNTIME_DIR`` is set, but will fall back
202 to paths associated to the root user instead of a regular logged-in user if it's not set.
203
204 If you wish to ensure that a logged-in root user path is returned e.g. ``/run/user/0``, use `user_runtime_dir`
205 instead.
206
207 For FreeBSD/OpenBSD/NetBSD, it would return ``/var/run/$appname/$version`` if ``$XDG_RUNTIME_DIR`` is not set.
208
209 """
210 if sys.platform.startswith(("freebsd", "openbsd", "netbsd")):
211 path = "/var/run"
212 else:
213 path = "/run"
214 return self._append_app_name_and_version(path, private=False)
215
216
217class Unix(XDGMixin, _UnixDefaults):
218 """On Unix/Linux, we follow the `XDG Basedir Spec <https://specifications.freedesktop.org/basedir/latest/>`_.
219
220 The spec allows overriding directories with environment variables. The examples shown are the default values,
221 alongside the name of the environment variable that overrides them. Makes use of the `appname
222 <platformdirs.api.PlatformDirsABC.appname>`, `version <platformdirs.api.PlatformDirsABC.version>`, `multipath
223 <platformdirs.api.PlatformDirsABC.multipath>`, `opinion <platformdirs.api.PlatformDirsABC.opinion>`, `ensure_exists
224 <platformdirs.api.PlatformDirsABC.ensure_exists>`.
225
226 """
227
228 @property
229 def user_data_dir(self) -> str:
230 """Data directory tied to the user, or site equivalent when root with ``use_site_for_root``."""
231 return self.site_data_dir if self._use_site else super().user_data_dir
232
233 @property
234 def user_config_dir(self) -> str:
235 """Config directory tied to the user, or site equivalent when root with ``use_site_for_root``."""
236 return self.site_config_dir if self._use_site else super().user_config_dir
237
238 @property
239 def user_cache_dir(self) -> str:
240 """Cache directory tied to the user, or site equivalent when root with ``use_site_for_root``."""
241 return self.site_cache_dir if self._use_site else super().user_cache_dir
242
243 @property
244 def user_state_dir(self) -> str:
245 """State directory tied to the user, or site equivalent when root with ``use_site_for_root``."""
246 return self.site_state_dir if self._use_site else super().user_state_dir
247
248 @property
249 def user_log_dir(self) -> str:
250 """Log directory tied to the user, or site equivalent when root with ``use_site_for_root``."""
251 return self.site_log_dir if self._use_site else super().user_log_dir
252
253 @property
254 def user_applications_dir(self) -> str:
255 """Applications directory tied to the user, or site equivalent when root with ``use_site_for_root``."""
256 return self.site_applications_dir if self._use_site else super().user_applications_dir
257
258 @property
259 def user_runtime_dir(self) -> str:
260 """Runtime directory tied to the user, e.g. ``$XDG_RUNTIME_DIR/$appname/$version``.
261
262 Accepts ``$XDG_RUNTIME_DIR`` only as a directory, not a symlink, that the user owns with mode ``0700``, as the
263 XDG spec requires, and creates a missing one that way under ``ensure_exists``. When the variable is unset or
264 fails that check, emits one :class:`~platformdirs.RuntimeDirWarning` per cause and falls back to the platform
265 default (``/tmp/run/user/<uid>`` on OpenBSD, ``/var/run/user/<uid>`` on FreeBSD/NetBSD, ``/run/user/<uid>``
266 elsewhere) if it passes the same check, else to ``runtime-<uid>`` in the temporary directory. Root with
267 ``use_site_for_root`` gets the site equivalent.
268
269 :raises PermissionError: if the temporary fallback is a symlink, not a directory, or owned by another user.
270
271 """
272 if self._use_site:
273 # XDGMixin.site_runtime_dir reads $XDG_RUNTIME_DIR, which belongs to the user who started the root process.
274 return super(XDGMixin, self).site_runtime_dir
275 if not (path := _xdg_dir("XDG_RUNTIME_DIR")):
276 reason = "XDG_RUNTIME_DIR is not set"
277 else:
278 with suppress(OSError):
279 self._optionally_create_directory(path, private=True)
280 if not (problem := _runtime_dir_problem(path)):
281 return self._append_app_name_and_version(path, private=True)
282 reason = f"XDG_RUNTIME_DIR {problem}"
283 _warn_once(f"{reason}, falling back to {(fallback := self._default_runtime_dir())}")
284 return self._append_app_name_and_version(fallback, private=True)
285
286 @property
287 def user_bin_dir(self) -> str:
288 """Bin directory tied to the user, or site equivalent when root with ``use_site_for_root``."""
289 return self.site_bin_dir if self._use_site else super().user_bin_dir
290
291 @property
292 def user_data_path(self) -> Path:
293 """Data path tied to the user, or the first site entry when root with ``use_site_for_root``."""
294 return self.site_data_path if self._use_site else super().user_data_path
295
296 @property
297 def user_config_path(self) -> Path:
298 """Config path tied to the user, or the first site entry when root with ``use_site_for_root``."""
299 return self.site_config_path if self._use_site else super().user_config_path
300
301 @property
302 def user_preference_path(self) -> Path:
303 """Preference path tied to the user, or the first site config entry when root with ``use_site_for_root``."""
304 return self.site_config_path if self._use_site else super().user_preference_path
305
306 @property
307 def user_applications_path(self) -> Path:
308 """Applications path tied to the user, or the first site entry when root with ``use_site_for_root``."""
309 return self.site_applications_path if self._use_site else super().user_applications_path
310
311
312_USER_DIRS_LINE: Final = re.compile(
313 r"""
314 [ \t]*(?P<key>\w+)[ \t]*=[ \t]* # KEY=, blanks allowed around the key and after the =
315 (?:
316 "(?P<quoted>(?:[^"\\]|\\.)*)" # a double-quoted value with backslash escapes, text after it ignored
317 |(?P<bare>[^"\s].*?) # or an unquoted value, up to
318 (?:[ \t]+\#.*|[ \t]*)$ # a comment, which sh starts at a word-initial #, or the line end
319 )
320 """,
321 re.VERBOSE,
322)
323
324
325def _get_user_dirs_folder(key: str) -> str | None:
326 """Return directory from user-dirs.dirs config file.
327
328 ``xdg-user-dirs-update`` writes shell assignments, so match each line like ``xdg-user-dir`` does and keep the last
329 valid assignment to ``key``.
330
331 See https://freedesktop.org/wiki/Software/xdg-user-dirs/.
332
333 """
334 config_home = _xdg_dir("XDG_CONFIG_HOME") or _expand_user("~/.config")
335 user_dirs_config_path = Path(config_home) / "user-dirs.dirs"
336 if not user_dirs_config_path.exists():
337 return None
338 folder = None
339 # the file holds raw path bytes, and surrogateescape round-trips undecodable ones like os.fsdecode
340 with user_dirs_config_path.open(encoding=sys.getfilesystemencoding(), errors="surrogateescape") as stream:
341 for line in stream:
342 if (entry := _USER_DIRS_LINE.match(line)) and entry["key"] == key:
343 folder = _resolve_user_dirs_value(entry) or folder
344 return folder
345
346
347def _resolve_user_dirs_value(entry: re.Match[str]) -> str | None:
348 value = entry["bare"] if entry["quoted"] is None else entry["quoted"]
349 if value == "$HOME" or value.startswith("$HOME/"):
350 prefix, value = _expand_user("~"), value.removeprefix("$HOME")
351 elif value.startswith("/"):
352 prefix = ""
353 else:
354 return None
355 if entry["quoted"] is not None:
356 # xdg-user-dirs-update backslash-escapes $, `, " and \ inside the quotes.
357 value = re.sub(r"\\(.)", r"\1", value)
358 return prefix + value
359
360
361def _runtime_dir_problem(path: str, *, check_mode: bool = True) -> str | None:
362 """Return why ``path`` fails the checks of Qt's ``checkXdgRuntimeDir``, or ``None`` if it passes or is missing."""
363 try:
364 info = os.lstat(path)
365 except FileNotFoundError:
366 return None
367 except OSError as error:
368 return f"{path} cannot be checked: {error.strerror}"
369 mode = info.st_mode & 0o777
370 checks = (
371 (stat.S_ISLNK(info.st_mode), "is a symlink"),
372 (not stat.S_ISDIR(info.st_mode), "is not a directory"),
373 (info.st_uid != (uid := getuid()), f"is owned by uid {info.st_uid}, not {uid}"),
374 (check_mode and mode != _RUNTIME_DIR_MODE, f"has mode {mode:04o}, not {_RUNTIME_DIR_MODE:04o}"),
375 )
376 return next((f"{path} {problem}" for failed, problem in checks if failed), None)
377
378
379def _warn_once(message: str) -> None:
380 # Each read of user_runtime_dir repeats the check, so a warning per read would flood long-running programs.
381 if message not in _WARNED:
382 _WARNED.add(message)
383 warnings.warn(message, RuntimeDirWarning, stacklevel=3)
384
385
386_WARNED: Final[set[str]] = set()
387_RUNTIME_DIR_MODE: Final = 0o700
388
389
390__all__ = [
391 "Unix",
392]