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))
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
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
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_OKandos.W_OKkwargs: further keyword arguments are forwarded to
os.access()
Returns
Boolean value whether this access mode is allowed
Raises
- ValueError: if the supplied mode is invalid
- kiwi.exceptions.KiwiFileNotFound: if the path does not exist or is not accessible by the current user
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
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
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
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
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
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
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