]>
Commit | Line | Data |
---|---|---|
53e6db90 DC |
1 | """Unix.""" |
2 | from __future__ import annotations | |
3 | ||
4 | import os | |
5 | import sys | |
6 | from configparser import ConfigParser | |
7 | from pathlib import Path | |
8 | ||
9 | from .api import PlatformDirsABC | |
10 | ||
11 | if sys.platform == "win32": | |
12 | ||
13 | def getuid() -> int: | |
14 | msg = "should only be used on Unix" | |
15 | raise RuntimeError(msg) | |
16 | ||
17 | else: | |
18 | from os import getuid | |
19 | ||
20 | ||
21 | class Unix(PlatformDirsABC): | |
22 | """ | |
23 | On Unix/Linux, we follow the | |
24 | `XDG Basedir Spec <https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html>`_. The spec allows | |
25 | overriding directories with environment variables. The examples show are the default values, alongside the name of | |
26 | the environment variable that overrides them. Makes use of the | |
27 | `appname <platformdirs.api.PlatformDirsABC.appname>`, | |
28 | `version <platformdirs.api.PlatformDirsABC.version>`, | |
29 | `multipath <platformdirs.api.PlatformDirsABC.multipath>`, | |
30 | `opinion <platformdirs.api.PlatformDirsABC.opinion>`, | |
31 | `ensure_exists <platformdirs.api.PlatformDirsABC.ensure_exists>`. | |
32 | """ | |
33 | ||
34 | @property | |
35 | def user_data_dir(self) -> str: | |
36 | """ | |
37 | :return: data directory tied to the user, e.g. ``~/.local/share/$appname/$version`` or | |
38 | ``$XDG_DATA_HOME/$appname/$version`` | |
39 | """ | |
40 | path = os.environ.get("XDG_DATA_HOME", "") | |
41 | if not path.strip(): | |
42 | path = os.path.expanduser("~/.local/share") # noqa: PTH111 | |
43 | return self._append_app_name_and_version(path) | |
44 | ||
45 | @property | |
46 | def site_data_dir(self) -> str: | |
47 | """ | |
48 | :return: data directories shared by users (if `multipath <platformdirs.api.PlatformDirsABC.multipath>` is | |
49 | enabled and ``XDG_DATA_DIR`` is set and a multi path the response is also a multi path separated by the OS | |
50 | path separator), e.g. ``/usr/local/share/$appname/$version`` or ``/usr/share/$appname/$version`` | |
51 | """ | |
52 | # XDG default for $XDG_DATA_DIRS; only first, if multipath is False | |
53 | path = os.environ.get("XDG_DATA_DIRS", "") | |
54 | if not path.strip(): | |
55 | path = f"/usr/local/share{os.pathsep}/usr/share" | |
56 | return self._with_multi_path(path) | |
57 | ||
58 | def _with_multi_path(self, path: str) -> str: | |
59 | path_list = path.split(os.pathsep) | |
60 | if not self.multipath: | |
61 | path_list = path_list[0:1] | |
62 | path_list = [self._append_app_name_and_version(os.path.expanduser(p)) for p in path_list] # noqa: PTH111 | |
63 | return os.pathsep.join(path_list) | |
64 | ||
65 | @property | |
66 | def user_config_dir(self) -> str: | |
67 | """ | |
68 | :return: config directory tied to the user, e.g. ``~/.config/$appname/$version`` or | |
69 | ``$XDG_CONFIG_HOME/$appname/$version`` | |
70 | """ | |
71 | path = os.environ.get("XDG_CONFIG_HOME", "") | |
72 | if not path.strip(): | |
73 | path = os.path.expanduser("~/.config") # noqa: PTH111 | |
74 | return self._append_app_name_and_version(path) | |
75 | ||
76 | @property | |
77 | def site_config_dir(self) -> str: | |
78 | """ | |
79 | :return: config directories shared by users (if `multipath <platformdirs.api.PlatformDirsABC.multipath>` | |
80 | is enabled and ``XDG_DATA_DIR`` is set and a multi path the response is also a multi path separated by the OS | |
81 | path separator), e.g. ``/etc/xdg/$appname/$version`` | |
82 | """ | |
83 | # XDG default for $XDG_CONFIG_DIRS only first, if multipath is False | |
84 | path = os.environ.get("XDG_CONFIG_DIRS", "") | |
85 | if not path.strip(): | |
86 | path = "/etc/xdg" | |
87 | return self._with_multi_path(path) | |
88 | ||
89 | @property | |
90 | def user_cache_dir(self) -> str: | |
91 | """ | |
92 | :return: cache directory tied to the user, e.g. ``~/.cache/$appname/$version`` or | |
93 | ``~/$XDG_CACHE_HOME/$appname/$version`` | |
94 | """ | |
95 | path = os.environ.get("XDG_CACHE_HOME", "") | |
96 | if not path.strip(): | |
97 | path = os.path.expanduser("~/.cache") # noqa: PTH111 | |
98 | return self._append_app_name_and_version(path) | |
99 | ||
100 | @property | |
101 | def site_cache_dir(self) -> str: | |
102 | """:return: cache directory shared by users, e.g. ``/var/tmp/$appname/$version``""" | |
103 | return self._append_app_name_and_version("/var/tmp") # noqa: S108 | |
104 | ||
105 | @property | |
106 | def user_state_dir(self) -> str: | |
107 | """ | |
108 | :return: state directory tied to the user, e.g. ``~/.local/state/$appname/$version`` or | |
109 | ``$XDG_STATE_HOME/$appname/$version`` | |
110 | """ | |
111 | path = os.environ.get("XDG_STATE_HOME", "") | |
112 | if not path.strip(): | |
113 | path = os.path.expanduser("~/.local/state") # noqa: PTH111 | |
114 | return self._append_app_name_and_version(path) | |
115 | ||
116 | @property | |
117 | def user_log_dir(self) -> str: | |
118 | """:return: log directory tied to the user, same as `user_state_dir` if not opinionated else ``log`` in it""" | |
119 | path = self.user_state_dir | |
120 | if self.opinion: | |
121 | path = os.path.join(path, "log") # noqa: PTH118 | |
122 | self._optionally_create_directory(path) | |
123 | return path | |
124 | ||
125 | @property | |
126 | def user_documents_dir(self) -> str: | |
127 | """:return: documents directory tied to the user, e.g. ``~/Documents``""" | |
128 | return _get_user_media_dir("XDG_DOCUMENTS_DIR", "~/Documents") | |
129 | ||
130 | @property | |
131 | def user_downloads_dir(self) -> str: | |
132 | """:return: downloads directory tied to the user, e.g. ``~/Downloads``""" | |
133 | return _get_user_media_dir("XDG_DOWNLOAD_DIR", "~/Downloads") | |
134 | ||
135 | @property | |
136 | def user_pictures_dir(self) -> str: | |
137 | """:return: pictures directory tied to the user, e.g. ``~/Pictures``""" | |
138 | return _get_user_media_dir("XDG_PICTURES_DIR", "~/Pictures") | |
139 | ||
140 | @property | |
141 | def user_videos_dir(self) -> str: | |
142 | """:return: videos directory tied to the user, e.g. ``~/Videos``""" | |
143 | return _get_user_media_dir("XDG_VIDEOS_DIR", "~/Videos") | |
144 | ||
145 | @property | |
146 | def user_music_dir(self) -> str: | |
147 | """:return: music directory tied to the user, e.g. ``~/Music``""" | |
148 | return _get_user_media_dir("XDG_MUSIC_DIR", "~/Music") | |
149 | ||
150 | @property | |
151 | def user_desktop_dir(self) -> str: | |
152 | """:return: desktop directory tied to the user, e.g. ``~/Desktop``""" | |
153 | return _get_user_media_dir("XDG_DESKTOP_DIR", "~/Desktop") | |
154 | ||
155 | @property | |
156 | def user_runtime_dir(self) -> str: | |
157 | """ | |
158 | :return: runtime directory tied to the user, e.g. ``/run/user/$(id -u)/$appname/$version`` or | |
159 | ``$XDG_RUNTIME_DIR/$appname/$version``. | |
160 | ||
161 | For FreeBSD/OpenBSD/NetBSD, it would return ``/var/run/user/$(id -u)/$appname/$version`` if | |
162 | exists, otherwise ``/tmp/runtime-$(id -u)/$appname/$version``, if``$XDG_RUNTIME_DIR`` | |
163 | is not set. | |
164 | """ | |
165 | path = os.environ.get("XDG_RUNTIME_DIR", "") | |
166 | if not path.strip(): | |
167 | if sys.platform.startswith(("freebsd", "openbsd", "netbsd")): | |
168 | path = f"/var/run/user/{getuid()}" | |
169 | if not Path(path).exists(): | |
170 | path = f"/tmp/runtime-{getuid()}" # noqa: S108 | |
171 | else: | |
172 | path = f"/run/user/{getuid()}" | |
173 | return self._append_app_name_and_version(path) | |
174 | ||
175 | @property | |
176 | def site_runtime_dir(self) -> str: | |
177 | """ | |
178 | :return: runtime directory shared by users, e.g. ``/run/$appname/$version`` or \ | |
179 | ``$XDG_RUNTIME_DIR/$appname/$version``. | |
180 | ||
181 | Note that this behaves almost exactly like `user_runtime_dir` if ``$XDG_RUNTIME_DIR`` is set, but will | |
182 | fall back to paths associated to the root user instead of a regular logged-in user if it's not set. | |
183 | ||
184 | If you wish to ensure that a logged-in root user path is returned e.g. ``/run/user/0``, use `user_runtime_dir` | |
185 | instead. | |
186 | ||
187 | For FreeBSD/OpenBSD/NetBSD, it would return ``/var/run/$appname/$version`` if ``$XDG_RUNTIME_DIR`` is not set. | |
188 | """ | |
189 | path = os.environ.get("XDG_RUNTIME_DIR", "") | |
190 | if not path.strip(): | |
191 | if sys.platform.startswith(("freebsd", "openbsd", "netbsd")): | |
192 | path = "/var/run" | |
193 | else: | |
194 | path = "/run" | |
195 | return self._append_app_name_and_version(path) | |
196 | ||
197 | @property | |
198 | def site_data_path(self) -> Path: | |
199 | """:return: data path shared by users. Only return first item, even if ``multipath`` is set to ``True``""" | |
200 | return self._first_item_as_path_if_multipath(self.site_data_dir) | |
201 | ||
202 | @property | |
203 | def site_config_path(self) -> Path: | |
204 | """:return: config path shared by the users. Only return first item, even if ``multipath`` is set to ``True``""" | |
205 | return self._first_item_as_path_if_multipath(self.site_config_dir) | |
206 | ||
207 | @property | |
208 | def site_cache_path(self) -> Path: | |
209 | """:return: cache path shared by users. Only return first item, even if ``multipath`` is set to ``True``""" | |
210 | return self._first_item_as_path_if_multipath(self.site_cache_dir) | |
211 | ||
212 | def _first_item_as_path_if_multipath(self, directory: str) -> Path: | |
213 | if self.multipath: | |
214 | # If multipath is True, the first path is returned. | |
215 | directory = directory.split(os.pathsep)[0] | |
216 | return Path(directory) | |
217 | ||
218 | ||
219 | def _get_user_media_dir(env_var: str, fallback_tilde_path: str) -> str: | |
220 | media_dir = _get_user_dirs_folder(env_var) | |
221 | if media_dir is None: | |
222 | media_dir = os.environ.get(env_var, "").strip() | |
223 | if not media_dir: | |
224 | media_dir = os.path.expanduser(fallback_tilde_path) # noqa: PTH111 | |
225 | ||
226 | return media_dir | |
227 | ||
228 | ||
229 | def _get_user_dirs_folder(key: str) -> str | None: | |
230 | """Return directory from user-dirs.dirs config file. See https://freedesktop.org/wiki/Software/xdg-user-dirs/.""" | |
231 | user_dirs_config_path = Path(Unix().user_config_dir) / "user-dirs.dirs" | |
232 | if user_dirs_config_path.exists(): | |
233 | parser = ConfigParser() | |
234 | ||
235 | with user_dirs_config_path.open() as stream: | |
236 | # Add fake section header, so ConfigParser doesn't complain | |
237 | parser.read_string(f"[top]\n{stream.read()}") | |
238 | ||
239 | if key not in parser["top"]: | |
240 | return None | |
241 | ||
242 | path = parser["top"][key].strip('"') | |
243 | # Handle relative home paths | |
244 | return path.replace("$HOME", os.path.expanduser("~")) # noqa: PTH111 | |
245 | ||
246 | return None | |
247 | ||
248 | ||
249 | __all__ = [ | |
250 | "Unix", | |
251 | ] |