Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 # Copyright 2017 The LUCI Authors. All rights reserved. | |
| 2 # Use of this source code is governed under the Apache License, Version 2.0 | |
| 3 # that can be found in the LICENSE file. | |
| 4 | |
| 5 from recipe_engine import recipe_api | |
| 6 | |
| 7 | |
| 8 import os | |
| 9 import fnmatch | |
| 10 | |
| 11 # TODO(iannucci): Introduce the concept of a 'native step' and implement these | |
| 12 # directly in the current python interpreter without the need for a subprocess | |
| 13 # invocation. | |
| 14 | |
| 15 class FileApi(recipe_api.RecipeApi): | |
| 16 """FileApi contains helper functions for reading and writing files.""" | |
| 17 | |
| 18 class Error(recipe_api.InfraFailure): | |
| 19 """Error is an InfraFailure, except that it also contains an errno field | |
| 20 indicating the errno name (i.e. 'EEXIST') of the underlying error. | |
| 21 """ | |
| 22 def __init__(self, step_name, errno_name, message): | |
| 23 reason = 'Step(%r) failed %r with: %s' % (step_name, errno_name, message) | |
| 24 super(FileApi.Error, self).__init__(reason) | |
| 25 self.errno_name = errno_name | |
| 26 | |
| 27 def _run(self, name, args, step_test_data=None, stdout=None): | |
| 28 if not step_test_data: | |
| 29 step_test_data = self.test_api.errno | |
| 30 args = [ | |
| 31 '--json-output', self.m.json.output(add_json_log=False) | |
| 32 ] + args | |
| 33 result = self.m.python( | |
| 34 name, self.resource('fileutil.py'), args=args, | |
| 35 step_test_data=step_test_data, stdout=stdout, | |
| 36 infra_step=True) | |
| 37 j = result.json.output | |
| 38 if not j['ok']: | |
| 39 result.presentation.step_text = j['message'] | |
| 40 # pylint thinks this isn't a standard exception... silly pylint. | |
| 41 # pylint: disable=nonstandard-exception | |
| 42 raise self.Error(name, j['errno_name'], j['message']) | |
| 43 return result | |
| 44 | |
| 45 def copy(self, name, source, dest): | |
| 46 """Copies a file (including mode bits) from source to destination on the | |
| 47 local filesystem. | |
| 48 | |
| 49 Behaves identically to shutil.copy. | |
| 50 | |
| 51 Args: | |
| 52 name (str) - The name of the step. | |
| 53 source (Path) - The path to the file you want to copy. | |
| 54 dest (Path) - The path to the destination file name. If this path exists | |
| 55 and is a directory, the basename of `source` will be appended to derive | |
| 56 a path to a destination file. | |
| 57 | |
| 58 Raises file.Error | |
| 59 """ | |
| 60 self.m.path.assert_absolute(source) | |
| 61 self.m.path.assert_absolute(dest) | |
| 62 self._run(name, ['copy', source, dest]) | |
| 63 self.m.path.mock_copy_paths(source, dest) | |
| 64 | |
| 65 def copytree(self, name, source, dest, symlinks=False): | |
| 66 """Recursively copies a directory tree. | |
| 67 | |
| 68 Behaves identically to shutil.copytree. | |
| 69 `dest` must not exist. | |
| 70 | |
| 71 Args: | |
| 72 name (str) - The name of the step. | |
| 73 source (Path) - The path of the directory to copy. | |
| 74 dest (Path) - The place where you want the recursive copy to show up. This | |
| 75 must not already exist. | |
| 76 symlinks (bool) - Preserve symlinks. No effect on Windows. | |
| 77 | |
| 78 Raises file.Error | |
| 79 """ | |
| 80 self.m.path.assert_absolute(source) | |
| 81 self.m.path.assert_absolute(dest) | |
| 82 prefix = ['--symlinks'] if symlinks else [] | |
|
Lei Zhang
2017/06/27 05:03:05
On https://build.chromium.org/p/chromium.infra.cro
| |
| 83 self._run(name, prefix+['copytree', source, dest]) | |
| 84 self.m.path.mock_copy_paths(source, dest) | |
| 85 | |
| 86 def move(self, name, source, dest): | |
| 87 """Moves a file or directory. | |
| 88 | |
| 89 Behaves identically to shutil.move. | |
| 90 | |
| 91 Args: | |
| 92 name (str) - The name of the step. | |
| 93 source (Path) - The path of the item to move. | |
| 94 dest (Path) - The new name of the item. | |
| 95 | |
| 96 Raises file.Error | |
| 97 """ | |
| 98 self.m.path.assert_absolute(source) | |
| 99 self.m.path.assert_absolute(dest) | |
| 100 self._run(name, ['move', source, dest]) | |
| 101 self.m.path.mock_copy_paths(source, dest) | |
| 102 self.m.path.mock_remove_paths(source) | |
| 103 | |
| 104 def read_text(self, name, source, test_data=''): | |
| 105 """Reads a file as UTF-8 encoded text. | |
| 106 | |
| 107 Args: | |
| 108 name (str) - The name of the step. | |
| 109 source (Path) - The path of the file to read. | |
| 110 test_data (str) - Some default data for this step to return when running | |
| 111 under simulation. | |
| 112 | |
| 113 Returns (str) - The content of the file. | |
| 114 | |
| 115 Raises file.Error | |
| 116 """ | |
| 117 self.m.path.assert_absolute(source) | |
| 118 step_test_data = lambda: self.test_api.read_text(test_data) | |
| 119 result = self._run(name, ['copy', source, self.m.raw_io.output_text()], | |
| 120 step_test_data=step_test_data) | |
| 121 return result.raw_io.output_text | |
| 122 | |
| 123 def write_text(self, name, dest, text_data): | |
| 124 """Write the given UTF-8 encoded `text_data` to `dest`. | |
| 125 | |
| 126 Args: | |
| 127 name (str) - The name of the step. | |
| 128 dest (Path) - The path of the file to write. | |
| 129 text_data (str) - The UTF-8 encoded data to write. | |
| 130 | |
| 131 Raises file.Error. | |
| 132 """ | |
| 133 self.m.path.assert_absolute(dest) | |
| 134 self._run(name, ['copy', self.m.raw_io.input_text(text_data), dest]) | |
| 135 self.m.path.mock_add_paths(dest) | |
| 136 | |
| 137 def glob_paths(self, name, source, pattern, test_data=()): | |
| 138 """Performs glob expansion on `pattern`. | |
| 139 | |
| 140 glob rules for `pattern` follow the same syntax as for the python `glob` | |
| 141 stdlib module. | |
| 142 | |
| 143 Args: | |
| 144 name (str) - The name of the step. | |
| 145 source (Path) - The directory whose contents should be globbed. | |
| 146 pattern (str) - The glob pattern to apply under `source`. | |
| 147 test_data (iterable[str]) - Some default data for this step to return when | |
| 148 running under simulation. This should be the list of file items found | |
| 149 in this directory. | |
| 150 | |
| 151 Returns list[Path] - All paths found. | |
| 152 | |
| 153 Raises file.Error. | |
| 154 """ | |
| 155 ret = self._run( | |
| 156 name, ['glob', source, pattern], | |
| 157 lambda: self.test_api.glob_paths(test_data), | |
| 158 self.m.raw_io.output_text()) | |
| 159 return [ | |
| 160 source.join(x) for x in ret.stdout.splitlines() | |
| 161 ] | |
| 162 | |
| 163 def remove(self, name, source): | |
| 164 """Remove a file. | |
| 165 | |
| 166 Args: | |
| 167 name (str) - The name of the step. | |
| 168 source (Path) - The file to remove. | |
| 169 | |
| 170 Raises file.Error. | |
| 171 """ | |
| 172 self.m.path.assert_absolute(source) | |
| 173 self._run(name, ['remove', source]) | |
| 174 self.m.path.mock_remove_paths(source) | |
| 175 | |
| 176 def listdir(self, name, source, test_data=()): | |
| 177 """List all files inside a directory. | |
| 178 | |
| 179 Args: | |
| 180 name (str) - The name of the step. | |
| 181 source (Path) - The directory to list. | |
| 182 test_data (iterable[str]) - Some default data for this step to return when | |
| 183 running under simulation. This should be the list of file items found | |
| 184 in this directory. | |
| 185 | |
| 186 Returns list[Path] | |
| 187 | |
| 188 Raises file.Error. | |
| 189 """ | |
| 190 self.m.path.assert_absolute(source) | |
| 191 ret = self._run( | |
| 192 name, ['listdir', source], | |
| 193 lambda: self.test_api.listdir(test_data), | |
| 194 self.m.raw_io.output_text()) | |
| 195 return [source.join(x) for x in ret.stdout.splitlines()] | |
| 196 | |
| 197 def ensure_directory(self, name, dest, mode=0777): | |
| 198 """Ensures that `dest` exists and is a directory. | |
| 199 | |
| 200 Args: | |
| 201 name (str) - The name of the step. | |
| 202 dest (Path) - The directory to ensure. | |
| 203 mode (int) - The mode to use if the directory doesn't exist. This method | |
| 204 does not ensure the mode if the directory already exists (if you need | |
| 205 that behaviour, file a bug). | |
| 206 | |
| 207 Raises file.Error if the path exists but is not a directory. | |
| 208 """ | |
| 209 self.m.path.assert_absolute(dest) | |
| 210 self._run( | |
| 211 name, ['ensure-directory', '--mode', oct(mode), dest]) | |
| 212 self.m.path.mock_add_paths(dest) | |
| 213 | |
| 214 def rmtree(self, name, source): | |
| 215 """Recursively removes a directory. | |
| 216 | |
| 217 This uses a native python on Linux/Mac, and uses `rd` on Windows to avoid | |
| 218 issues w.r.t. path lengths and read-only attributes. If the directory is | |
| 219 gone already, this returns without error. | |
| 220 | |
| 221 Args: | |
| 222 name (str) - The name of the step. | |
| 223 source (Path) - The directory to remove. | |
| 224 | |
| 225 Raises file.Error. | |
| 226 """ | |
| 227 self.m.path.assert_absolute(source) | |
| 228 self._run(name, ['rmtree', source]) | |
| 229 self.m.path.mock_remove_paths(str(source)) | |
| 230 | |
| 231 def rmcontents(self, name, source): | |
| 232 """Similar to rmtree, but removes only contents not the directory. | |
| 233 | |
| 234 This is useful e.g. when removing contents of current working directory. | |
| 235 Deleting current working directory makes all further getcwd calls fail | |
| 236 until chdir is called. chdir would be tricky in recipes, so we provide | |
| 237 a call that doesn't delete the directory itself. | |
| 238 | |
| 239 Args: | |
| 240 name (str) - The name of the step. | |
| 241 source (Path) - The directory whose contents should be removed. | |
| 242 | |
| 243 Raises file.Error. | |
| 244 """ | |
| 245 self.m.path.assert_absolute(source) | |
| 246 self._run(name, ['rmcontents', source]) | |
| 247 self.m.path.mock_remove_paths(str(source)+self.m.path.sep) | |
| 248 | |
| 249 def rmglob(self, name, source, pattern): | |
| 250 """Removes all entries in `source` matching the glob `pattern`. | |
| 251 | |
| 252 Args: | |
| 253 name (str) - The name of the step. | |
| 254 source (Path) - The directory whose contents should be filtered and | |
| 255 removed. | |
| 256 pattern (str) - The glob pattern to apply under `source`. Anything | |
| 257 matching this pattern will be removed. | |
| 258 | |
| 259 Raises file.Error. | |
| 260 """ | |
| 261 self.m.path.assert_absolute(source) | |
| 262 self._run(name, ['rmglob', source, pattern]) | |
| 263 | |
| 264 src = str(source) | |
| 265 def filt(p): | |
| 266 assert p.startswith(src), (src, p) | |
| 267 return fnmatch.fnmatch(p[len(src)+1:].split(os.path.sep)[0], pattern) | |
| 268 self.m.path.mock_remove_paths(str(source), filt) | |
| OLD | NEW |