Source code for dpdispatcher.utils.hdfs_cli

# /usr/bin/python

"""Wrap the Hadoop CLI operations used by the HDFS context."""

import os
import shlex
from collections.abc import Sequence

from dpdispatcher.utils.utils import run_cmd_with_all_output


def _command_text(command: Sequence[str]) -> str:
    """Format an argv command for diagnostics without executing a shell."""
    return " ".join(shlex.quote(argument) for argument in command)


def _run_hadoop(command: list[str]) -> tuple[int, bytes, bytes]:
    """Run one Hadoop command without allowing shell interpretation."""
    return run_cmd_with_all_output(command, shell=False)


def _validate_operand(value: str, name: str) -> str:
    """Reject values that Hadoop could parse as options or invalid argv data."""
    if not isinstance(value, str):
        raise TypeError(f"{name} must be a string")
    if not value:
        raise ValueError(f"{name} must not be empty")
    if "\x00" in value:
        raise ValueError(f"{name} must not contain NUL characters")
    if value.startswith("-"):
        raise ValueError(f"{name} must not begin with '-'")
    return value


[docs] class HDFSMissingPathError(RuntimeError): """Raised when Hadoop reports that a requested source path is absent."""
def _reports_missing_path(output: bytes, error: bytes) -> bool: """Recognize Hadoop's standard missing-path diagnostics.""" message = (output + b"\n" + error).decode("utf-8", errors="replace").lower() return any( marker in message for marker in ( "no such file or directory", "filenotfoundexception", "does not exist", ) )
[docs] class HDFS: """Fundamental class for HDFS basic manipulation."""
[docs] @staticmethod def exists(uri: str) -> bool: """Return whether an HDFS URI exists.""" uri = _validate_operand(uri, "uri") command = ["hadoop", "fs", "-test", "-e", uri] command_text = _command_text(command) try: ret, out, err = _run_hadoop(command) if ret == 0: return True if ret == 1: return False raise RuntimeError( f"Cannot check existence of hdfs uri[{uri}] " f"with cmd[{command_text}]; ret[{ret}] stdout[{out}] stderr[{err}]" ) except Exception as error: raise RuntimeError( f"Cannot check existence of hdfs uri[{uri}] with cmd[{command_text}]" ) from error
[docs] @staticmethod def remove(uri: str) -> bool: """Remove an HDFS URI recursively.""" uri = _validate_operand(uri, "uri") command = ["hadoop", "fs", "-rm", "-r", uri] command_text = _command_text(command) try: ret, out, err = _run_hadoop(command) if ret == 0: return True raise RuntimeError( f"Cannot remove hdfs uri[{uri}] " f"with cmd[{command_text}]; ret[{ret}] output[{out}] stderr[{err}]" ) except Exception as error: raise RuntimeError( f"Cannot remove hdfs uri[{uri}] with cmd[{command_text}]" ) from error
[docs] @staticmethod def mkdir(uri: str) -> bool: """Create an HDFS directory and any missing parents.""" uri = _validate_operand(uri, "uri") command = ["hadoop", "fs", "-mkdir", "-p", uri] command_text = _command_text(command) try: ret, out, err = _run_hadoop(command) if ret == 0: return True raise RuntimeError( f"Cannot mkdir of hdfs uri[{uri}] " f"with cmd[{command_text}]; ret[{ret}] output[{out}] stderr[{err}]" ) except Exception as error: raise RuntimeError( f"Cannot mkdir of hdfs uri[{uri}] with cmd[{command_text}]" ) from error
[docs] @staticmethod def copy_from_local(local_path: str, to_uri: str) -> tuple[bool, bytes]: """Copy a readable local path to an HDFS URI.""" local_path = _validate_operand(local_path, "local_path") to_uri = _validate_operand(to_uri, "to_uri") if not os.path.exists(local_path) or not os.access(local_path, os.R_OK): raise RuntimeError(f"try to access local_path[{local_path}] but failed") command = ["hadoop", "fs", "-copyFromLocal", "-f", local_path, to_uri] command_text = _command_text(command) try: ret, out, err = _run_hadoop(command) if ret == 0: return True, out raise RuntimeError( f"Cannot copy local[{local_path}] to remote[{to_uri}] with cmd[{command_text}]; " f"ret[{ret}] output[{out}] stderr[{err}]" ) except Exception as error: raise RuntimeError( f"Cannot copy local[{local_path}] to remote[{to_uri}] with cmd[{command_text}]" ) from error
[docs] @staticmethod def copy_to_local( from_uri: str | list[str] | tuple[str, ...], local_path: str ) -> bool: """Copy one or more HDFS paths into a local directory.""" if isinstance(from_uri, str): remote_arguments = [_validate_operand(from_uri, "from_uri")] elif isinstance(from_uri, (list, tuple)): if not from_uri: raise ValueError("from_uri must contain at least one HDFS URI") remote_arguments = [_validate_operand(uri, "from_uri") for uri in from_uri] else: raise TypeError("from_uri must be a string, list, or tuple of strings") local_path = _validate_operand(local_path, "local_path") command = [ "hadoop", "fs", "-copyToLocal", *remote_arguments, local_path, ] command_text = _command_text(command) try: ret, out, err = _run_hadoop(command) if ret == 0: return True error_type = ( HDFSMissingPathError if _reports_missing_path(out, err) else RuntimeError ) raise error_type( f"Cannot copy remote[{from_uri}] to local[{local_path}] with cmd[{command_text}]; " f"ret[{ret}] output[{out}] stderr[{err}]" ) except HDFSMissingPathError: raise except Exception as error: raise RuntimeError( f"Cannot copy remote[{from_uri}] to local[{local_path}] with cmd[{command_text}]" ) from error
[docs] @staticmethod def read_hdfs_file(uri: str) -> bytes: """Return the decoded output of ``hadoop fs -text`` for an HDFS URI.""" uri = _validate_operand(uri, "uri") command = ["hadoop", "fs", "-text", uri] command_text = _command_text(command) try: ret, out, err = _run_hadoop(command) if ret == 0: return out raise RuntimeError( f"Cannot read text from uri[{uri}]" f"cmd [{command_text}] ret[{ret}] output[{out}] stderr[{err}]" ) except Exception as error: raise RuntimeError( f"Cannot read text from uri[{uri}]cmd [{command_text}]" ) from error
[docs] @staticmethod def move(from_uri: str, to_uri: str) -> bool: """Move an HDFS path to a new URI.""" from_uri = _validate_operand(from_uri, "from_uri") to_uri = _validate_operand(to_uri, "to_uri") command = ["hadoop", "fs", "-mv", from_uri, to_uri] command_text = _command_text(command) try: ret, out, err = _run_hadoop(command) if ret == 0: return True raise RuntimeError( f"Cannot move from_uri[{from_uri}] to " f"to_uri[{to_uri}] with cmd[{command_text}]; " f"ret[{ret}] output[{out}] stderr[{err}]" ) except Exception as error: raise RuntimeError( f"Cannot move from_uri[{from_uri}] to to_uri[{to_uri}] with cmd[{command_text}]" ) from error