mirror of
https://github.com/Stone-Red-Code/dvc.git
synced 2026-11-10 15:28:34 +01:00
190 lines
5.6 KiB
Python
190 lines
5.6 KiB
Python
import errno
|
|
import logging
|
|
from functools import partial, wraps
|
|
from typing import TYPE_CHECKING, Any, Callable, Iterable, Optional
|
|
|
|
from funcy import split
|
|
|
|
from dvc_objects._tqdm import Tqdm
|
|
from dvc_objects.executors import ThreadPoolExecutor
|
|
|
|
if TYPE_CHECKING:
|
|
from dvc_objects.db import ObjectDB
|
|
from dvc_objects.hash_info import HashInfo
|
|
|
|
from .db.index import ObjectDBIndexBase
|
|
from .status import CompareStatusResult
|
|
from .tree import Tree
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class TransferError(Exception):
|
|
def __init__(self, fails: int) -> None:
|
|
self.fails = fails
|
|
super().__init__(f"{fails} transfer failed")
|
|
|
|
|
|
def _log_exceptions(func):
|
|
@wraps(func)
|
|
def wrapper(fs_path, *args, **kwargs):
|
|
try:
|
|
func(fs_path, *args, **kwargs)
|
|
return 0
|
|
except Exception as exc: # pylint: disable=broad-except
|
|
# NOTE: this means we ran out of file descriptors and there is no
|
|
# reason to try to proceed, as we will hit this error anyways.
|
|
# pylint: disable=no-member
|
|
if isinstance(exc, OSError) and exc.errno == errno.EMFILE:
|
|
raise
|
|
|
|
logger.debug("failed to transfer '%s'", fs_path)
|
|
return 1
|
|
|
|
return wrapper
|
|
|
|
|
|
def find_tree_by_obj_id(
|
|
odbs: Iterable[Optional["ObjectDB"]], obj_id: "HashInfo"
|
|
) -> Optional["Tree"]:
|
|
from dvc_objects.errors import ObjectFormatError
|
|
|
|
from .tree import Tree
|
|
|
|
for odb in odbs:
|
|
if odb is not None:
|
|
try:
|
|
return Tree.load(odb, obj_id)
|
|
except (FileNotFoundError, ObjectFormatError):
|
|
pass
|
|
return None
|
|
|
|
|
|
def _do_transfer(
|
|
src: "ObjectDB",
|
|
dest: "ObjectDB",
|
|
obj_ids: Iterable["HashInfo"],
|
|
missing_ids: Iterable["HashInfo"],
|
|
processor: Callable,
|
|
src_index: Optional["ObjectDBIndexBase"] = None,
|
|
dest_index: Optional["ObjectDBIndexBase"] = None,
|
|
cache_odb: Optional["ObjectDB"] = None,
|
|
**kwargs: Any,
|
|
):
|
|
dir_ids, file_ids = split(lambda hash_info: hash_info.isdir, obj_ids)
|
|
total_fails = 0
|
|
succeeded_dir_objs = []
|
|
all_file_ids = set(file_ids)
|
|
|
|
for dir_hash in dir_ids:
|
|
dir_obj = find_tree_by_obj_id([cache_odb, src], dir_hash)
|
|
assert dir_obj
|
|
|
|
entry_ids = {oid for _, _, oid in dir_obj}
|
|
bound_file_ids = all_file_ids & entry_ids
|
|
all_file_ids -= entry_ids
|
|
|
|
dir_fails = sum(processor(bound_file_ids))
|
|
if dir_fails:
|
|
logger.debug(
|
|
"failed to upload full contents of '%s', "
|
|
"aborting .dir file upload",
|
|
dir_obj.name,
|
|
)
|
|
logger.debug(
|
|
"failed to upload '%s' to '%s'",
|
|
src.get(dir_obj.hash_info).fs_path,
|
|
dest.get(dir_obj.hash_info).fs_path,
|
|
)
|
|
total_fails += dir_fails + 1
|
|
elif entry_ids.intersection(missing_ids):
|
|
# if for some reason a file contained in this dir is
|
|
# missing both locally and in the remote, we want to
|
|
# push whatever file content we have, but should not
|
|
# push .dir file
|
|
logger.debug(
|
|
"directory '%s' contains missing files,"
|
|
"skipping .dir file upload",
|
|
dir_obj.name,
|
|
)
|
|
else:
|
|
is_dir_failed = sum(processor([dir_obj.hash_info]))
|
|
total_fails += is_dir_failed
|
|
if not is_dir_failed:
|
|
succeeded_dir_objs.append(dir_obj)
|
|
|
|
# insert the rest
|
|
total_fails += sum(processor(all_file_ids))
|
|
if total_fails:
|
|
if src_index:
|
|
src_index.clear()
|
|
raise TransferError(total_fails)
|
|
|
|
# index successfully pushed dirs
|
|
if dest_index:
|
|
for dir_obj in succeeded_dir_objs:
|
|
file_hashes = {oid.value for _, _, oid in dir_obj}
|
|
logger.debug(
|
|
"Indexing pushed dir '%s' with '%s' nested files",
|
|
dir_obj.hash_info,
|
|
len(file_hashes),
|
|
)
|
|
assert dir_obj.hash_info and dir_obj.hash_info.value
|
|
dest_index.update([dir_obj.hash_info.value], file_hashes)
|
|
|
|
|
|
def transfer(
|
|
src: "ObjectDB",
|
|
dest: "ObjectDB",
|
|
obj_ids: Iterable["HashInfo"],
|
|
jobs: Optional[int] = None,
|
|
verify: bool = False,
|
|
hardlink: bool = False,
|
|
validate_status: Callable[["CompareStatusResult"], None] = None,
|
|
**kwargs,
|
|
) -> int:
|
|
"""Transfer (copy) the specified objects from one ODB to another.
|
|
|
|
Returns the number of successfully transferred objects
|
|
"""
|
|
from .status import compare_status
|
|
|
|
logger.debug(
|
|
"Preparing to transfer data from '%s' to '%s'",
|
|
src.fs_path,
|
|
dest.fs_path,
|
|
)
|
|
if src == dest:
|
|
return 0
|
|
|
|
status = compare_status(
|
|
src, dest, obj_ids, check_deleted=False, jobs=jobs, **kwargs
|
|
)
|
|
|
|
if validate_status:
|
|
validate_status(status)
|
|
|
|
if not status.new:
|
|
return 0
|
|
|
|
def func(hash_info: "HashInfo") -> None:
|
|
obj = src.get(hash_info)
|
|
return dest.add(
|
|
obj.fs_path,
|
|
obj.fs,
|
|
obj.hash_info,
|
|
verify=verify,
|
|
hardlink=hardlink,
|
|
)
|
|
|
|
total = len(status.new)
|
|
jobs = jobs or dest.fs.jobs
|
|
with Tqdm(total=total, unit="file", desc="Transferring") as pbar:
|
|
with ThreadPoolExecutor(max_workers=jobs) as executor:
|
|
wrapped_func = pbar.wrap_fn(_log_exceptions(func))
|
|
processor = partial(executor.imap_unordered, wrapped_func)
|
|
_do_transfer(
|
|
src, dest, status.new, status.missing, processor, **kwargs
|
|
)
|
|
return total
|