kiwi.path

  1# Copyright (c) 2015 SUSE Linux GmbH.  All rights reserved.
  2#
  3# This file is part of kiwi.
  4#
  5# kiwi is free software: you can redistribute it and/or modify
  6# it under the terms of the GNU General Public License as published by
  7# the Free Software Foundation, either version 3 of the License, or
  8# (at your option) any later version.
  9#
 10# kiwi is distributed in the hope that it will be useful,
 11# but WITHOUT ANY WARRANTY; without even the implied warranty of
 12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 13# GNU General Public License for more details.
 14#
 15# You should have received a copy of the GNU General Public License
 16# along with kiwi.  If not, see <http://www.gnu.org/licenses/>
 17#
 18import os
 19import logging
 20import collections
 21import pathlib
 22import shutil
 23from typing import Dict, List, MutableMapping, Optional
 24
 25# project
 26from kiwi.command import Command
 27from kiwi.exceptions import KiwiFileAccessError
 28
 29log = logging.getLogger('kiwi')
 30
 31
 32class Path:
 33    """
 34    **Directory path helpers**
 35    """
 36    @staticmethod
 37    def sort_by_hierarchy(path_list: List[str]) -> List[str]:
 38        """
 39        Sort given list of path names by their hierachy in the tree
 40
 41        Example:
 42
 43        .. code:: python
 44
 45            result = Path.sort_by_hierarchy(['/var/lib', '/var'])
 46
 47        :param list path_list: list of path names
 48
 49        :return: hierachy sorted path_list
 50
 51        :rtype: list
 52        """
 53        paths_at_depth: Dict[int, List[str]] = {}
 54        for path in path_list:
 55            path_elements = path.split('/')
 56            path_depth = len(path_elements)
 57            if path_depth not in paths_at_depth:
 58                paths_at_depth[path_depth] = []
 59            paths_at_depth[path_depth].append(path)
 60        ordered_paths_at_depth = collections.OrderedDict(
 61            sorted(paths_at_depth.items())
 62        )
 63        ordered_paths = []
 64        for path_depth in ordered_paths_at_depth:
 65            for path in ordered_paths_at_depth[path_depth]:
 66                ordered_paths.append(path)
 67        return ordered_paths
 68
 69    @staticmethod
 70    def access(path: str, mode: int, **kwargs) -> bool:
 71        """
 72        Check whether path can be accessed with the given mode.
 73
 74        :param str path: The path that should be checked for
 75            access.
 76
 77        :param int mode: Which access mode should be checked.
 78            This value must be a bit-wise or of one or more of the following
 79            constants: :py:const:`os.F_OK` (note that this one is zero),
 80            :py:const:`os.X_OK`, :py:const:`os.R_OK` and :py:const:`os.W_OK`
 81
 82        :param kwargs: further keyword arguments are forwarded to
 83            :func:`os.access`
 84
 85        :return: Boolean value whether this access mode is allowed
 86        :rtype: bool
 87
 88        :raises ValueError: if the supplied mode is invalid
 89        :raises kiwi.exceptions.KiwiFileNotFound: if the path does not exist or
 90            is not accessible by the current user
 91        """
 92        if mode & ~(os.F_OK | os.X_OK | os.R_OK | os.W_OK) != 0:
 93            raise ValueError('Invalid mode 0x{:X}'.format(mode))
 94        try:
 95            os.stat(path)
 96        except Exception as exc:
 97            raise KiwiFileAccessError(
 98                'Error accessing path {0} failed with: {1}'.format(path, exc)
 99            )
100
101        return os.access(path, mode, **kwargs)
102
103    @staticmethod
104    def create(path: str) -> None:
105        """
106        Create path and all sub directories to target
107
108        :param string path: path name
109        """
110        log.debug("Creating directory %s", path)
111        try:
112            pathlib.Path(path).mkdir(parents=True, exist_ok=True)
113        except Exception as issue:
114            raise KiwiFileAccessError(
115                f'Cannot create directory: {path}: {issue}'
116            )
117
118    @staticmethod
119    def wipe(path: str) -> None:
120        """
121        Delete path and all contents
122
123        :param string path: path name
124        """
125        if os.path.exists(path):
126            Command.run(
127                ['rm', '-r', '-f', path]
128            )
129
130    @staticmethod
131    def remove_hierarchy(root: str, path: str) -> None:
132        """
133        Recursively remove an empty path and its sub directories
134        starting at a given root directory. Ignore non empty or
135        protected paths and leave them untouched
136
137        :param string root: start at directory
138        :param string path: path name below root
139        """
140        Command.run(
141            [
142                'rmdir', '--ignore-fail-on-non-empty',
143                os.path.normpath(os.sep.join([root, path]))
144            ]
145        )
146        path_elements = path.split(os.sep)
147        protected_elements = [
148            'boot', 'dev', 'proc', 'run', 'sys', 'tmp', 'home', 'mnt'
149        ]
150        for path_index in reversed(range(0, len(path_elements))):
151            sub_path = os.sep.join(path_elements[0:path_index])
152            if sub_path:
153                if path_elements[path_index - 1] in protected_elements:
154                    log.warning(
155                        'remove_hierarchy: path {0} is protected'.format(
156                            os.path.normpath(os.sep.join([root, sub_path]))
157                        )
158                    )
159                    return
160                Command.run(
161                    [
162                        'rmdir', '--ignore-fail-on-non-empty',
163                        os.path.normpath(os.sep.join([root, sub_path]))
164                    ]
165                )
166
167    @staticmethod
168    def move_to_root(root: str, elements: List[str]) -> List[str]:
169        """
170        Change the given path elements to a new root directory
171
172        :param str root: the root path to trim
173        :param list elements: list of path names
174
175        :return: changed elements
176
177        :rtype: list
178        """
179        result = []
180        for element in elements:
181            normalized_element = os.path.normpath(element)
182            result.append(
183                normalized_element.replace(
184                    os.path.normpath(root), os.sep
185                ).replace('{0}{0}'.format(os.sep), os.sep)
186            )
187        return result
188
189    @staticmethod
190    def rebase_to_root(root: str, elements: List[str]) -> List[str]:
191        """
192        Include the root prefix for the given paths elements
193
194        :param str root: the new root path
195        :param list elements: list of path names
196
197        :return: changed elements
198
199        :rtype: list
200        """
201        result = []
202        for element in elements:
203            result.append(os.path.normpath(os.sep.join([root, element])))
204        return result
205
206    @staticmethod
207    def which(
208        filename: str,
209        custom_env: Optional[MutableMapping[str, str]] = None,
210        access_mode: int = os.F_OK | os.X_OK,
211        root_dir: Optional[str] = None
212    ) -> Optional[str]:
213        """
214        Lookup file name in PATH
215
216        :param string filename: file base name
217        :param list alternative_lookup_paths: list of additional lookup paths
218        :param list custom_env: a custom os.environ used to obtain ``$PATH``
219        :param int access_mode: one of the os access modes or a combination of
220         them (os.R_OK, os.W_OK and os.X_OK). If the provided access mode
221         does not match the file is considered not existing
222        :param str root_dir: the root path to look at
223
224        :return: absolute path to file or None
225
226        :rtype: str
227        """
228        system_path = (custom_env.get("PATH") if custom_env else os.environ.get("PATH")) or os.defpath
229
230        lookup_paths = system_path.split(os.pathsep)
231        if root_dir:
232            lookup_paths = Path.rebase_to_root(root_dir, lookup_paths)
233        log.debug(f"Looking for {filename} in {os.pathsep.join(lookup_paths)}")
234        return shutil.which(filename, access_mode, path=os.pathsep.join(lookup_paths))
235
236    @staticmethod
237    def first_exists(path: str) -> str:
238        """
239        Lookup first path that exists in the given path hierarchy
240        """
241        p = pathlib.Path(path)
242        if p.exists():
243            return path
244        else:
245            return Path.first_exists(format(p.parent))
log = <Logger kiwi (DEBUG)>
class Path:
 33class Path:
 34    """
 35    **Directory path helpers**
 36    """
 37    @staticmethod
 38    def sort_by_hierarchy(path_list: List[str]) -> List[str]:
 39        """
 40        Sort given list of path names by their hierachy in the tree
 41
 42        Example:
 43
 44        .. code:: python
 45
 46            result = Path.sort_by_hierarchy(['/var/lib', '/var'])
 47
 48        :param list path_list: list of path names
 49
 50        :return: hierachy sorted path_list
 51
 52        :rtype: list
 53        """
 54        paths_at_depth: Dict[int, List[str]] = {}
 55        for path in path_list:
 56            path_elements = path.split('/')
 57            path_depth = len(path_elements)
 58            if path_depth not in paths_at_depth:
 59                paths_at_depth[path_depth] = []
 60            paths_at_depth[path_depth].append(path)
 61        ordered_paths_at_depth = collections.OrderedDict(
 62            sorted(paths_at_depth.items())
 63        )
 64        ordered_paths = []
 65        for path_depth in ordered_paths_at_depth:
 66            for path in ordered_paths_at_depth[path_depth]:
 67                ordered_paths.append(path)
 68        return ordered_paths
 69
 70    @staticmethod
 71    def access(path: str, mode: int, **kwargs) -> bool:
 72        """
 73        Check whether path can be accessed with the given mode.
 74
 75        :param str path: The path that should be checked for
 76            access.
 77
 78        :param int mode: Which access mode should be checked.
 79            This value must be a bit-wise or of one or more of the following
 80            constants: :py:const:`os.F_OK` (note that this one is zero),
 81            :py:const:`os.X_OK`, :py:const:`os.R_OK` and :py:const:`os.W_OK`
 82
 83        :param kwargs: further keyword arguments are forwarded to
 84            :func:`os.access`
 85
 86        :return: Boolean value whether this access mode is allowed
 87        :rtype: bool
 88
 89        :raises ValueError: if the supplied mode is invalid
 90        :raises kiwi.exceptions.KiwiFileNotFound: if the path does not exist or
 91            is not accessible by the current user
 92        """
 93        if mode & ~(os.F_OK | os.X_OK | os.R_OK | os.W_OK) != 0:
 94            raise ValueError('Invalid mode 0x{:X}'.format(mode))
 95        try:
 96            os.stat(path)
 97        except Exception as exc:
 98            raise KiwiFileAccessError(
 99                'Error accessing path {0} failed with: {1}'.format(path, exc)
100            )
101
102        return os.access(path, mode, **kwargs)
103
104    @staticmethod
105    def create(path: str) -> None:
106        """
107        Create path and all sub directories to target
108
109        :param string path: path name
110        """
111        log.debug("Creating directory %s", path)
112        try:
113            pathlib.Path(path).mkdir(parents=True, exist_ok=True)
114        except Exception as issue:
115            raise KiwiFileAccessError(
116                f'Cannot create directory: {path}: {issue}'
117            )
118
119    @staticmethod
120    def wipe(path: str) -> None:
121        """
122        Delete path and all contents
123
124        :param string path: path name
125        """
126        if os.path.exists(path):
127            Command.run(
128                ['rm', '-r', '-f', path]
129            )
130
131    @staticmethod
132    def remove_hierarchy(root: str, path: str) -> None:
133        """
134        Recursively remove an empty path and its sub directories
135        starting at a given root directory. Ignore non empty or
136        protected paths and leave them untouched
137
138        :param string root: start at directory
139        :param string path: path name below root
140        """
141        Command.run(
142            [
143                'rmdir', '--ignore-fail-on-non-empty',
144                os.path.normpath(os.sep.join([root, path]))
145            ]
146        )
147        path_elements = path.split(os.sep)
148        protected_elements = [
149            'boot', 'dev', 'proc', 'run', 'sys', 'tmp', 'home', 'mnt'
150        ]
151        for path_index in reversed(range(0, len(path_elements))):
152            sub_path = os.sep.join(path_elements[0:path_index])
153            if sub_path:
154                if path_elements[path_index - 1] in protected_elements:
155                    log.warning(
156                        'remove_hierarchy: path {0} is protected'.format(
157                            os.path.normpath(os.sep.join([root, sub_path]))
158                        )
159                    )
160                    return
161                Command.run(
162                    [
163                        'rmdir', '--ignore-fail-on-non-empty',
164                        os.path.normpath(os.sep.join([root, sub_path]))
165                    ]
166                )
167
168    @staticmethod
169    def move_to_root(root: str, elements: List[str]) -> List[str]:
170        """
171        Change the given path elements to a new root directory
172
173        :param str root: the root path to trim
174        :param list elements: list of path names
175
176        :return: changed elements
177
178        :rtype: list
179        """
180        result = []
181        for element in elements:
182            normalized_element = os.path.normpath(element)
183            result.append(
184                normalized_element.replace(
185                    os.path.normpath(root), os.sep
186                ).replace('{0}{0}'.format(os.sep), os.sep)
187            )
188        return result
189
190    @staticmethod
191    def rebase_to_root(root: str, elements: List[str]) -> List[str]:
192        """
193        Include the root prefix for the given paths elements
194
195        :param str root: the new root path
196        :param list elements: list of path names
197
198        :return: changed elements
199
200        :rtype: list
201        """
202        result = []
203        for element in elements:
204            result.append(os.path.normpath(os.sep.join([root, element])))
205        return result
206
207    @staticmethod
208    def which(
209        filename: str,
210        custom_env: Optional[MutableMapping[str, str]] = None,
211        access_mode: int = os.F_OK | os.X_OK,
212        root_dir: Optional[str] = None
213    ) -> Optional[str]:
214        """
215        Lookup file name in PATH
216
217        :param string filename: file base name
218        :param list alternative_lookup_paths: list of additional lookup paths
219        :param list custom_env: a custom os.environ used to obtain ``$PATH``
220        :param int access_mode: one of the os access modes or a combination of
221         them (os.R_OK, os.W_OK and os.X_OK). If the provided access mode
222         does not match the file is considered not existing
223        :param str root_dir: the root path to look at
224
225        :return: absolute path to file or None
226
227        :rtype: str
228        """
229        system_path = (custom_env.get("PATH") if custom_env else os.environ.get("PATH")) or os.defpath
230
231        lookup_paths = system_path.split(os.pathsep)
232        if root_dir:
233            lookup_paths = Path.rebase_to_root(root_dir, lookup_paths)
234        log.debug(f"Looking for {filename} in {os.pathsep.join(lookup_paths)}")
235        return shutil.which(filename, access_mode, path=os.pathsep.join(lookup_paths))
236
237    @staticmethod
238    def first_exists(path: str) -> str:
239        """
240        Lookup first path that exists in the given path hierarchy
241        """
242        p = pathlib.Path(path)
243        if p.exists():
244            return path
245        else:
246            return Path.first_exists(format(p.parent))

Directory path helpers

@staticmethod
def sort_by_hierarchy(path_list: List[str]) -> List[str]:
37    @staticmethod
38    def sort_by_hierarchy(path_list: List[str]) -> List[str]:
39        """
40        Sort given list of path names by their hierachy in the tree
41
42        Example:
43
44        .. code:: python
45
46            result = Path.sort_by_hierarchy(['/var/lib', '/var'])
47
48        :param list path_list: list of path names
49
50        :return: hierachy sorted path_list
51
52        :rtype: list
53        """
54        paths_at_depth: Dict[int, List[str]] = {}
55        for path in path_list:
56            path_elements = path.split('/')
57            path_depth = len(path_elements)
58            if path_depth not in paths_at_depth:
59                paths_at_depth[path_depth] = []
60            paths_at_depth[path_depth].append(path)
61        ordered_paths_at_depth = collections.OrderedDict(
62            sorted(paths_at_depth.items())
63        )
64        ordered_paths = []
65        for path_depth in ordered_paths_at_depth:
66            for path in ordered_paths_at_depth[path_depth]:
67                ordered_paths.append(path)
68        return ordered_paths

Sort given list of path names by their hierachy in the tree

Example:

.. code:: python

result = Path.sort_by_hierarchy(['/var/lib', '/var'])
Parameters
  • list path_list: list of path names
Returns

hierachy sorted path_list

@staticmethod
def access(path: str, mode: int, **kwargs) -> bool:
 70    @staticmethod
 71    def access(path: str, mode: int, **kwargs) -> bool:
 72        """
 73        Check whether path can be accessed with the given mode.
 74
 75        :param str path: The path that should be checked for
 76            access.
 77
 78        :param int mode: Which access mode should be checked.
 79            This value must be a bit-wise or of one or more of the following
 80            constants: :py:const:`os.F_OK` (note that this one is zero),
 81            :py:const:`os.X_OK`, :py:const:`os.R_OK` and :py:const:`os.W_OK`
 82
 83        :param kwargs: further keyword arguments are forwarded to
 84            :func:`os.access`
 85
 86        :return: Boolean value whether this access mode is allowed
 87        :rtype: bool
 88
 89        :raises ValueError: if the supplied mode is invalid
 90        :raises kiwi.exceptions.KiwiFileNotFound: if the path does not exist or
 91            is not accessible by the current user
 92        """
 93        if mode & ~(os.F_OK | os.X_OK | os.R_OK | os.W_OK) != 0:
 94            raise ValueError('Invalid mode 0x{:X}'.format(mode))
 95        try:
 96            os.stat(path)
 97        except Exception as exc:
 98            raise KiwiFileAccessError(
 99                'Error accessing path {0} failed with: {1}'.format(path, exc)
100            )
101
102        return os.access(path, mode, **kwargs)

Check whether path can be accessed with the given mode.

Parameters
  • str path: The path that should be checked for access.

  • int mode: Which access mode should be checked. This value must be a bit-wise or of one or more of the following constants: os.F_OK (note that this one is zero), os.X_OK, os.R_OK and os.W_OK

  • kwargs: further keyword arguments are forwarded to os.access()

Returns

Boolean value whether this access mode is allowed

Raises
@staticmethod
def create(path: str) -> None:
104    @staticmethod
105    def create(path: str) -> None:
106        """
107        Create path and all sub directories to target
108
109        :param string path: path name
110        """
111        log.debug("Creating directory %s", path)
112        try:
113            pathlib.Path(path).mkdir(parents=True, exist_ok=True)
114        except Exception as issue:
115            raise KiwiFileAccessError(
116                f'Cannot create directory: {path}: {issue}'
117            )

Create path and all sub directories to target

Parameters
  • string path: path name
@staticmethod
def wipe(path: str) -> None:
119    @staticmethod
120    def wipe(path: str) -> None:
121        """
122        Delete path and all contents
123
124        :param string path: path name
125        """
126        if os.path.exists(path):
127            Command.run(
128                ['rm', '-r', '-f', path]
129            )

Delete path and all contents

Parameters
  • string path: path name
@staticmethod
def remove_hierarchy(root: str, path: str) -> None:
131    @staticmethod
132    def remove_hierarchy(root: str, path: str) -> None:
133        """
134        Recursively remove an empty path and its sub directories
135        starting at a given root directory. Ignore non empty or
136        protected paths and leave them untouched
137
138        :param string root: start at directory
139        :param string path: path name below root
140        """
141        Command.run(
142            [
143                'rmdir', '--ignore-fail-on-non-empty',
144                os.path.normpath(os.sep.join([root, path]))
145            ]
146        )
147        path_elements = path.split(os.sep)
148        protected_elements = [
149            'boot', 'dev', 'proc', 'run', 'sys', 'tmp', 'home', 'mnt'
150        ]
151        for path_index in reversed(range(0, len(path_elements))):
152            sub_path = os.sep.join(path_elements[0:path_index])
153            if sub_path:
154                if path_elements[path_index - 1] in protected_elements:
155                    log.warning(
156                        'remove_hierarchy: path {0} is protected'.format(
157                            os.path.normpath(os.sep.join([root, sub_path]))
158                        )
159                    )
160                    return
161                Command.run(
162                    [
163                        'rmdir', '--ignore-fail-on-non-empty',
164                        os.path.normpath(os.sep.join([root, sub_path]))
165                    ]
166                )

Recursively remove an empty path and its sub directories starting at a given root directory. Ignore non empty or protected paths and leave them untouched

Parameters
  • string root: start at directory
  • string path: path name below root
@staticmethod
def move_to_root(root: str, elements: List[str]) -> List[str]:
168    @staticmethod
169    def move_to_root(root: str, elements: List[str]) -> List[str]:
170        """
171        Change the given path elements to a new root directory
172
173        :param str root: the root path to trim
174        :param list elements: list of path names
175
176        :return: changed elements
177
178        :rtype: list
179        """
180        result = []
181        for element in elements:
182            normalized_element = os.path.normpath(element)
183            result.append(
184                normalized_element.replace(
185                    os.path.normpath(root), os.sep
186                ).replace('{0}{0}'.format(os.sep), os.sep)
187            )
188        return result

Change the given path elements to a new root directory

Parameters
  • str root: the root path to trim
  • list elements: list of path names
Returns

changed elements

@staticmethod
def rebase_to_root(root: str, elements: List[str]) -> List[str]:
190    @staticmethod
191    def rebase_to_root(root: str, elements: List[str]) -> List[str]:
192        """
193        Include the root prefix for the given paths elements
194
195        :param str root: the new root path
196        :param list elements: list of path names
197
198        :return: changed elements
199
200        :rtype: list
201        """
202        result = []
203        for element in elements:
204            result.append(os.path.normpath(os.sep.join([root, element])))
205        return result

Include the root prefix for the given paths elements

Parameters
  • str root: the new root path
  • list elements: list of path names
Returns

changed elements

@staticmethod
def which( filename: str, custom_env: Optional[MutableMapping[str, str]] = None, access_mode: int = 1, root_dir: Optional[str] = None) -> Optional[str]:
207    @staticmethod
208    def which(
209        filename: str,
210        custom_env: Optional[MutableMapping[str, str]] = None,
211        access_mode: int = os.F_OK | os.X_OK,
212        root_dir: Optional[str] = None
213    ) -> Optional[str]:
214        """
215        Lookup file name in PATH
216
217        :param string filename: file base name
218        :param list alternative_lookup_paths: list of additional lookup paths
219        :param list custom_env: a custom os.environ used to obtain ``$PATH``
220        :param int access_mode: one of the os access modes or a combination of
221         them (os.R_OK, os.W_OK and os.X_OK). If the provided access mode
222         does not match the file is considered not existing
223        :param str root_dir: the root path to look at
224
225        :return: absolute path to file or None
226
227        :rtype: str
228        """
229        system_path = (custom_env.get("PATH") if custom_env else os.environ.get("PATH")) or os.defpath
230
231        lookup_paths = system_path.split(os.pathsep)
232        if root_dir:
233            lookup_paths = Path.rebase_to_root(root_dir, lookup_paths)
234        log.debug(f"Looking for {filename} in {os.pathsep.join(lookup_paths)}")
235        return shutil.which(filename, access_mode, path=os.pathsep.join(lookup_paths))

Lookup file name in PATH

Parameters
  • string filename: file base name
  • list alternative_lookup_paths: list of additional lookup paths
  • list custom_env: a custom os.environ used to obtain $PATH
  • int access_mode: one of the os access modes or a combination of them (os.R_OK, os.W_OK and os.X_OK). If the provided access mode does not match the file is considered not existing
  • str root_dir: the root path to look at
Returns

absolute path to file or None

@staticmethod
def first_exists(path: str) -> str:
237    @staticmethod
238    def first_exists(path: str) -> str:
239        """
240        Lookup first path that exists in the given path hierarchy
241        """
242        p = pathlib.Path(path)
243        if p.exists():
244            return path
245        else:
246            return Path.first_exists(format(p.parent))

Lookup first path that exists in the given path hierarchy