mirror of http://git.sairate.top/sairate/doc.git
296 lines
11 KiB
Python
296 lines
11 KiB
Python
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
|