|
1 | 1 | """Implements Path-like object for remote hosts.""" |
2 | 2 |
|
3 | 3 | import logging |
4 | | -import re |
5 | 4 | import stat |
| 5 | +from collections import deque |
| 6 | +from fnmatch import fnmatch |
6 | 7 | from functools import wraps |
7 | 8 | from os import fspath |
8 | | -from os.path import join |
9 | 9 | from pathlib import Path, PurePosixPath, PureWindowsPath # type: ignore |
10 | | -from typing import TYPE_CHECKING, Any, Callable, Generator, Optional, Union |
| 10 | +from typing import (TYPE_CHECKING, Any, Callable, Deque, Generator, Optional, |
| 11 | + Union) |
11 | 12 |
|
12 | | -from ..utils import for_all_methods, glob2re |
| 13 | +from ..utils import for_all_methods |
13 | 14 |
|
14 | 15 | if TYPE_CHECKING: |
15 | 16 | from paramiko.sftp_attr import SFTPAttributes |
@@ -113,7 +114,6 @@ def __new__(cls, connection: "SSHConnection", *args, **kwargs): |
113 | 114 | else: |
114 | 115 | cls._flavour = PurePosixPath._flavour # type: ignore |
115 | 116 | except AttributeError as e: |
116 | | - print(e) |
117 | 117 | log.exception(e) |
118 | 118 |
|
119 | 119 | self = cls._from_parts(args, init=False) # type: ignore |
@@ -234,31 +234,108 @@ def glob(self, pattern: str) -> Generator["SSHPath", None, None]: |
234 | 234 | ------ |
235 | 235 | FileNotFoundError |
236 | 236 | if current path does not point to directory |
| 237 | + NotImplementedError |
| 238 | + when non-relative pattern is passed in |
237 | 239 |
|
238 | 240 | Warnings |
239 | 241 | -------- |
240 | 242 | This method follows symlinks by default |
241 | 243 | """ |
242 | 244 | if not self.is_dir(): |
243 | 245 | raise FileNotFoundError(f"Directory {self} does not exist.") |
244 | | - |
245 | | - if pattern.startswith("**"): |
| 246 | + if pattern.startswith("/"): |
| 247 | + raise NotImplementedError("Non-relative patterns are unsupported") |
| 248 | + |
| 249 | + # first count shell pattern symbols if more than two are present, |
| 250 | + # search must be recursive |
| 251 | + pattern_count = sum([ |
| 252 | + pattern.count("*"), |
| 253 | + pattern.count("?"), |
| 254 | + min((pattern.count("["), pattern.count("]"))) |
| 255 | + ]) |
| 256 | + |
| 257 | + if pattern_count >= 2: |
246 | 258 | recursive = True |
247 | 259 | else: |
248 | 260 | recursive = False |
249 | 261 |
|
250 | | - pattern = glob2re(pattern) |
| 262 | + # split pattern to parts, so we can easily match at each subdir level |
| 263 | + parts: Deque[str] = deque(SSHPath(self.c, pattern).parts) |
251 | 264 |
|
| 265 | + # append to origin path that parts of pattern that do not contain any |
| 266 | + # wildcards, so we search as minimal number of sub-directories as |
| 267 | + # possible |
| 268 | + while True: |
| 269 | + p = parts.popleft() |
| 270 | + if "*" in p or "?" in p or ("[" in p and "]" in p): |
| 271 | + parts.appendleft(p) |
| 272 | + break |
| 273 | + else: |
| 274 | + self /= p |
| 275 | + |
| 276 | + # precompute number of origin path parts and pattern parts for speed |
| 277 | + origin_parts = len(self.parts) |
| 278 | + pattern_parts = len(parts) - 1 |
252 | 279 | for root, dirs, files in self.c.os.walk(self, followlinks=True): |
253 | 280 |
|
254 | | - for path in dirs + files: |
255 | | - path = join(root, path) |
256 | | - if re.search(pattern, f"/{path}", re.I): |
257 | | - yield SSHPath(self.c, path) |
| 281 | + # compute number of actual root path parts |
| 282 | + root_parts = len(SSHPath(self.c, root).parts) |
| 283 | + # the difference determines which path of pattern to use, this is |
| 284 | + # because walk traverses directories "depth-first" |
| 285 | + idx = root_parts - origin_parts |
| 286 | + |
| 287 | + # if we do not have the last part we are interested only in |
| 288 | + # directories because we need to get deeper in to the directory |
| 289 | + # structure |
| 290 | + if idx < pattern_parts: |
| 291 | + pattern = parts[idx] |
| 292 | + |
| 293 | + # now get directories that match the pattern and delete the |
| 294 | + # others, tish takes advantage of list mutability - the next |
| 295 | + # seach paths will be built by walk based on already filtered |
| 296 | + # directories list |
| 297 | + indices = [] |
| 298 | + for i, d in enumerate(dirs): |
| 299 | + if not fnmatch(d, pattern): |
| 300 | + indices.append(i) |
| 301 | + |
| 302 | + for index in sorted(indices, reverse=True): |
| 303 | + del dirs[index] |
| 304 | + |
| 305 | + elif idx >= pattern_parts: |
| 306 | + pattern = parts[-1] |
| 307 | + r = SSHPath(self.c, root) |
| 308 | + for path in dirs + files: |
| 309 | + if fnmatch(path, pattern): |
| 310 | + yield r / path |
258 | 311 |
|
259 | 312 | if not recursive: |
260 | 313 | break |
261 | 314 |
|
| 315 | + def group(self) -> str: |
| 316 | + """Return file group. |
| 317 | +
|
| 318 | + Returns |
| 319 | + ------- |
| 320 | + str |
| 321 | + string with group name |
| 322 | +
|
| 323 | + Raises |
| 324 | + ------ |
| 325 | + NotImplementedError |
| 326 | + when used on windows host |
| 327 | + """ |
| 328 | + if self.c.os.name == "nt": |
| 329 | + raise NotImplementedError("This is implemented only for posix " |
| 330 | + "type systems") |
| 331 | + else: |
| 332 | + cmd = ["stat", "-c", "'%G'", str(self)] |
| 333 | + group = self.c.subprocess.run(cmd, suppress_out=True, quiet=True, |
| 334 | + capture_output=True, |
| 335 | + encoding="utf-8").stdout |
| 336 | + |
| 337 | + return group |
| 338 | + |
262 | 339 | def is_dir(self) -> bool: |
263 | 340 | """Check if path points to directory. |
264 | 341 |
|
@@ -404,6 +481,30 @@ def open(self, mode: str = "r", buffering: int = -1, # type: ignore |
404 | 481 | self.c.builtins.open(self, mode=mode, bufsize=buffering, |
405 | 482 | encoding=encoding) |
406 | 483 |
|
| 484 | + def owner(self): |
| 485 | + """Return file owner. |
| 486 | +
|
| 487 | + Returns |
| 488 | + ------- |
| 489 | + str |
| 490 | + string with owner name |
| 491 | +
|
| 492 | + Raises |
| 493 | + ------ |
| 494 | + NotImplementedError |
| 495 | + when used on windows host |
| 496 | + """ |
| 497 | + if self.c.os.name == "nt": |
| 498 | + raise NotImplementedError("This is implemented only for posix " |
| 499 | + "type systems") |
| 500 | + else: |
| 501 | + cmd = ["getent", "passwd", self.stat().st_uid, "|", "cut", "-d:", "-f1"] |
| 502 | + owner = self.c.subprocess.run(cmd, suppress_out=True, quiet=True, |
| 503 | + capture_output=True, |
| 504 | + encoding="utf-8").stdout |
| 505 | + |
| 506 | + return owner |
| 507 | + |
407 | 508 | def read_bytes(self) -> bytes: |
408 | 509 | """Read contents of a file as bytes. |
409 | 510 |
|
@@ -541,6 +642,10 @@ def symlink_to(self, target: "_SPATH", target_is_directory: bool = False): |
541 | 642 | target path to which symlink will point |
542 | 643 | target_is_directory: bool |
543 | 644 | this parameter is ignored |
| 645 | +
|
| 646 | + Warnings |
| 647 | + -------- |
| 648 | + `target_is_directory` parameter is ignored |
544 | 649 | """ |
545 | 650 | self.c.sftp.symlink(self._2str, fspath(target)) |
546 | 651 |
|
@@ -632,14 +737,8 @@ def write_text(self, data, encoding: Optional[str] = None, |
632 | 737 | f.write(data) |
633 | 738 |
|
634 | 739 | # ! NOT IMPLEMENTED |
635 | | - def group(self): |
636 | | - raise NotImplementedError |
637 | | - |
638 | 740 | def link_to(self, target): |
639 | 741 | raise NotImplementedError |
640 | 742 |
|
641 | | - def owner(self): |
642 | | - raise NotImplementedError |
643 | | - |
644 | 743 | def is_mount(self): |
645 | 744 | raise NotImplementedError |
0 commit comments