1 # Copyright (C) The Arvados Authors. All rights reserved.
3 # SPDX-License-Identifier: Apache-2.0
4 """Base directories utility module
6 This module provides a set of classes useful to search and manipulate base
7 directory defined by systemd and the XDG specification. Most users will just
8 instantiate and use `BaseDirectories`.
19 from pathlib import Path, PurePath
27 logger = logging.getLogger('arvados')
29 @dataclasses.dataclass
30 class BaseDirectorySpec:
31 """Parse base directories
33 A BaseDirectorySpec defines all the environment variable keys and defaults
34 related to a set of base directories (cache, config, state, etc.). It
35 provides pure methods to parse environment settings into valid paths.
39 xdg_home_default: PurePath
40 xdg_dirs_key: Optional[str] = None
41 xdg_dirs_default: str = ''
44 def _abspath_from_env(env: Mapping[str, str], key: str) -> Optional[Path]:
47 except (KeyError, ValueError):
50 ok = path.is_absolute()
51 return path if ok else None
54 def _iter_abspaths(value: str) -> Iterator[Path]:
55 for path_s in value.split(':'):
57 if path.is_absolute():
60 def iter_systemd(self, env: Mapping[str, str]) -> Iterator[Path]:
61 return self._iter_abspaths(env.get(self.systemd_key, ''))
63 def iter_xdg(self, env: Mapping[str, str], subdir: PurePath) -> Iterator[Path]:
64 yield self.xdg_home(env, subdir)
65 if self.xdg_dirs_key is not None:
66 for path in self._iter_abspaths(env.get(self.xdg_dirs_key) or self.xdg_dirs_default):
69 def xdg_home(self, env: Mapping[str, str], subdir: PurePath) -> Path:
71 self._abspath_from_env(env, self.xdg_home_key)
72 or self.xdg_home_default_path(env)
75 def xdg_home_default_path(self, env: Mapping[str, str]) -> Path:
76 return (self._abspath_from_env(env, 'HOME') or Path.home()) / self.xdg_home_default
78 def xdg_home_is_customized(self, env: Mapping[str, str]) -> bool:
79 xdg_home = self._abspath_from_env(env, self.xdg_home_key)
80 return xdg_home is not None and xdg_home != self.xdg_home_default_path(env)
83 class BaseDirectorySpecs(enum.Enum):
84 """Base directory specifications
86 This enum provides easy access to the standard base directory settings.
88 CACHE = BaseDirectorySpec(
93 CONFIG = BaseDirectorySpec(
94 'CONFIGURATION_DIRECTORY',
100 STATE = BaseDirectorySpec(
103 PurePath('.local', 'state'),
107 class BaseDirectories:
108 """Resolve paths from a base directory spec
110 Given a BaseDirectorySpec, this class provides stateful methods to find
111 existing files and return the most-preferred directory for writing.
113 _STORE_MODE = stat.S_IFDIR | stat.S_IWUSR
117 spec: Union[BaseDirectorySpec, BaseDirectorySpecs, str],
118 env: Mapping[str, str]=os.environ,
119 xdg_subdir: Union[os.PathLike, str]='arvados',
121 if isinstance(spec, str):
122 spec = BaseDirectorySpecs[spec].value
123 elif isinstance(spec, BaseDirectorySpecs):
127 self._xdg_subdir = PurePath(xdg_subdir)
129 def search(self, name: str) -> Iterator[Path]:
131 for search_path in itertools.chain(
132 self._spec.iter_systemd(self._env),
133 self._spec.iter_xdg(self._env, self._xdg_subdir),
135 path = search_path / name
139 # The rest of this function is dedicated to warning the user if they
140 # have a custom XDG_*_HOME value that prevented the search from
141 # succeeding. This should be rare.
142 if any_found or not self._spec.xdg_home_is_customized(self._env):
144 default_home = self._spec.xdg_home_default_path(self._env)
145 default_path = Path(self._xdg_subdir / name)
146 if not (default_home / default_path).exists():
148 if self._spec.xdg_dirs_key is None:
149 suggest_key = self._spec.xdg_home_key
150 suggest_value = default_home
152 suggest_key = self._spec.xdg_dirs_key
153 cur_value = self._env.get(suggest_key, '')
154 value_sep = ':' if cur_value else ''
155 suggest_value = f'{cur_value}{value_sep}{default_home}'
158 %s was not found under your configured $%s (%s), \
159 but does exist at the default location (%s) - \
160 consider running this program with the environment setting %s=%s\
163 self._spec.xdg_home_key,
164 self._spec.xdg_home(self._env, ''),
167 shlex.quote(suggest_value),
172 subdir: Union[str, os.PathLike]=PurePath(),
175 for path in self._spec.iter_systemd(self._env):
177 mode = path.stat().st_mode
180 if (mode & self._STORE_MODE) == self._STORE_MODE:
183 path = self._spec.xdg_home(self._env, self._xdg_subdir)
185 path.mkdir(parents=True, exist_ok=True, mode=mode)