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() | |
|
dnj
2017/05/31 23:58:58
Still think this should be "add_json_log=False". I
iannucci
2017/06/01 00:31:03
changed to step_text only when it fails
| |
| 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 # pylint thinks this isn't a standard exception... silly pylint. | |
| 40 # pylint: disable=nonstandard-exception | |
| 41 raise self.Error(name, j['errno_name'], j['message']) | |
| 42 return result | |
| 43 | |
| 44 def copy(self, name, source, dest): | |
| 45 """Copies a file (including mode bits) from source to destination on the | |
| 46 local filesystem. | |
| 47 | |
| 48 Behaves identically to shutil.copy. | |
| 49 | |
| 50 Args: | |
| 51 name (str) - The name of the step. | |
| 52 source (Path) - The path to the file you want to copy. | |
| 53 dest (Path) - The path to the destination file name. If this path exists | |
| 54 and is a directory, the basename of `source` will be appended to derive | |
| 55 a path to a destination file. | |
| 56 | |
| 57 Raises file.Error | |
| 58 """ | |
| 59 self.m.path.assert_absolute(source) | |
| 60 self.m.path.assert_absolute(dest) | |
| 61 self._run(name, ['copy', source, dest]) | |
| 62 self.m.path.mock_copy_paths(source, dest) | |
| 63 | |
| 64 def copytree(self, name, source, dest, symlinks=False): | |
| 65 """Recursively copies a directory tree. | |
| 66 | |
| 67 Behaves identically to shutil.copytree. | |
| 68 `dest` must not exist. | |
| 69 | |
| 70 Args: | |
| 71 name (str) - The name of the step. | |
| 72 source (Path) - The path of the directory to copy. | |
| 73 dest (Path) - The place where you want the recursive copy to show up. This | |
| 74 must not already exist. | |
| 75 symlinks (bool) - Preserve symlinks. No effect on Windows. | |
| 76 | |
| 77 Raises file.Error | |
| 78 """ | |
| 79 self.m.path.assert_absolute(source) | |
| 80 self.m.path.assert_absolute(dest) | |
| 81 prefix = ['--symlinks'] if symlinks else [] | |
| 82 self._run(name, prefix+['copytree', source, dest]) | |
| 83 self.m.path.mock_copy_paths(source, dest) | |
| 84 | |
| 85 def move(self, name, source, dest): | |
| 86 """Moves a file or directory. | |
| 87 | |
| 88 Behaves identically to shutil.move. | |
| 89 | |
| 90 Args: | |
| 91 name (str) - The name of the step. | |
| 92 source (Path) - The path of the item to move. | |
| 93 dest (Path) - The new name of the item. | |
| 94 | |
| 95 Raises file.Error | |
| 96 """ | |
| 97 self.m.path.assert_absolute(source) | |
| 98 self.m.path.assert_absolute(dest) | |
| 99 self._run(name, ['move', source, dest]) | |
| 100 self.m.path.mock_copy_paths(source, dest) | |
| 101 self.m.path.mock_remove_paths(source) | |
| 102 | |
| 103 def read_text(self, name, source, test_data=''): | |
| 104 """Reads a file as UTF-8 encoded text. | |
| 105 | |
| 106 Args: | |
| 107 name (str) - The name of the step. | |
| 108 source (Path) - The path of the file to read. | |
| 109 test_data (str) - Some default data for this step to return when running | |
| 110 under simulation. | |
| 111 | |
| 112 Returns (str) - The content of the file. | |
| 113 | |
| 114 Raises file.Error | |
| 115 """ | |
| 116 self.m.path.assert_absolute(source) | |
| 117 step_test_data = lambda: self.test_api.read_text(test_data) | |
| 118 result = self._run(name, ['copy', source, self.m.raw_io.output_text()], | |
| 119 step_test_data=step_test_data) | |
| 120 return result.raw_io.output_text | |
| 121 | |
| 122 def write_text(self, name, dest, text_data): | |
| 123 """Write the given UTF-8 encoded `text_data` to `dest`. | |
| 124 | |
| 125 Args: | |
| 126 name (str) - The name of the step. | |
| 127 dest (Path) - The path of the file to write. | |
| 128 text_data (str) - The UTF-8 encoded data to write. | |
| 129 | |
| 130 Raises file.Error. | |
| 131 """ | |
| 132 self.m.path.assert_absolute(dest) | |
| 133 self._run(name, ['copy', self.m.raw_io.input_text(text_data), dest]) | |
| 134 self.m.path.mock_add_paths(dest) | |
| 135 | |
| 136 def glob_paths(self, name, pattern, test_data=()): | |
| 137 """Performs glob expansion on `pattern`. | |
| 138 | |
| 139 glob rules for `pattern` follow the same syntax as for the python `glob` | |
| 140 stdlib module. | |
| 141 | |
| 142 Args: | |
| 143 name (str) - The name of the step. | |
| 144 pattern (str|Path) - The glob pattern to expand. | |
| 145 test_data (iterable[str|Path]) - Some default paths for this step to | |
| 146 return when running under simulation. | |
| 147 | |
| 148 Returns list[Path] - All paths found. | |
| 149 | |
| 150 Raises file.Error. | |
| 151 """ | |
| 152 ret = self._run( | |
| 153 name, ['glob', pattern], | |
| 154 lambda: self.test_api.glob_paths(test_data), | |
| 155 self.m.raw_io.output_text()) | |
| 156 return map(self.m.path.abs_to_path, ret.stdout.splitlines()) | |
| 157 | |
| 158 def remove(self, name, source): | |
| 159 """Remove a file. | |
| 160 | |
| 161 Args: | |
| 162 name (str) - The name of the step. | |
| 163 source (Path) - The file to remove. | |
| 164 | |
| 165 Raises file.Error. | |
| 166 """ | |
| 167 self.m.path.assert_absolute(source) | |
| 168 self._run(name, ['remove', source]) | |
| 169 self.m.path.mock_remove_paths(source) | |
| 170 | |
| 171 def listdir(self, name, source, test_data=()): | |
| 172 """List all files inside a directory. | |
| 173 | |
| 174 Args: | |
| 175 name (str) - The name of the step. | |
| 176 source (Path) - The directory to list. | |
| 177 test_data (iterable[str]) - Some default data for this step to return when | |
| 178 running under simulation. | |
| 179 | |
| 180 Returns list[Path] | |
| 181 | |
| 182 Raises file.Error. | |
| 183 """ | |
| 184 self.m.path.assert_absolute(source) | |
| 185 ret = self._run( | |
| 186 name, ['listdir', source], | |
| 187 lambda: self.test_api.listdir(test_data), | |
| 188 self.m.raw_io.output_text()) | |
| 189 return map(self.m.path.abs_to_path, ret.stdout.splitlines()) | |
|
dnj
2017/05/31 23:58:58
If the process returns abspath, the test data will
iannucci
2017/06/01 00:31:03
Done.
| |
| 190 | |
| 191 def ensure_directory(self, name, dest, mode=0777): | |
| 192 """Ensures that `dest` exists and is a directory. | |
| 193 | |
| 194 Args: | |
| 195 name (str) - The name of the step. | |
| 196 dest (Path) - The directory to ensure. | |
| 197 mode (int) - The mode to use if the directory doesn't exist. This method | |
| 198 does not ensure the mode if the directory already exists (if you need | |
| 199 that behaviour, file a bug). | |
| 200 | |
| 201 Raises file.Error if the path exists but is not a directory. | |
| 202 """ | |
| 203 self.m.path.assert_absolute(dest) | |
| 204 self._run( | |
| 205 name, ['ensure-directory', '--mode', oct(mode), dest]) | |
| 206 self.m.path.mock_add_paths(dest) | |
| 207 | |
| 208 def rmtree(self, name, source): | |
| 209 """Recursively removes a directory. | |
| 210 | |
| 211 This uses a native python on Linux/Mac, and uses `rd` on Windows to avoid | |
| 212 issues w.r.t. path lengths and read-only attributes. If the directory is | |
| 213 gone already, this returns without error. | |
| 214 | |
| 215 Args: | |
| 216 name (str) - The name of the step. | |
| 217 source (Path) - The directory to remove. | |
| 218 | |
| 219 Raises file.Error. | |
| 220 """ | |
| 221 self.m.path.assert_absolute(source) | |
| 222 self._run(name, ['rmtree', source]) | |
| 223 self.m.path.mock_remove_paths(str(source)) | |
| 224 | |
| 225 def rmcontents(self, name, source): | |
| 226 """Similar to rmtree, but removes only contents not the directory. | |
| 227 | |
| 228 This is useful e.g. when removing contents of current working directory. | |
| 229 Deleting current working directory makes all further getcwd calls fail | |
| 230 until chdir is called. chdir would be tricky in recipes, so we provide | |
| 231 a call that doesn't delete the directory itself. | |
| 232 | |
| 233 Args: | |
| 234 name (str) - The name of the step. | |
| 235 source (Path) - The directory whose contents should be removed. | |
| 236 | |
| 237 Raises file.Error. | |
| 238 """ | |
| 239 self.m.path.assert_absolute(source) | |
| 240 self._run(name, ['rmcontents', source]) | |
| 241 self.m.path.mock_remove_paths(str(source)+self.m.path.sep) | |
| 242 | |
| 243 def rmglob(self, name, source, pattern): | |
| 244 """Removes all entries in `source` matching the glob `pattern`. | |
| 245 | |
| 246 Args: | |
| 247 name (str) - The name of the step. | |
| 248 source (Path) - The directory whose contents should be filtered and | |
| 249 removed. | |
| 250 pattern (str) - The glob pattern to apply under `source`. Anything | |
| 251 matching this pattern will be removed. | |
| 252 | |
| 253 Raises file.Error. | |
| 254 """ | |
| 255 self.m.path.assert_absolute(source) | |
| 256 self._run(name, ['rmglob', source, pattern]) | |
| 257 | |
| 258 src = str(source) | |
| 259 def filt(p): | |
| 260 assert p.startswith(src), (src, p) | |
| 261 return fnmatch.fnmatch(p[len(src)+1:].split(os.path.sep)[0], pattern) | |
| 262 self.m.path.mock_remove_paths(str(source), filt) | |
| OLD | NEW |