def _readlink(self, path: AbstractPath, recursive: bool) -> Path: self.__ensure_sftp() lpath = cast(PurePosixPath, path) if recursive: # SFTP's normalize() raises if there's a link loop or a \ # non-existing target, which we don't want, so we use \ # our own algorithm. max_iter = 32 cur_path = lpath iter_count = 0 while self._is_symlink(cur_path) and iter_count < max_iter: target = PurePosixPath(self.__sftp.readlink(str(cur_path))) if not target.is_absolute(): target = cur_path.parent / target cur_path = target iter_count += 1 if iter_count == max_iter: raise RuntimeError('Too many symbolic links detected') target = PurePosixPath(self.__sftp.normalize(str(path))) else: target = PurePosixPath(self.__sftp.readlink(str(path))) if not target.is_absolute(): target = lpath.parent / target return Path(self, target)
def _copy(source_path: Path, target_path: Path, overwrite: str, copy_permissions: bool, context: Optional[Path], callback: Optional[CopyCallback], already_written: int, size: int ) -> int: """Copy a file or directory from one path to another. See the documentation of copy() for the required behaviour. The context path is guaranteed to be a prefix of source_path, on \ the same file system. Args: source_path: The path to the source file. target_path: A path to copy it to. overwrite: Selects behaviour when the target exists. copy_permissions: Whether to copy permissions along. context: Root of the tree we are copying, or None. callback: A callback function to call. already_written: Starting count of bytes written. size: Approximate total size of data to copy. Returns: The approximate total number of bytes written. """ logger.debug('Copying %s to %s', source_path, target_path) if source_path.is_symlink(): if _copy_symlink(source_path, target_path, overwrite, context): return already_written if source_path.is_file(): already_written = _copy_file(source_path, target_path, overwrite, copy_permissions, callback, already_written, size) elif source_path.is_dir(): already_written = _copy_dir(source_path, target_path, overwrite, copy_permissions, context, callback, already_written, size) elif source_path.exists() or source_path.is_symlink(): # We don't copy special entries or broken links logger.debug( 'Skipping special entry or broken link %s', source_path) else: raise FileNotFoundError(('Source path {} does not exist, cannot' ' copy').format(source_path)) return already_written
def _readlink(self, path: AbstractPath, recursive: bool) -> Path: lpath = cast(pathlib.Path, path) if recursive: # pathlib.Path.resolve() raises if the link is broken # we don't want that, so use our own implementation max_iter = 32 cur_path = lpath iter_count = 0 while cur_path.is_symlink() and iter_count < max_iter: target = pathlib.Path(os.readlink(str(cur_path))) if not target.is_absolute(): target = cur_path.parent / target cur_path = target iter_count += 1 if iter_count == max_iter: raise RuntimeError('Too many symbolic links detected') return Path(self, cur_path) else: return Path(self, pathlib.Path(os.readlink(str(lpath))))
def _get_approx_size(path: Path) -> int: count = 0 if not path.is_symlink() and path.is_file(): count += path.size() elif not path.is_symlink() and path.is_dir(): for subdir in path.iterdir(): count += _get_approx_size(subdir) return count
def root(self) -> Path: return Path(self, PurePosixPath('/'))
def __truediv__(self, segment: str) -> Path: return Path(self, PurePosixPath('/' + segment.strip('/')))
def __truediv__(self, segment: str) -> Path: # TODO: segment: Union[str, pathlib.Path]? absseg = '/' + segment.strip('/') path = pathlib.Path(absseg) return Path(self, path)
def root(self) -> Path: return Path(self, pathlib.Path('/'))
def _copy_dir(source_path: Path, target_path: Path, overwrite: str, copy_permissions: bool, context: Optional[Path], callback: Optional[CopyCallback], already_written: int, size: int ) -> int: """Copy a directory recursively.""" target_path_exists = target_path.exists() or target_path.is_symlink() if target_path_exists: if overwrite == 'always': if not target_path.is_dir(): target_path.unlink() elif overwrite == 'raise': raise FileExistsError('Target path exists, not overwriting') elif overwrite == 'never': return already_written if not target_path.exists(): logger.debug('Making new dir %s', target_path) target_path.mkdir() perms = dict() for permission in Permission: perms[permission] = target_path.has_permission(permission) try: target_path.chmod(0o700) except UnsupportedOperationError: pass for entry in source_path.iterdir(): logger.debug('Recursively copying entry %s', entry) already_written = _copy(entry, target_path / entry.name, overwrite, copy_permissions, context, callback, already_written, size) try: for permission in Permission: if copy_permissions: target_path.set_permission( permission, source_path.has_permission(permission)) else: target_path.set_permission( permission, perms[permission] and source_path.has_permission(permission)) except UnsupportedOperationError: pass return already_written
def copy(source_path: Path, target_path: Path, overwrite: str = 'never', copy_into: bool = True, copy_permissions: bool = False, callback: Optional[CopyCallback] = None) -> None: """Copy a file or directory from one path to another. Note that source_path and target_path may be paths on different \ file systems. The overwrite parameter decides what to do if a file is encountered \ on the target side that would be overwritten. If overwrite equals \ 'raise', then a FileExistsError is raised. If overwrite equals \ 'always', then the file is overwritten (removed and replaced \ if needed). If overwrite equals 'never', then the existing \ file is kept, and the source file not copied. If the target is a directory and copy_into is True (the default), \ the source will be copied into the \ target directory. If an entry with the same name already exists \ within the target directory, then the overwrite parameter decides \ what happens. If copy_into is False, the source will \ be copied on top of the directory, subject to the setting for \ overwrite. If copy_permissions is True, this function will make the target's \ permissions match those of the source, `including` SETUID, SETGID \ and sticky bits. If copy_permissions is False, the target's \ permissions are left at their default values (according to the \ umask, on Unix-like systems), less any permissions that the source \ file does not have. If callback is provided, it should be a function taking two \ arguments, the current count of bytes copied and the total number \ of bytes to be copied. It will be called once at the beginning of \ the copy operation (with count == 0), once at the end (with count \ == total), and in between about once per second, if the copy takes \ long enough. Note that the total number of bytes passed to the \ callback is approximate, and that the count may be larger than \ the total if the estimate was off. To abort the copy, raise an \ exception from the callback function. Args: source_path: The path to the source file. target_path: A path to copy it to. overwrite: Selects behaviour when the target exists. copy_into: Whether to copy into target directories. copy_permissions: Whether to copy permissions along. callback: A callback function to call regularly with progress \ reports. """ if overwrite not in ['always', 'never', 'raise']: raise ValueError('Invalid value for overwrite. Valid values are' + '"always", "never" and "raise".') if target_path.is_dir() and copy_into: target_path = target_path / source_path.name size = _get_approx_size(source_path) if callback is not None: callback(0, size) total_written = _copy(source_path, target_path, overwrite, copy_permissions, source_path, callback, 0, size) if callback is not None: callback(total_written, size)
def _copy_file(source_path: Path, target_path: Path, overwrite: str, copy_permissions: bool, callback: Optional[CopyCallback], already_written: int, size: int) -> int: """Copy a file. Returns the number of bytes written. """ target_path_exists = target_path.exists() or target_path.is_symlink() if not target_path_exists or overwrite == 'always': logger.debug('Copying file from %s to %s', source_path, target_path) if not target_path.is_symlink() and target_path.is_dir(): target_path.rmdir(recursive=True) elif target_path.exists(): target_path.unlink() target_path.touch() perms = dict() for permission in Permission: perms[permission] = target_path.has_permission(permission) try: target_path.chmod(0o600) except UnsupportedOperationError: pass target_path.streaming_write( _call_back( callback, perf_counter() + 1.0, already_written, size, source_path.streaming_read())) already_written += source_path.size() try: for permission in Permission: if copy_permissions: target_path.set_permission( permission, source_path.has_permission(permission)) else: target_path.set_permission( permission, perms[permission] and source_path.has_permission(permission)) except UnsupportedOperationError: pass elif overwrite == 'raise': raise FileExistsError('Target path exists, not overwriting') return already_written
def _copy_symlink(source_path: Path, target_path: Path, overwrite: str, context: Optional[Path]) -> bool: """Copy a symlink. Copies links to an existing file within the context as links and returns True, otherwise returns False. If overwrite is True, overwrites the target. """ target_path_exists = target_path.exists() or target_path.is_symlink() if not target_path_exists or overwrite == 'always': if context is not None: linked_path = source_path.readlink(recursive=False) if context in linked_path.parents: rel_path = linked_path.relative_to(context) logger.debug( 'Making relative link from %s to %s', target_path, rel_path) target_fs = target_path.filesystem if target_path.exists() or target_path.is_symlink(): if target_path.is_dir(): target_path.rmdir(recursive=True) else: target_path.unlink() target_path.symlink_to(target_fs / str(rel_path)) return True return False # fall through and copy as file or directory elif overwrite == 'raise': raise FileExistsError('Target path exists, not overwriting') return True # target path exists and overwrite is never, fail silently