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, timeout, 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(name='main', add_json_log=False) | |
| 32 ] + args | |
| 33 result = self.m.python( | |
| 34 name, self.resource('fileutil.py'), args=args, | |
| 35 timeout=timeout, step_test_data=step_test_data, stdout=stdout, | |
| 36 infra_step=True) | |
| 37 j = result.json.outputs['main'] | |
|
dnj
2017/05/31 16:50:40
Only one output, WDYT about naming default?
iannucci
2017/05/31 22:59:26
at first I thought that other things would need th
| |
| 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, timeout=None): | |
| 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 timeout (int) - Maximum number of seconds to allow this command to run. | |
| 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], timeout) | |
| 63 self.m.path.mock_copy_paths(source, dest) | |
| 64 | |
| 65 def copytree(self, name, source, dest, symlinks=False, timeout=None): | |
| 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 timeout (int) - Maximum number of seconds to allow this command to run. | |
|
dnj
2017/05/31 16:50:40
nit: Document "symlinks"
iannucci
2017/05/31 22:59:26
Done.
| |
| 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 [] | |
| 83 self._run(name, prefix+['copytree', source, dest], timeout) | |
| 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], timeout=5) | |
|
dnj
2017/05/31 16:50:40
This timeout might actually be inadequate if it's
iannucci
2017/05/31 22:59:26
blergh, true. Removed.
| |
| 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 timeout=30, 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 timeout=30) | |
| 136 self.m.path.mock_add_paths(dest) | |
| 137 | |
| 138 def glob_paths(self, name, pattern, test_data=(), timeout=None): | |
| 139 """Performs glob expansion on `pattern`. | |
| 140 | |
| 141 glob rules for `pattern` follow the same syntax as for the python `glob` | |
| 142 stdlib module. | |
| 143 | |
| 144 Args: | |
| 145 name (str) - The name of the step. | |
| 146 pattern (str|Path) - The glob pattern to expand. | |
| 147 test_data (iterable[str|Path]) - Some default paths for this step to | |
| 148 return when running under simulation. | |
| 149 timeout (int) - Maximum number of seconds to allow this command to run. | |
| 150 | |
| 151 Returns list[Path] - All paths found. | |
| 152 | |
| 153 Raises file.Error. | |
| 154 """ | |
| 155 ret = self._run( | |
| 156 name, ['glob', pattern], timeout, | |
| 157 lambda: self.test_api.glob_paths(test_data), | |
| 158 self.m.raw_io.output_text()) | |
| 159 return map(self.m.path.abs_to_path, ret.stdout.splitlines()) | |
| 160 | |
| 161 def remove(self, name, source): | |
| 162 """Remove a file. | |
| 163 | |
| 164 Args: | |
| 165 name (str) - The name of the step. | |
| 166 source (Path) - The file to remove. | |
| 167 | |
| 168 Raises file.Error. | |
| 169 """ | |
| 170 self.m.path.assert_absolute(source) | |
| 171 self._run(name, ['remove', source], timeout=5) | |
| 172 self.m.path.mock_remove_paths(source) | |
| 173 | |
| 174 def listdir(self, name, source, test_data=(), timeout=None): | |
| 175 """List all files inside a directory. | |
| 176 | |
| 177 Args: | |
| 178 name (str) - The name of the step. | |
| 179 source (Path) - The directory to list. | |
| 180 test_data (iterable[str]) - Some default data for this step to return when | |
| 181 running under simulation. | |
| 182 timeout (int) - Maximum number of seconds to allow this command to run. | |
|
dnj
2017/05/31 16:50:40
Is timeout useful here? I can't think of a meaning
| |
| 183 | |
| 184 Returns list[str] - The names of the entries in the source directory. Note | |
|
dnj
2017/05/31 16:50:40
QQ: why not Path objects?
iannucci
2017/05/31 22:59:26
Because the usage of listdir frequently deals with
| |
| 185 that this is just the names of the files/folders, NOT Path objects. If you | |
| 186 want Path objects, join these with `source`, and use the path module's | |
| 187 `abs_to_path()` method. | |
| 188 | |
| 189 Raises file.Error. | |
| 190 """ | |
| 191 self.m.path.assert_absolute(source) | |
| 192 ret = self._run( | |
| 193 name, ['listdir', source], timeout, | |
| 194 lambda: self.test_api.listdir(test_data), | |
| 195 self.m.raw_io.output_text()) | |
| 196 return ret.stdout.splitlines() | |
| 197 | |
| 198 def ensure_directory(self, name, dest, mode=0777): | |
| 199 """Ensures that `dest` exists and is a directory. | |
| 200 | |
| 201 Args: | |
| 202 name (str) - The name of the step. | |
| 203 dest (Path) - The directory to ensure. | |
| 204 mode (int) - The mode to use if the directory doesn't exist. This method | |
| 205 does not ensure the mode if the directory already exists (if you need | |
| 206 that behaviour, file a bug). | |
| 207 | |
| 208 Raises file.Error if the path exists but is not a directory. | |
| 209 """ | |
| 210 self.m.path.assert_absolute(dest) | |
| 211 self._run( | |
| 212 name, ['ensure-directory', '--mode', oct(mode), dest], timeout=5) | |
| 213 self.m.path.mock_add_paths(dest) | |
| 214 | |
| 215 def rmtree(self, name, source, timeout=None): | |
| 216 """Recursively removes a directory. | |
| 217 | |
| 218 This uses a native python on Linux/Mac, and uses `rd` on Windows to avoid | |
| 219 issues w.r.t. path lengths and read-only attributes. If the directory is | |
| 220 gone already, this returns without error. | |
| 221 | |
| 222 Args: | |
| 223 name (str) - The name of the step. | |
| 224 source (Path) - The directory to remove. | |
| 225 timeout (int) - Maximum number of seconds to allow this command to run. | |
| 226 | |
| 227 Raises file.Error. | |
| 228 """ | |
| 229 self.m.path.assert_absolute(source) | |
| 230 self._run(name, ['rmtree', source], timeout) | |
| 231 self.m.path.mock_remove_paths(str(source)) | |
| 232 | |
| 233 def rmcontents(self, name, source, timeout=None): | |
| 234 """Similar to rmtree, but removes only contents not the directory. | |
| 235 | |
| 236 This is useful e.g. when removing contents of current working directory. | |
| 237 Deleting current working directory makes all further getcwd calls fail | |
| 238 until chdir is called. chdir would be tricky in recipes, so we provide | |
| 239 a call that doesn't delete the directory itself. | |
| 240 | |
| 241 Args: | |
| 242 name (str) - The name of the step. | |
| 243 source (Path) - The directory whose contents should be removed. | |
| 244 timeout (int) - Maximum number of seconds to allow this command to run. | |
| 245 | |
| 246 Raises file.Error. | |
| 247 """ | |
| 248 self.m.path.assert_absolute(source) | |
| 249 self._run(name, ['rmcontents', source], timeout) | |
| 250 self.m.path.mock_remove_paths(str(source)+self.m.path.sep) | |
| 251 | |
| 252 def rmglob(self, name, source, pattern, timeout=None): | |
| 253 """Removes all entries in `source` matching the glob `pattern`. | |
| 254 | |
| 255 Args: | |
| 256 name (str) - The name of the step. | |
| 257 source (Path) - The directory whose contents should be filtered and | |
| 258 removed. | |
| 259 pattern (str) - The glob pattern to apply under `source`. Anything | |
| 260 matching this pattern will be removed. | |
| 261 timeout (int) - Maximum number of seconds to allow this command to run. | |
| 262 | |
| 263 Raises file.Error. | |
| 264 """ | |
| 265 self.m.path.assert_absolute(source) | |
| 266 self._run(name, ['rmglob', source, pattern], timeout) | |
| 267 | |
| 268 src = str(source) | |
| 269 def filt(p): | |
| 270 assert p.startswith(src), (src, p) | |
| 271 return fnmatch.fnmatch(p[len(src)+1:].split(os.path.sep)[0], pattern) | |
| 272 self.m.path.mock_remove_paths(str(source), filt) | |
| OLD | NEW |