doc/.venv/Lib/site-packages/cerulean/copy_files.py

296 lines
11 KiB
Python
Raw Normal View History

import logging
from time import perf_counter
from typing import Callable, Generator, Iterable, Optional
from cerulean.path import Path, Permission
from cerulean.file_system import UnsupportedOperationError
logger = logging.getLogger(__name__)
CopyCallback = Callable[[int, int], None]
"""The type of a callback function for the copy() function.
A callback function takes two arguments, the number of bytes copied \
thus far, and the approximate total number of bytes to copy. To \
interrupt the copy operation, raise an exception.
"""
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(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 _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
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 _call_back(callback: Optional[CopyCallback], next_callback: float,
already_written: int, total_size: int, stream: Iterable[bytes]
) -> Generator[bytes, None, None]:
"""Calls the callback every second or so.
"""
written_here = 0
for chunk in stream:
yield chunk
written_here += len(chunk)
if perf_counter() >= next_callback:
if callback is not None:
callback(already_written + written_here, total_size)
next_callback = perf_counter() + 1.0
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 _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