1# -*- test-case-name: twisted.test.test_paths -*- 2# Copyright (c) Twisted Matrix Laboratories. 3# See LICENSE for details. 4 5""" 6Object-oriented filesystem path representation. 7""" 8 9 10import base64 11import errno 12import os 13import sys 14from os import listdir, stat, utime 15from os.path import ( 16 abspath, 17 basename, 18 dirname, 19 exists, 20 isabs, 21 join as joinpath, 22 normpath, 23 splitext, 24) 25from stat import ( 26 S_IMODE, 27 S_IRGRP, 28 S_IROTH, 29 S_IRUSR, 30 S_ISBLK, 31 S_ISDIR, 32 S_ISREG, 33 S_ISSOCK, 34 S_IWGRP, 35 S_IWOTH, 36 S_IWUSR, 37 S_IXGRP, 38 S_IXOTH, 39 S_IXUSR, 40) 41from typing import IO, Union, cast 42 43from zope.interface import Attribute, Interface, implementer 44 45from twisted.python.compat import cmp, comparable 46from twisted.python.runtime import platform 47from twisted.python.util import FancyEqMixin 48from twisted.python.win32 import ( 49 ERROR_DIRECTORY, 50 ERROR_FILE_NOT_FOUND, 51 ERROR_INVALID_NAME, 52 ERROR_PATH_NOT_FOUND, 53 O_BINARY, 54) 55 56# Please keep this as light as possible on other Twisted imports; many, many 57# things import this module, and it would be good if it could easily be 58# modified for inclusion in the standard library. --glyph 59 60 61_CREATE_FLAGS = os.O_EXCL | os.O_CREAT | os.O_RDWR | O_BINARY 62 63 64def _stub_islink(path): 65 """ 66 Always return C{False} if the operating system does not support symlinks. 67 68 @param path: A path string. 69 @type path: L{str} 70 71 @return: C{False} 72 @rtype: L{bool} 73 """ 74 return False 75 76 77islink = getattr(os.path, "islink", _stub_islink) 78randomBytes = os.urandom 79armor = base64.urlsafe_b64encode 80 81 82class IFilePath(Interface): 83 """ 84 File path object. 85 86 A file path represents a location for a file-like-object and can be 87 organized into a hierarchy; a file path can can children which are 88 themselves file paths. 89 90 A file path has a name which unique identifies it in the context of its 91 parent (if it has one); a file path can not have two children with the same 92 name. This name is referred to as the file path's "base name". 93 94 A series of such names can be used to locate nested children of a file 95 path; such a series is referred to as the child's "path", relative to the 96 parent. In this case, each name in the path is referred to as a "path 97 segment"; the child's base name is the segment in the path. 98 99 When representing a file path as a string, a "path separator" is used to 100 delimit the path segments within the string. For a file system path, that 101 would be C{os.sep}. 102 103 Note that the values of child names may be restricted. For example, a file 104 system path will not allow the use of the path separator in a name, and 105 certain names (e.g. C{"."} and C{".."}) may be reserved or have special 106 meanings. 107 108 @since: 12.1 109 """ 110 111 sep = Attribute("The path separator to use in string representations") 112 113 def child(name): 114 """ 115 Obtain a direct child of this file path. The child may or may not 116 exist. 117 118 @param name: the name of a child of this path. C{name} must be a direct 119 child of this path and may not contain a path separator. 120 @return: the child of this path with the given C{name}. 121 @raise InsecurePath: if C{name} describes a file path that is not a 122 direct child of this file path. 123 """ 124 125 def open(mode="r"): 126 """ 127 Opens this file path with the given mode. 128 129 @return: a file-like object. 130 @raise Exception: if this file path cannot be opened. 131 """ 132 133 def changed(): 134 """ 135 Clear any cached information about the state of this path on disk. 136 """ 137 138 def getsize(): 139 """ 140 Retrieve the size of this file in bytes. 141 142 @return: the size of the file at this file path in bytes. 143 @raise Exception: if the size cannot be obtained. 144 """ 145 146 def getModificationTime(): 147 """ 148 Retrieve the time of last access from this file. 149 150 @return: a number of seconds from the epoch. 151 @rtype: L{float} 152 """ 153 154 def getStatusChangeTime(): 155 """ 156 Retrieve the time of the last status change for this file. 157 158 @return: a number of seconds from the epoch. 159 @rtype: L{float} 160 """ 161 162 def getAccessTime(): 163 """ 164 Retrieve the time that this file was last accessed. 165 166 @return: a number of seconds from the epoch. 167 @rtype: L{float} 168 """ 169 170 def exists(): 171 """ 172 Check if this file path exists. 173 174 @return: C{True} if the file at this file path exists, C{False} 175 otherwise. 176 @rtype: L{bool} 177 """ 178 179 def isdir(): 180 """ 181 Check if this file path refers to a directory. 182 183 @return: C{True} if the file at this file path is a directory, C{False} 184 otherwise. 185 """ 186 187 def isfile(): 188 """ 189 Check if this file path refers to a regular file. 190 191 @return: C{True} if the file at this file path is a regular file, 192 C{False} otherwise. 193 """ 194 195 def children(): 196 """ 197 List the children of this path object. 198 199 @return: a sequence of the children of the directory at this file path. 200 @raise Exception: if the file at this file path is not a directory. 201 """ 202 203 def basename(): 204 """ 205 Retrieve the final component of the file path's path (everything 206 after the final path separator). 207 208 @return: the base name of this file path. 209 @rtype: L{str} 210 """ 211 212 def parent(): 213 """ 214 A file path for the directory containing the file at this file path. 215 """ 216 217 def sibling(name): 218 """ 219 A file path for the directory containing the file at this file path. 220 221 @param name: the name of a sibling of this path. C{name} must be a 222 direct sibling of this path and may not contain a path separator. 223 224 @return: a sibling file path of this one. 225 """ 226 227 228class InsecurePath(Exception): 229 """ 230 Error that is raised when the path provided to L{FilePath} is invalid. 231 """ 232 233 234class LinkError(Exception): 235 """ 236 An error with symlinks - either that there are cyclical symlinks or that 237 symlink are not supported on this platform. 238 """ 239 240 241class UnlistableError(OSError): 242 """ 243 An exception which is used to distinguish between errors which mean 'this 244 is not a directory you can list' and other, more catastrophic errors. 245 246 This error will try to look as much like the original error as possible, 247 while still being catchable as an independent type. 248 249 @ivar originalException: the actual original exception instance. 250 """ 251 252 def __init__(self, originalException: OSError): 253 """ 254 Create an UnlistableError exception. 255 256 @param originalException: an instance of OSError. 257 """ 258 self.__dict__.update(originalException.__dict__) 259 self.originalException = originalException 260 261 262def _secureEnoughString(path): 263 """ 264 Compute a string usable as a new, temporary filename. 265 266 @param path: The path that the new temporary filename should be able to be 267 concatenated with. 268 269 @return: A pseudorandom, 16 byte string for use in secure filenames. 270 @rtype: the type of C{path} 271 """ 272 secureishString = armor(randomBytes(16))[:16] 273 return _coerceToFilesystemEncoding(path, secureishString) 274 275 276class AbstractFilePath: 277 """ 278 Abstract implementation of an L{IFilePath}; must be completed by a 279 subclass. 280 281 This class primarily exists to provide common implementations of certain 282 methods in L{IFilePath}. It is *not* a required parent class for 283 L{IFilePath} implementations, just a useful starting point. 284 """ 285 286 def getContent(self): 287 """ 288 Retrieve the contents of the file at this path. 289 290 @return: the contents of the file 291 @rtype: L{bytes} 292 """ 293 with self.open() as fp: 294 return fp.read() 295 296 def parents(self): 297 """ 298 Retrieve an iterator of all the ancestors of this path. 299 300 @return: an iterator of all the ancestors of this path, from the most 301 recent (its immediate parent) to the root of its filesystem. 302 """ 303 path = self 304 parent = path.parent() 305 # root.parent() == root, so this means "are we the root" 306 while path != parent: 307 yield parent 308 path = parent 309 parent = parent.parent() 310 311 def children(self): 312 """ 313 List the children of this path object. 314 315 @raise OSError: If an error occurs while listing the directory. If the 316 error is 'serious', meaning that the operation failed due to an access 317 violation, exhaustion of some kind of resource (file descriptors or 318 memory), OSError or a platform-specific variant will be raised. 319 320 @raise UnlistableError: If the inability to list the directory is due 321 to this path not existing or not being a directory, the more specific 322 OSError subclass L{UnlistableError} is raised instead. 323 324 @return: an iterable of all currently-existing children of this object. 325 """ 326 try: 327 subnames = self.listdir() 328 except OSError as ose: 329 # Under Python 3.3 and higher on Windows, WindowsError is an 330 # alias for OSError. OSError has a winerror attribute and an 331 # errno attribute. 332 # 333 # The winerror attribute is bound to the Windows error code while 334 # the errno attribute is bound to a translation of that code to a 335 # perhaps equivalent POSIX error number. 336 # 337 # For further details, refer to: 338 # https://docs.python.org/3/library/exceptions.html#OSError 339 if getattr(ose, "winerror", None) in ( 340 ERROR_PATH_NOT_FOUND, 341 ERROR_FILE_NOT_FOUND, 342 ERROR_INVALID_NAME, 343 ERROR_DIRECTORY, 344 ): 345 raise UnlistableError(ose) 346 if ose.errno in (errno.ENOENT, errno.ENOTDIR): 347 raise UnlistableError(ose) 348 # Other possible errors here, according to linux manpages: 349 # EACCES, EMIFLE, ENFILE, ENOMEM. None of these seem like the 350 # sort of thing which should be handled normally. -glyph 351 raise 352 return [self.child(name) for name in subnames] 353 354 def walk(self, descend=None): 355 """ 356 Yield myself, then each of my children, and each of those children's 357 children in turn. 358 359 The optional argument C{descend} is a predicate that takes a FilePath, 360 and determines whether or not that FilePath is traversed/descended 361 into. It will be called with each path for which C{isdir} returns 362 C{True}. If C{descend} is not specified, all directories will be 363 traversed (including symbolic links which refer to directories). 364 365 @param descend: A one-argument callable that will return True for 366 FilePaths that should be traversed, False otherwise. 367 368 @return: a generator yielding FilePath-like objects. 369 """ 370 yield self 371 if self.isdir(): 372 for c in self.children(): 373 # we should first see if it's what we want, then we 374 # can walk through the directory 375 if descend is None or descend(c): 376 for subc in c.walk(descend): 377 if os.path.realpath(self.path).startswith( 378 os.path.realpath(subc.path) 379 ): 380 raise LinkError("Cycle in file graph.") 381 yield subc 382 else: 383 yield c 384 385 def sibling(self, path): 386 """ 387 Return a L{FilePath} with the same directory as this instance but with 388 a basename of C{path}. 389 390 @param path: The basename of the L{FilePath} to return. 391 @type path: L{str} 392 393 @return: The sibling path. 394 @rtype: L{FilePath} 395 """ 396 return self.parent().child(path) 397 398 def descendant(self, segments): 399 """ 400 Retrieve a child or child's child of this path. 401 402 @param segments: A sequence of path segments as L{str} instances. 403 404 @return: A L{FilePath} constructed by looking up the C{segments[0]} 405 child of this path, the C{segments[1]} child of that path, and so 406 on. 407 408 @since: 10.2 409 """ 410 path = self 411 for name in segments: 412 path = path.child(name) 413 return path 414 415 def segmentsFrom(self, ancestor): 416 """ 417 Return a list of segments between a child and its ancestor. 418 419 For example, in the case of a path X representing /a/b/c/d and a path Y 420 representing /a/b, C{Y.segmentsFrom(X)} will return C{['c', 421 'd']}. 422 423 @param ancestor: an instance of the same class as self, ostensibly an 424 ancestor of self. 425 426 @raise ValueError: If the C{ancestor} parameter is not actually an 427 ancestor, i.e. a path for /x/y/z is passed as an ancestor for /a/b/c/d. 428 429 @return: a list of strs 430 """ 431 # this might be an unnecessarily inefficient implementation but it will 432 # work on win32 and for zipfiles; later I will deterimine if the 433 # obvious fast implemenation does the right thing too 434 f = self 435 p = f.parent() 436 segments = [] 437 while f != ancestor and p != f: 438 segments[0:0] = [f.basename()] 439 f = p 440 p = p.parent() 441 if f == ancestor and segments: 442 return segments 443 raise ValueError(f"{ancestor!r} not parent of {self!r}") 444 445 # new in 8.0 446 def __hash__(self): 447 """ 448 Hash the same as another L{FilePath} with the same path as mine. 449 """ 450 return hash((self.__class__, self.path)) 451 452 # pending deprecation in 8.0 453 def getmtime(self): 454 """ 455 Deprecated. Use getModificationTime instead. 456 """ 457 return int(self.getModificationTime()) 458 459 def getatime(self): 460 """ 461 Deprecated. Use getAccessTime instead. 462 """ 463 return int(self.getAccessTime()) 464 465 def getctime(self): 466 """ 467 Deprecated. Use getStatusChangeTime instead. 468 """ 469 return int(self.getStatusChangeTime()) 470 471 472class RWX(FancyEqMixin): 473 """ 474 A class representing read/write/execute permissions for a single user 475 category (i.e. user/owner, group, or other/world). Instantiate with 476 three boolean values: readable? writable? executable?. 477 478 @type read: C{bool} 479 @ivar read: Whether permission to read is given 480 481 @type write: C{bool} 482 @ivar write: Whether permission to write is given 483 484 @type execute: C{bool} 485 @ivar execute: Whether permission to execute is given 486 487 @since: 11.1 488 """ 489 490 compareAttributes = ("read", "write", "execute") 491 492 def __init__(self, readable, writable, executable): 493 self.read = readable 494 self.write = writable 495 self.execute = executable 496 497 def __repr__(self) -> str: 498 return "RWX(read={}, write={}, execute={})".format( 499 self.read, 500 self.write, 501 self.execute, 502 ) 503 504 def shorthand(self): 505 """ 506 Returns a short string representing the permission bits. Looks like 507 part of what is printed by command line utilities such as 'ls -l' 508 (e.g. 'rwx') 509 510 @return: The shorthand string. 511 @rtype: L{str} 512 """ 513 returnval = ["r", "w", "x"] 514 i = 0 515 for val in (self.read, self.write, self.execute): 516 if not val: 517 returnval[i] = "-" 518 i += 1 519 return "".join(returnval) 520 521 522class Permissions(FancyEqMixin): 523 """ 524 A class representing read/write/execute permissions. Instantiate with any 525 portion of the file's mode that includes the permission bits. 526 527 @type user: L{RWX} 528 @ivar user: User/Owner permissions 529 530 @type group: L{RWX} 531 @ivar group: Group permissions 532 533 @type other: L{RWX} 534 @ivar other: Other/World permissions 535 536 @since: 11.1 537 """ 538 539 compareAttributes = ("user", "group", "other") 540 541 def __init__(self, statModeInt): 542 self.user, self.group, self.other = ( 543 RWX(*(statModeInt & bit > 0 for bit in bitGroup)) 544 for bitGroup in [ 545 [S_IRUSR, S_IWUSR, S_IXUSR], 546 [S_IRGRP, S_IWGRP, S_IXGRP], 547 [S_IROTH, S_IWOTH, S_IXOTH], 548 ] 549 ) 550 551 def __repr__(self) -> str: 552 return f"[{str(self.user)} | {str(self.group)} | {str(self.other)}]" 553 554 def shorthand(self): 555 """ 556 Returns a short string representing the permission bits. Looks like 557 what is printed by command line utilities such as 'ls -l' 558 (e.g. 'rwx-wx--x') 559 560 @return: The shorthand string. 561 @rtype: L{str} 562 """ 563 return "".join([x.shorthand() for x in (self.user, self.group, self.other)]) 564 565 566def _asFilesystemBytes(path: Union[bytes, str], encoding: str = "") -> bytes: 567 """ 568 Return C{path} as a string of L{bytes} suitable for use on this system's 569 filesystem. 570 571 @param path: The path to be made suitable. 572 @type path: L{bytes} or L{unicode} 573 @param encoding: The encoding to use if coercing to L{bytes}. If none is 574 given, L{sys.getfilesystemencoding} is used. 575 576 @return: L{bytes} 577 """ 578 if isinstance(path, bytes): 579 return path 580 else: 581 if not encoding: 582 encoding = sys.getfilesystemencoding() 583 return path.encode(encoding) 584 585 586def _asFilesystemText(path, encoding=None): 587 """ 588 Return C{path} as a string of L{unicode} suitable for use on this system's 589 filesystem. 590 591 @param path: The path to be made suitable. 592 @type path: L{bytes} or L{unicode} 593 594 @param encoding: The encoding to use if coercing to L{unicode}. If none 595 is given, L{sys.getfilesystemencoding} is used. 596 597 @return: L{unicode} 598 """ 599 if type(path) == str: 600 return path 601 else: 602 if encoding is None: 603 encoding = sys.getfilesystemencoding() 604 return path.decode(encoding) 605 606 607def _coerceToFilesystemEncoding(path, newpath, encoding=None): 608 """ 609 Return a C{newpath} that is suitable for joining to C{path}. 610 611 @param path: The path that it should be suitable for joining to. 612 @param newpath: The new portion of the path to be coerced if needed. 613 @param encoding: If coerced, the encoding that will be used. 614 """ 615 if type(path) == bytes: 616 return _asFilesystemBytes(newpath, encoding=encoding) 617 else: 618 return _asFilesystemText(newpath, encoding=encoding) 619 620 621@comparable 622@implementer(IFilePath) 623class FilePath(AbstractFilePath): 624 """ 625 I am a path on the filesystem that only permits 'downwards' access. 626 627 Instantiate me with a pathname (for example, 628 FilePath('/home/myuser/public_html')) and I will attempt to only provide 629 access to files which reside inside that path. I may be a path to a file, 630 a directory, or a file which does not exist. 631 632 The correct way to use me is to instantiate me, and then do ALL filesystem 633 access through me. In other words, do not import the 'os' module; if you 634 need to open a file, call my 'open' method. If you need to list a 635 directory, call my 'path' method. 636 637 Even if you pass me a relative path, I will convert that to an absolute 638 path internally. 639 640 The type of C{path} when instantiating decides the mode of the L{FilePath}. 641 That is, C{FilePath(b"/")} will return a L{bytes} mode L{FilePath}, and 642 C{FilePath(u"/")} will return a L{unicode} mode L{FilePath}. 643 C{FilePath("/")} will return a L{bytes} mode L{FilePath} on Python 2, and a 644 L{unicode} mode L{FilePath} on Python 3. 645 646 Methods that return a new L{FilePath} use the type of the given subpath to 647 decide its mode. For example, C{FilePath(b"/").child(u"tmp")} will return a 648 L{unicode} mode L{FilePath}. 649 650 @type alwaysCreate: L{bool} 651 @ivar alwaysCreate: When opening this file, only succeed if the file does 652 not already exist. 653 654 @ivar path: The path from which 'downward' traversal is permitted. 655 """ 656 657 _statinfo = None 658 path: Union[bytes, str] = None # type: ignore[assignment] 659 660 def __init__(self, path, alwaysCreate=False): 661 """ 662 Convert a path string to an absolute path if necessary and initialize 663 the L{FilePath} with the result. 664 """ 665 self.path = abspath(path) 666 self.alwaysCreate = alwaysCreate 667 668 def __getstate__(self): 669 """ 670 Support serialization by discarding cached L{os.stat} results and 671 returning everything else. 672 """ 673 d = self.__dict__.copy() 674 if "_statinfo" in d: 675 del d["_statinfo"] 676 return d 677 678 @property 679 def sep(self): 680 """ 681 Return a filesystem separator. 682 683 @return: The native filesystem separator. 684 @returntype: The same type as C{self.path}. 685 """ 686 return _coerceToFilesystemEncoding(self.path, os.sep) 687 688 def _asBytesPath(self, encoding=None): 689 """ 690 Return the path of this L{FilePath} as bytes. 691 692 @param encoding: The encoding to use if coercing to L{bytes}. If none is 693 given, L{sys.getfilesystemencoding} is used. 694 695 @return: L{bytes} 696 """ 697 return _asFilesystemBytes(self.path, encoding=encoding) 698 699 def _asTextPath(self, encoding=None): 700 """ 701 Return the path of this L{FilePath} as text. 702 703 @param encoding: The encoding to use if coercing to L{unicode}. If none 704 is given, L{sys.getfilesystemencoding} is used. 705 706 @return: L{unicode} 707 """ 708 return _asFilesystemText(self.path, encoding=encoding) 709 710 def asBytesMode(self, encoding=None): 711 """ 712 Return this L{FilePath} in L{bytes}-mode. 713 714 @param encoding: The encoding to use if coercing to L{bytes}. If none is 715 given, L{sys.getfilesystemencoding} is used. 716 717 @return: L{bytes} mode L{FilePath} 718 """ 719 if type(self.path) == str: 720 return self.clonePath(self._asBytesPath(encoding=encoding)) 721 return self 722 723 def asTextMode(self, encoding=None): 724 """ 725 Return this L{FilePath} in L{unicode}-mode. 726 727 @param encoding: The encoding to use if coercing to L{unicode}. If none 728 is given, L{sys.getfilesystemencoding} is used. 729 730 @return: L{unicode} mode L{FilePath} 731 """ 732 if type(self.path) == bytes: 733 return self.clonePath(self._asTextPath(encoding=encoding)) 734 return self 735 736 def _getPathAsSameTypeAs(self, pattern): 737 """ 738 If C{pattern} is C{bytes}, return L{FilePath.path} as L{bytes}. 739 Otherwise, return L{FilePath.path} as L{unicode}. 740 741 @param pattern: The new element of the path that L{FilePath.path} may 742 need to be coerced to match. 743 """ 744 if type(pattern) == bytes: 745 return self._asBytesPath() 746 else: 747 return self._asTextPath() 748 749 def child(self, path): 750 """ 751 Create and return a new L{FilePath} representing a path contained by 752 C{self}. 753 754 @param path: The base name of the new L{FilePath}. If this contains 755 directory separators or parent references it will be rejected. 756 @type path: L{bytes} or L{unicode} 757 758 @raise InsecurePath: If the result of combining this path with C{path} 759 would result in a path which is not a direct child of this path. 760 761 @return: The child path. 762 @rtype: L{FilePath} with a mode equal to the type of C{path}. 763 """ 764 colon = _coerceToFilesystemEncoding(path, ":") 765 sep = _coerceToFilesystemEncoding(path, os.sep) 766 ourPath = self._getPathAsSameTypeAs(path) 767 768 if platform.isWindows() and path.count(colon): 769 # Catch paths like C:blah that don't have a slash 770 raise InsecurePath(f"{path!r} contains a colon.") 771 772 norm = normpath(path) 773 if sep in norm: 774 raise InsecurePath(f"{path!r} contains one or more directory separators") 775 776 newpath = abspath(joinpath(ourPath, norm)) 777 if not newpath.startswith(ourPath): 778 raise InsecurePath(f"{newpath!r} is not a child of {ourPath}") 779 return self.clonePath(newpath) 780 781 def preauthChild(self, path): 782 """ 783 Use me if C{path} might have slashes in it, but you know they're safe. 784 785 @param path: A relative path (ie, a path not starting with C{"/"}) 786 which will be interpreted as a child or descendant of this path. 787 @type path: L{bytes} or L{unicode} 788 789 @return: The child path. 790 @rtype: L{FilePath} with a mode equal to the type of C{path}. 791 """ 792 ourPath = self._getPathAsSameTypeAs(path) 793 794 newpath = abspath(joinpath(ourPath, normpath(path))) 795 if not newpath.startswith(ourPath): 796 raise InsecurePath(f"{newpath} is not a child of {ourPath}") 797 return self.clonePath(newpath) 798 799 def childSearchPreauth(self, *paths): 800 """ 801 Return my first existing child with a name in C{paths}. 802 803 C{paths} is expected to be a list of *pre-secured* path fragments; 804 in most cases this will be specified by a system administrator and not 805 an arbitrary user. 806 807 If no appropriately-named children exist, this will return L{None}. 808 809 @return: L{None} or the child path. 810 @rtype: L{None} or L{FilePath} 811 """ 812 for child in paths: 813 p = self._getPathAsSameTypeAs(child) 814 jp = joinpath(p, child) 815 if exists(jp): 816 return self.clonePath(jp) 817 818 def siblingExtensionSearch(self, *exts): 819 """ 820 Attempt to return a path with my name, given multiple possible 821 extensions. 822 823 Each extension in C{exts} will be tested and the first path which 824 exists will be returned. If no path exists, L{None} will be returned. 825 If C{''} is in C{exts}, then if the file referred to by this path 826 exists, C{self} will be returned. 827 828 The extension '*' has a magic meaning, which means "any path that 829 begins with C{self.path + '.'} is acceptable". 830 """ 831 for ext in exts: 832 if not ext and self.exists(): 833 return self 834 835 p = self._getPathAsSameTypeAs(ext) 836 star = _coerceToFilesystemEncoding(ext, "*") 837 dot = _coerceToFilesystemEncoding(ext, ".") 838 839 if ext == star: 840 basedot = basename(p) + dot 841 for fn in listdir(dirname(p)): 842 if fn.startswith(basedot): 843 return self.clonePath(joinpath(dirname(p), fn)) 844 p2 = p + ext 845 if exists(p2): 846 return self.clonePath(p2) 847 848 def realpath(self): 849 """ 850 Returns the absolute target as a L{FilePath} if self is a link, self 851 otherwise. 852 853 The absolute link is the ultimate file or directory the 854 link refers to (for instance, if the link refers to another link, and 855 another...). If the filesystem does not support symlinks, or 856 if the link is cyclical, raises a L{LinkError}. 857 858 Behaves like L{os.path.realpath} in that it does not resolve link 859 names in the middle (ex. /x/y/z, y is a link to w - realpath on z 860 will return /x/y/z, not /x/w/z). 861 862 @return: L{FilePath} of the target path. 863 @rtype: L{FilePath} 864 @raises LinkError: if links are not supported or links are cyclical. 865 """ 866 if self.islink(): 867 result = os.path.realpath(self.path) 868 if result == self.path: 869 raise LinkError("Cyclical link - will loop forever") 870 return self.clonePath(result) 871 return self 872 873 def siblingExtension(self, ext): 874 """ 875 Attempt to return a path with my name, given the extension at C{ext}. 876 877 @param ext: File-extension to search for. 878 @type ext: L{bytes} or L{unicode} 879 880 @return: The sibling path. 881 @rtype: L{FilePath} with the same mode as the type of C{ext}. 882 """ 883 ourPath = self._getPathAsSameTypeAs(ext) 884 return self.clonePath(ourPath + ext) 885 886 def linkTo(self, linkFilePath): 887 """ 888 Creates a symlink to self to at the path in the L{FilePath} 889 C{linkFilePath}. 890 891 Only works on posix systems due to its dependence on 892 L{os.symlink}. Propagates L{OSError}s up from L{os.symlink} if 893 C{linkFilePath.parent()} does not exist, or C{linkFilePath} already 894 exists. 895 896 @param linkFilePath: a FilePath representing the link to be created. 897 @type linkFilePath: L{FilePath} 898 """ 899 os.symlink(self.path, linkFilePath.path) 900 901 def open(self, mode: str = "r") -> IO[bytes]: 902 """ 903 Open this file using C{mode} or for writing if C{alwaysCreate} is 904 C{True}. 905 906 In all cases the file is opened in binary mode, so it is not necessary 907 to include C{"b"} in C{mode}. 908 909 @param mode: The mode to open the file in. Default is C{"r"}. 910 @raises AssertionError: If C{"a"} is included in the mode and 911 C{alwaysCreate} is C{True}. 912 @return: An open file-like object. 913 """ 914 if self.alwaysCreate: 915 assert "a" not in mode, ( 916 "Appending not supported when " "alwaysCreate == True" 917 ) 918 return self.create() 919 # Make sure we open with exactly one "b" in the mode. 920 mode = mode.replace("b", "") 921 return open(self.path, mode + "b") 922 923 # stat methods below 924 925 def restat(self, reraise=True): 926 """ 927 Re-calculate cached effects of 'stat'. To refresh information on this 928 path after you know the filesystem may have changed, call this method. 929 930 @param reraise: a boolean. If true, re-raise exceptions from 931 L{os.stat}; otherwise, mark this path as not existing, and remove 932 any cached stat information. 933 934 @raise Exception: If C{reraise} is C{True} and an exception occurs 935 while reloading metadata. 936 """ 937 try: 938 self._statinfo = stat(self.path) 939 except OSError: 940 self._statinfo = 0 941 if reraise: 942 raise 943 944 def changed(self): 945 """ 946 Clear any cached information about the state of this path on disk. 947 948 @since: 10.1.0 949 """ 950 self._statinfo = None 951 952 def chmod(self, mode): 953 """ 954 Changes the permissions on self, if possible. Propagates errors from 955 L{os.chmod} up. 956 957 @param mode: integer representing the new permissions desired (same as 958 the command line chmod) 959 @type mode: L{int} 960 """ 961 os.chmod(self.path, mode) 962 963 def getsize(self): 964 """ 965 Retrieve the size of this file in bytes. 966 967 @return: The size of the file at this file path in bytes. 968 @raise Exception: if the size cannot be obtained. 969 @rtype: L{int} 970 """ 971 st = self._statinfo 972 if not st: 973 self.restat() 974 st = self._statinfo 975 return st.st_size 976 977 def getModificationTime(self): 978 """ 979 Retrieve the time of last access from this file. 980 981 @return: a number of seconds from the epoch. 982 @rtype: L{float} 983 """ 984 st = self._statinfo 985 if not st: 986 self.restat() 987 st = self._statinfo 988 return float(st.st_mtime) 989 990 def getStatusChangeTime(self): 991 """ 992 Retrieve the time of the last status change for this file. 993 994 @return: a number of seconds from the epoch. 995 @rtype: L{float} 996 """ 997 st = self._statinfo 998 if not st: 999 self.restat() 1000 st = self._statinfo 1001 return float(st.st_ctime) 1002 1003 def getAccessTime(self): 1004 """ 1005 Retrieve the time that this file was last accessed. 1006 1007 @return: a number of seconds from the epoch. 1008 @rtype: L{float} 1009 """ 1010 st = self._statinfo 1011 if not st: 1012 self.restat() 1013 st = self._statinfo 1014 return float(st.st_atime) 1015 1016 def getInodeNumber(self): 1017 """ 1018 Retrieve the file serial number, also called inode number, which 1019 distinguishes this file from all other files on the same device. 1020 1021 @raise NotImplementedError: if the platform is Windows, since the 1022 inode number would be a dummy value for all files in Windows 1023 @return: a number representing the file serial number 1024 @rtype: L{int} 1025 @since: 11.0 1026 """ 1027 if platform.isWindows(): 1028 raise NotImplementedError 1029 1030 st = self._statinfo 1031 if not st: 1032 self.restat() 1033 st = self._statinfo 1034 return st.st_ino 1035 1036 def getDevice(self): 1037 """ 1038 Retrieves the device containing the file. The inode number and device 1039 number together uniquely identify the file, but the device number is 1040 not necessarily consistent across reboots or system crashes. 1041 1042 @raise NotImplementedError: if the platform is Windows, since the 1043 device number would be 0 for all partitions on a Windows platform 1044 1045 @return: a number representing the device 1046 @rtype: L{int} 1047 1048 @since: 11.0 1049 """ 1050 if platform.isWindows(): 1051 raise NotImplementedError 1052 1053 st = self._statinfo 1054 if not st: 1055 self.restat() 1056 st = self._statinfo 1057 return st.st_dev 1058 1059 def getNumberOfHardLinks(self): 1060 """ 1061 Retrieves the number of hard links to the file. 1062 1063 This count keeps track of how many directories have entries for this 1064 file. If the count is ever decremented to zero then the file itself is 1065 discarded as soon as no process still holds it open. Symbolic links 1066 are not counted in the total. 1067 1068 @raise NotImplementedError: if the platform is Windows, since Windows 1069 doesn't maintain a link count for directories, and L{os.stat} does 1070 not set C{st_nlink} on Windows anyway. 1071 @return: the number of hard links to the file 1072 @rtype: L{int} 1073 @since: 11.0 1074 """ 1075 if platform.isWindows(): 1076 raise NotImplementedError 1077 1078 st = self._statinfo 1079 if not st: 1080 self.restat() 1081 st = self._statinfo 1082 return st.st_nlink 1083 1084 def getUserID(self): 1085 """ 1086 Returns the user ID of the file's owner. 1087 1088 @raise NotImplementedError: if the platform is Windows, since the UID 1089 is always 0 on Windows 1090 @return: the user ID of the file's owner 1091 @rtype: L{int} 1092 @since: 11.0 1093 """ 1094 if platform.isWindows(): 1095 raise NotImplementedError 1096 1097 st = self._statinfo 1098 if not st: 1099 self.restat() 1100 st = self._statinfo 1101 return st.st_uid 1102 1103 def getGroupID(self): 1104 """ 1105 Returns the group ID of the file. 1106 1107 @raise NotImplementedError: if the platform is Windows, since the GID 1108 is always 0 on windows 1109 @return: the group ID of the file 1110 @rtype: L{int} 1111 @since: 11.0 1112 """ 1113 if platform.isWindows(): 1114 raise NotImplementedError 1115 1116 st = self._statinfo 1117 if not st: 1118 self.restat() 1119 st = self._statinfo 1120 return st.st_gid 1121 1122 def getPermissions(self): 1123 """ 1124 Returns the permissions of the file. Should also work on Windows, 1125 however, those permissions may not be what is expected in Windows. 1126 1127 @return: the permissions for the file 1128 @rtype: L{Permissions} 1129 @since: 11.1 1130 """ 1131 st = self._statinfo 1132 if not st: 1133 self.restat() 1134 st = self._statinfo 1135 return Permissions(S_IMODE(st.st_mode)) 1136 1137 def exists(self): 1138 """ 1139 Check if this L{FilePath} exists. 1140 1141 @return: C{True} if the stats of C{path} can be retrieved successfully, 1142 C{False} in the other cases. 1143 @rtype: L{bool} 1144 """ 1145 if self._statinfo: 1146 return True 1147 else: 1148 self.restat(False) 1149 if self._statinfo: 1150 return True 1151 else: 1152 return False 1153 1154 def isdir(self): 1155 """ 1156 Check if this L{FilePath} refers to a directory. 1157 1158 @return: C{True} if this L{FilePath} refers to a directory, C{False} 1159 otherwise. 1160 @rtype: L{bool} 1161 """ 1162 st = self._statinfo 1163 if not st: 1164 self.restat(False) 1165 st = self._statinfo 1166 if not st: 1167 return False 1168 return S_ISDIR(st.st_mode) 1169 1170 def isfile(self): 1171 """ 1172 Check if this file path refers to a regular file. 1173 1174 @return: C{True} if this L{FilePath} points to a regular file (not a 1175 directory, socket, named pipe, etc), C{False} otherwise. 1176 @rtype: L{bool} 1177 """ 1178 st = self._statinfo 1179 if not st: 1180 self.restat(False) 1181 st = self._statinfo 1182 if not st: 1183 return False 1184 return S_ISREG(st.st_mode) 1185 1186 def isBlockDevice(self): 1187 """ 1188 Returns whether the underlying path is a block device. 1189 1190 @return: C{True} if it is a block device, C{False} otherwise 1191 @rtype: L{bool} 1192 @since: 11.1 1193 """ 1194 st = self._statinfo 1195 if not st: 1196 self.restat(False) 1197 st = self._statinfo 1198 if not st: 1199 return False 1200 return S_ISBLK(st.st_mode) 1201 1202 def isSocket(self): 1203 """ 1204 Returns whether the underlying path is a socket. 1205 1206 @return: C{True} if it is a socket, C{False} otherwise 1207 @rtype: L{bool} 1208 @since: 11.1 1209 """ 1210 st = self._statinfo 1211 if not st: 1212 self.restat(False) 1213 st = self._statinfo 1214 if not st: 1215 return False 1216 return S_ISSOCK(st.st_mode) 1217 1218 def islink(self): 1219 """ 1220 Check if this L{FilePath} points to a symbolic link. 1221 1222 @return: C{True} if this L{FilePath} points to a symbolic link, 1223 C{False} otherwise. 1224 @rtype: L{bool} 1225 """ 1226 # We can't use cached stat results here, because that is the stat of 1227 # the destination - (see #1773) which in *every case* but this one is 1228 # the right thing to use. We could call lstat here and use that, but 1229 # it seems unlikely we'd actually save any work that way. -glyph 1230 return islink(self.path) 1231 1232 def isabs(self): 1233 """ 1234 Check if this L{FilePath} refers to an absolute path. 1235 1236 This always returns C{True}. 1237 1238 @return: C{True}, always. 1239 @rtype: L{bool} 1240 """ 1241 return isabs(self.path) 1242 1243 def listdir(self): 1244 """ 1245 List the base names of the direct children of this L{FilePath}. 1246 1247 @return: A L{list} of L{bytes}/L{unicode} giving the names of the 1248 contents of the directory this L{FilePath} refers to. These names 1249 are relative to this L{FilePath}. 1250 @rtype: L{list} 1251 1252 @raise OSError: Any exception the platform L{os.listdir} implementation 1253 may raise. 1254 """ 1255 return listdir(self.path) 1256 1257 def splitext(self): 1258 """ 1259 Split the file path into a pair C{(root, ext)} such that 1260 C{root + ext == path}. 1261 1262 @return: Tuple where the first item is the filename and second item is 1263 the file extension. See Python docs for L{os.path.splitext}. 1264 @rtype: L{tuple} 1265 """ 1266 return splitext(self.path) 1267 1268 def __repr__(self) -> str: 1269 return f"FilePath({self.path!r})" 1270 1271 def touch(self): 1272 """ 1273 Updates the access and last modification times of the file at this 1274 file path to the current time. Also creates the file if it does not 1275 already exist. 1276 1277 @raise Exception: if unable to create or modify the last modification 1278 time of the file. 1279 """ 1280 try: 1281 self.open("a").close() 1282 except OSError: 1283 pass 1284 utime(self.path, None) 1285 1286 def remove(self): 1287 """ 1288 Removes the file or directory that is represented by self. If 1289 C{self.path} is a directory, recursively remove all its children 1290 before removing the directory. If it's a file or link, just delete it. 1291 """ 1292 if self.isdir() and not self.islink(): 1293 for child in self.children(): 1294 child.remove() 1295 os.rmdir(self.path) 1296 else: 1297 os.remove(self.path) 1298 self.changed() 1299 1300 def makedirs(self, ignoreExistingDirectory=False): 1301 """ 1302 Create all directories not yet existing in C{path} segments, using 1303 L{os.makedirs}. 1304 1305 @param ignoreExistingDirectory: Don't raise L{OSError} if directory 1306 already exists. 1307 @type ignoreExistingDirectory: L{bool} 1308 1309 @return: L{None} 1310 """ 1311 try: 1312 return os.makedirs(self.path) 1313 except OSError as e: 1314 if not ( 1315 e.errno == errno.EEXIST and ignoreExistingDirectory and self.isdir() 1316 ): 1317 raise 1318 1319 def globChildren(self, pattern): 1320 """ 1321 Assuming I am representing a directory, return a list of FilePaths 1322 representing my children that match the given pattern. 1323 1324 @param pattern: A glob pattern to use to match child paths. 1325 @type pattern: L{unicode} or L{bytes} 1326 1327 @return: A L{list} of matching children. 1328 @rtype: L{list} of L{FilePath}, with the mode of C{pattern}'s type 1329 """ 1330 sep = _coerceToFilesystemEncoding(pattern, os.sep) 1331 ourPath = self._getPathAsSameTypeAs(pattern) 1332 1333 import glob 1334 1335 path = ourPath[-1] == sep and ourPath + pattern or sep.join([ourPath, pattern]) 1336 return [self.clonePath(p) for p in glob.glob(path)] 1337 1338 def basename(self): 1339 """ 1340 Retrieve the final component of the file path's path (everything 1341 after the final path separator). 1342 1343 @return: The final component of the L{FilePath}'s path (Everything 1344 after the final path separator). 1345 @rtype: the same type as this L{FilePath}'s C{path} attribute 1346 """ 1347 return basename(self.path) 1348 1349 def dirname(self): 1350 """ 1351 Retrieve all of the components of the L{FilePath}'s path except the 1352 last one (everything up to the final path separator). 1353 1354 @return: All of the components of the L{FilePath}'s path except the 1355 last one (everything up to the final path separator). 1356 @rtype: the same type as this L{FilePath}'s C{path} attribute 1357 """ 1358 return dirname(self.path) 1359 1360 def parent(self): 1361 """ 1362 A file path for the directory containing the file at this file path. 1363 1364 @return: A L{FilePath} representing the path which directly contains 1365 this L{FilePath}. 1366 @rtype: L{FilePath} 1367 """ 1368 return self.clonePath(self.dirname()) 1369 1370 def setContent(self, content, ext=b".new"): 1371 """ 1372 Replace the file at this path with a new file that contains the given 1373 bytes, trying to avoid data-loss in the meanwhile. 1374 1375 On UNIX-like platforms, this method does its best to ensure that by the 1376 time this method returns, either the old contents I{or} the new 1377 contents of the file will be present at this path for subsequent 1378 readers regardless of premature device removal, program crash, or power 1379 loss, making the following assumptions: 1380 1381 - your filesystem is journaled (i.e. your filesystem will not 1382 I{itself} lose data due to power loss) 1383 1384 - your filesystem's C{rename()} is atomic 1385 1386 - your filesystem will not discard new data while preserving new 1387 metadata (see U{http://mjg59.livejournal.com/108257.html} for 1388 more detail) 1389 1390 On most versions of Windows there is no atomic C{rename()} (see 1391 U{http://bit.ly/win32-overwrite} for more information), so this method 1392 is slightly less helpful. There is a small window where the file at 1393 this path may be deleted before the new file is moved to replace it: 1394 however, the new file will be fully written and flushed beforehand so 1395 in the unlikely event that there is a crash at that point, it should be 1396 possible for the user to manually recover the new version of their 1397 data. In the future, Twisted will support atomic file moves on those 1398 versions of Windows which I{do} support them: see U{Twisted ticket 1399 3004<http://twistedmatrix.com/trac/ticket/3004>}. 1400 1401 This method should be safe for use by multiple concurrent processes, 1402 but note that it is not easy to predict which process's contents will 1403 ultimately end up on disk if they invoke this method at close to the 1404 same time. 1405 1406 @param content: The desired contents of the file at this path. 1407 @type content: L{bytes} 1408 1409 @param ext: An extension to append to the temporary filename used to 1410 store the bytes while they are being written. This can be used to 1411 make sure that temporary files can be identified by their suffix, 1412 for cleanup in case of crashes. 1413 @type ext: L{bytes} 1414 """ 1415 sib = self.temporarySibling(ext) 1416 with sib.open("w") as f: 1417 f.write(content) 1418 if platform.isWindows() and exists(self.path): 1419 os.unlink(self.path) 1420 os.rename(sib.path, self.asBytesMode().path) 1421 1422 def __cmp__(self, other): 1423 if not isinstance(other, FilePath): 1424 return NotImplemented 1425 return cmp(self.path, other.path) 1426 1427 def createDirectory(self): 1428 """ 1429 Create the directory the L{FilePath} refers to. 1430 1431 @see: L{makedirs} 1432 1433 @raise OSError: If the directory cannot be created. 1434 """ 1435 os.mkdir(self.path) 1436 1437 def requireCreate(self, val=1): 1438 """ 1439 Sets the C{alwaysCreate} variable. 1440 1441 @param val: C{True} or C{False}, indicating whether opening this path 1442 will be required to create the file or not. 1443 @type val: L{bool} 1444 1445 @return: L{None} 1446 """ 1447 self.alwaysCreate = val 1448 1449 def create(self) -> IO[bytes]: 1450 """ 1451 Exclusively create a file, only if this file previously did not exist. 1452 1453 @return: A file-like object opened from this path. 1454 """ 1455 fdint = os.open(self.path, _CREATE_FLAGS) 1456 1457 # XXX TODO: 'name' attribute of returned files is not mutable or 1458 # settable via fdopen, so this file is slightly less functional than the 1459 # one returned from 'open' by default. send a patch to Python... 1460 1461 return cast(IO[bytes], os.fdopen(fdint, "w+b")) 1462 1463 def temporarySibling(self, extension=b""): 1464 """ 1465 Construct a path referring to a sibling of this path. 1466 1467 The resulting path will be unpredictable, so that other subprocesses 1468 should neither accidentally attempt to refer to the same path before it 1469 is created, nor they should other processes be able to guess its name 1470 in advance. 1471 1472 @param extension: A suffix to append to the created filename. (Note 1473 that if you want an extension with a '.' you must include the '.' 1474 yourself.) 1475 @type extension: L{bytes} or L{unicode} 1476 1477 @return: a path object with the given extension suffix, C{alwaysCreate} 1478 set to True. 1479 @rtype: L{FilePath} with a mode equal to the type of C{extension} 1480 """ 1481 ourPath = self._getPathAsSameTypeAs(extension) 1482 sib = self.sibling( 1483 _secureEnoughString(ourPath) 1484 + self.clonePath(ourPath).basename() 1485 + extension 1486 ) 1487 sib.requireCreate() 1488 return sib 1489 1490 _chunkSize = 2 ** 2 ** 2 ** 2 1491 1492 def copyTo(self, destination, followLinks=True): 1493 """ 1494 Copies self to destination. 1495 1496 If self doesn't exist, an OSError is raised. 1497 1498 If self is a directory, this method copies its children (but not 1499 itself) recursively to destination - if destination does not exist as a 1500 directory, this method creates it. If destination is a file, an 1501 IOError will be raised. 1502 1503 If self is a file, this method copies it to destination. If 1504 destination is a file, this method overwrites it. If destination is a 1505 directory, an IOError will be raised. 1506 1507 If self is a link (and followLinks is False), self will be copied 1508 over as a new symlink with the same target as returned by os.readlink. 1509 That means that if it is absolute, both the old and new symlink will 1510 link to the same thing. If it's relative, then perhaps not (and 1511 it's also possible that this relative link will be broken). 1512 1513 File/directory permissions and ownership will NOT be copied over. 1514 1515 If followLinks is True, symlinks are followed so that they're treated 1516 as their targets. In other words, if self is a link, the link's target 1517 will be copied. If destination is a link, self will be copied to the 1518 destination's target (the actual destination will be destination's 1519 target). Symlinks under self (if self is a directory) will be 1520 followed and its target's children be copied recursively. 1521 1522 If followLinks is False, symlinks will be copied over as symlinks. 1523 1524 @param destination: the destination (a FilePath) to which self 1525 should be copied 1526 @param followLinks: whether symlinks in self should be treated as links 1527 or as their targets 1528 """ 1529 if self.islink() and not followLinks: 1530 os.symlink(os.readlink(self.path), destination.path) 1531 return 1532 # XXX TODO: *thorough* audit and documentation of the exact desired 1533 # semantics of this code. Right now the behavior of existent 1534 # destination symlinks is convenient, and quite possibly correct, but 1535 # its security properties need to be explained. 1536 if self.isdir(): 1537 if not destination.exists(): 1538 destination.createDirectory() 1539 for child in self.children(): 1540 destChild = destination.child(child.basename()) 1541 child.copyTo(destChild, followLinks) 1542 elif self.isfile(): 1543 with destination.open("w") as writefile, self.open() as readfile: 1544 while 1: 1545 # XXX TODO: optionally use os.open, os.read and 1546 # O_DIRECT and use os.fstatvfs to determine chunk sizes 1547 # and make *****sure**** copy is page-atomic; the 1548 # following is good enough for 99.9% of everybody and 1549 # won't take a week to audit though. 1550 chunk = readfile.read(self._chunkSize) 1551 writefile.write(chunk) 1552 if len(chunk) < self._chunkSize: 1553 break 1554 elif not self.exists(): 1555 raise OSError(errno.ENOENT, "No such file or directory") 1556 else: 1557 # If you see the following message because you want to copy 1558 # symlinks, fifos, block devices, character devices, or unix 1559 # sockets, please feel free to add support to do sensible things in 1560 # reaction to those types! 1561 raise NotImplementedError("Only copying of files and directories supported") 1562 1563 def moveTo(self, destination, followLinks=True): 1564 """ 1565 Move self to destination - basically renaming self to whatever 1566 destination is named. 1567 1568 If destination is an already-existing directory, 1569 moves all children to destination if destination is empty. If 1570 destination is a non-empty directory, or destination is a file, an 1571 OSError will be raised. 1572 1573 If moving between filesystems, self needs to be copied, and everything 1574 that applies to copyTo applies to moveTo. 1575 1576 @param destination: the destination (a FilePath) to which self 1577 should be copied 1578 @param followLinks: whether symlinks in self should be treated as links 1579 or as their targets (only applicable when moving between 1580 filesystems) 1581 """ 1582 try: 1583 os.rename(self._getPathAsSameTypeAs(destination.path), destination.path) 1584 except OSError as ose: 1585 if ose.errno == errno.EXDEV: 1586 # man 2 rename, ubuntu linux 5.10 "breezy": 1587 1588 # oldpath and newpath are not on the same mounted filesystem. 1589 # (Linux permits a filesystem to be mounted at multiple 1590 # points, but rename(2) does not work across different mount 1591 # points, even if the same filesystem is mounted on both.) 1592 1593 # that means it's time to copy trees of directories! 1594 secsib = destination.temporarySibling() 1595 self.copyTo(secsib, followLinks) # slow 1596 secsib.moveTo(destination, followLinks) # visible 1597 1598 # done creating new stuff. let's clean me up. 1599 mysecsib = self.temporarySibling() 1600 self.moveTo(mysecsib, followLinks) # visible 1601 mysecsib.remove() # slow 1602 else: 1603 raise 1604 else: 1605 self.changed() 1606 destination.changed() 1607 1608 1609FilePath.clonePath = FilePath # type: ignore[attr-defined] 1610