mirror of
https://github.com/volcengine/OpenViking.git
synced 2026-10-01 01:38:07 +08:00
* Add search retrieval telemetry breakdown * Fix pyagfs helper annotation imports * docs: document search relation controls and telemetry fields Document the new include_relations request parameter and the search telemetry summary fields so the API docs stay aligned with the latest retrieval changes. * refactor(search): drop relation enrichment and trim telemetry Remove relation fetching from the retrieval path and delete low-value search telemetry fields so retrieval stays simpler and the telemetry summary focuses on actionable diagnostics.
384 lines
12 KiB
Python
384 lines
12 KiB
Python
"""Helper functions for common file operations in AGFS.
|
|
|
|
This module provides high-level helper functions for common operations:
|
|
- cp: Copy files/directories within AGFS
|
|
- upload: Upload files/directories from local filesystem to AGFS
|
|
- download: Download files/directories from AGFS to local filesystem
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Iterator
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from .async_client import ensure_same_encryption_account
|
|
from .protocols import AGFSByteStream, AGFSSyncClientProtocol
|
|
|
|
|
|
def _call_with_optional_ctx(
|
|
method: Any, *args: Any, ctx: dict[str, str] | None = None, **kwargs: Any
|
|
) -> Any:
|
|
"""Call a sync AGFS method with ctx when the backend supports it."""
|
|
if ctx is None:
|
|
return method(*args, **kwargs)
|
|
try:
|
|
return method(*args, ctx=ctx, **kwargs)
|
|
except TypeError as exc:
|
|
if "unexpected keyword argument 'ctx'" not in str(exc):
|
|
raise
|
|
return method(*args, **kwargs)
|
|
|
|
|
|
def _try_copy_within_mount_fast_path(
|
|
client: AGFSSyncClientProtocol,
|
|
src: str,
|
|
dst: str,
|
|
*,
|
|
fs_ctx: dict[str, str] | None = None,
|
|
) -> bool:
|
|
"""Attempt a same-mount verbatim copy and return whether the fast-path was used."""
|
|
copy_within_mount = getattr(client, "copy_within_mount", None)
|
|
if not callable(copy_within_mount):
|
|
return False
|
|
|
|
result = _call_with_optional_ctx(copy_within_mount, src, dst, ctx=fs_ctx)
|
|
if isinstance(result, dict):
|
|
return bool(result.get("performed", False))
|
|
return bool(result)
|
|
|
|
|
|
def cp(
|
|
client: AGFSSyncClientProtocol,
|
|
src: str,
|
|
dst: str,
|
|
recursive: bool = False,
|
|
stream: bool = False,
|
|
fs_ctx: dict[str, str] | None = None,
|
|
) -> None:
|
|
"""Copy a file or directory within AGFS.
|
|
|
|
Args:
|
|
client: AGFS binding client instance
|
|
src: Source path in AGFS
|
|
dst: Destination path in AGFS
|
|
recursive: If True, copy directories recursively
|
|
stream: If True, use streaming for large files (memory efficient)
|
|
|
|
Raises:
|
|
AGFSClientError: If source doesn't exist or operation fails
|
|
|
|
Examples:
|
|
>>> cp(client, "/file.txt", "/backup/file.txt") # Copy file
|
|
>>> cp(client, "/dir", "/backup/dir", recursive=True) # Copy directory
|
|
"""
|
|
ensure_same_encryption_account(src, dst)
|
|
|
|
# Check if source exists and get its type
|
|
src_info = _call_with_optional_ctx(client.stat, src, ctx=fs_ctx)
|
|
is_dir = src_info.get("isDir", False)
|
|
|
|
if is_dir:
|
|
if not recursive:
|
|
raise ValueError(f"Cannot copy directory '{src}' without recursive=True")
|
|
_copy_directory(client, src, dst, stream, fs_ctx=fs_ctx)
|
|
else:
|
|
_copy_file(client, src, dst, stream, fs_ctx=fs_ctx)
|
|
|
|
|
|
def upload(
|
|
client: AGFSSyncClientProtocol,
|
|
local_path: str,
|
|
remote_path: str,
|
|
recursive: bool = False,
|
|
stream: bool = False,
|
|
) -> None:
|
|
"""Upload a file or directory from local filesystem to AGFS.
|
|
|
|
Args:
|
|
client: AGFS binding client instance
|
|
local_path: Path to local file or directory
|
|
remote_path: Destination path in AGFS
|
|
recursive: If True, upload directories recursively
|
|
stream: If True, use streaming for large files (memory efficient)
|
|
|
|
Raises:
|
|
FileNotFoundError: If local path doesn't exist
|
|
AGFSClientError: If upload fails
|
|
|
|
Examples:
|
|
>>> upload(client, "/tmp/file.txt", "/remote/file.txt") # Upload file
|
|
>>> upload(client, "/tmp/data", "/remote/data", recursive=True) # Upload directory
|
|
"""
|
|
local = Path(local_path)
|
|
|
|
if not local.exists():
|
|
raise FileNotFoundError(f"Local path does not exist: {local_path}")
|
|
|
|
if local.is_dir():
|
|
if not recursive:
|
|
raise ValueError(f"Cannot upload directory '{local_path}' without recursive=True")
|
|
_upload_directory(client, local, remote_path, stream)
|
|
else:
|
|
_upload_file(client, local, remote_path, stream)
|
|
|
|
|
|
def download(
|
|
client: AGFSSyncClientProtocol,
|
|
remote_path: str,
|
|
local_path: str,
|
|
recursive: bool = False,
|
|
stream: bool = False,
|
|
) -> None:
|
|
"""Download a file or directory from AGFS to local filesystem.
|
|
|
|
Args:
|
|
client: AGFS binding client instance
|
|
remote_path: Path in AGFS
|
|
local_path: Destination path on local filesystem
|
|
recursive: If True, download directories recursively
|
|
stream: If True, use streaming for large files (memory efficient)
|
|
|
|
Raises:
|
|
AGFSClientError: If remote path doesn't exist or download fails
|
|
|
|
Examples:
|
|
>>> download(client, "/remote/file.txt", "/tmp/file.txt") # Download file
|
|
>>> download(client, "/remote/data", "/tmp/data", recursive=True) # Download directory
|
|
"""
|
|
# Check if remote path exists and get its type
|
|
remote_info = client.stat(remote_path)
|
|
is_dir = remote_info.get("isDir", False)
|
|
|
|
if is_dir:
|
|
if not recursive:
|
|
raise ValueError(f"Cannot download directory '{remote_path}' without recursive=True")
|
|
_download_directory(client, remote_path, Path(local_path), stream)
|
|
else:
|
|
_download_file(client, remote_path, Path(local_path), stream)
|
|
|
|
|
|
# Internal helper functions
|
|
|
|
|
|
def _copy_file(
|
|
client: AGFSSyncClientProtocol,
|
|
src: str,
|
|
dst: str,
|
|
stream: bool,
|
|
*,
|
|
fs_ctx: dict[str, str] | None = None,
|
|
) -> None:
|
|
"""Copy a single file within AGFS.
|
|
|
|
Copy is always expressed in logical AGFS operations so encryption remains
|
|
transparent to the business layer.
|
|
"""
|
|
# Ensure parent directory exists
|
|
_ensure_remote_parent_dir(client, dst, fs_ctx=fs_ctx)
|
|
|
|
if _try_copy_within_mount_fast_path(client, src, dst, fs_ctx=fs_ctx):
|
|
return
|
|
|
|
if stream:
|
|
# The binding client returns bytes or an iterator of bytes, not an
|
|
# HTTP response object with iter_content().
|
|
_call_with_optional_ctx(
|
|
client.write,
|
|
dst,
|
|
_iter_file_bytes(_call_with_optional_ctx(client.cat, src, stream=True, ctx=fs_ctx)),
|
|
ctx=fs_ctx,
|
|
)
|
|
else:
|
|
# Read entire file and write
|
|
data = _call_with_optional_ctx(client.cat, src, ctx=fs_ctx)
|
|
_call_with_optional_ctx(client.write, dst, data, ctx=fs_ctx)
|
|
|
|
|
|
def _copy_directory(
|
|
client: AGFSSyncClientProtocol,
|
|
src: str,
|
|
dst: str,
|
|
stream: bool,
|
|
*,
|
|
fs_ctx: dict[str, str] | None = None,
|
|
) -> None:
|
|
"""Recursively copy a directory within AGFS."""
|
|
# Create destination directory
|
|
try:
|
|
_call_with_optional_ctx(client.mkdir, dst, ctx=fs_ctx)
|
|
except Exception:
|
|
# Directory might already exist, continue
|
|
pass
|
|
|
|
# List source directory contents
|
|
items = _call_with_optional_ctx(client.ls, src, ctx=fs_ctx)
|
|
|
|
for item in items:
|
|
item_name = item["name"]
|
|
src_path = f"{src.rstrip('/')}/{item_name}"
|
|
dst_path = f"{dst.rstrip('/')}/{item_name}"
|
|
|
|
if item.get("isDir", False):
|
|
# Recursively copy subdirectory
|
|
_copy_directory(client, src_path, dst_path, stream, fs_ctx=fs_ctx)
|
|
else:
|
|
# Copy file
|
|
_copy_file(client, src_path, dst_path, stream, fs_ctx=fs_ctx)
|
|
|
|
|
|
def _upload_file(
|
|
client: AGFSSyncClientProtocol,
|
|
local_file: Path,
|
|
remote_path: str,
|
|
stream: bool,
|
|
) -> None:
|
|
"""Upload a single file to AGFS."""
|
|
# Ensure parent directory exists in AGFS
|
|
_ensure_remote_parent_dir(client, remote_path)
|
|
|
|
if stream:
|
|
client.write(remote_path, _iter_local_file_bytes(local_file))
|
|
else:
|
|
# Read entire file
|
|
with open(local_file, "rb") as f:
|
|
data = f.read()
|
|
client.write(remote_path, data)
|
|
|
|
|
|
def _upload_directory(
|
|
client: AGFSSyncClientProtocol,
|
|
local_dir: Path,
|
|
remote_path: str,
|
|
stream: bool,
|
|
) -> None:
|
|
"""Recursively upload a directory to AGFS."""
|
|
# Create remote directory
|
|
try:
|
|
client.mkdir(remote_path)
|
|
except Exception:
|
|
# Directory might already exist, continue
|
|
pass
|
|
|
|
# Walk through local directory
|
|
for item in local_dir.iterdir():
|
|
remote_item_path = f"{remote_path.rstrip('/')}/{item.name}"
|
|
|
|
if item.is_dir():
|
|
# Recursively upload subdirectory
|
|
_upload_directory(client, item, remote_item_path, stream)
|
|
else:
|
|
# Upload file
|
|
_upload_file(client, item, remote_item_path, stream)
|
|
|
|
|
|
def _download_file(
|
|
client: AGFSSyncClientProtocol,
|
|
remote_path: str,
|
|
local_file: Path,
|
|
stream: bool,
|
|
) -> None:
|
|
"""Download a single file from AGFS."""
|
|
# Ensure parent directory exists locally
|
|
local_file.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
if stream:
|
|
with open(local_file, "wb") as f:
|
|
for chunk in _iter_file_bytes(client.cat(remote_path, stream=True)):
|
|
f.write(chunk)
|
|
else:
|
|
# Read entire file
|
|
data = client.cat(remote_path)
|
|
with open(local_file, "wb") as f:
|
|
f.write(data)
|
|
|
|
|
|
def _download_directory(
|
|
client: AGFSSyncClientProtocol,
|
|
remote_path: str,
|
|
local_dir: Path,
|
|
stream: bool,
|
|
) -> None:
|
|
"""Recursively download a directory from AGFS."""
|
|
# Create local directory
|
|
local_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# List remote directory contents
|
|
items = client.ls(remote_path)
|
|
|
|
for item in items:
|
|
item_name = item["name"]
|
|
remote_item_path = f"{remote_path.rstrip('/')}/{item_name}"
|
|
local_item_path = local_dir / item_name
|
|
|
|
if item.get("isDir", False):
|
|
# Recursively download subdirectory
|
|
_download_directory(client, remote_item_path, local_item_path, stream)
|
|
else:
|
|
# Download file
|
|
_download_file(client, remote_item_path, local_item_path, stream)
|
|
|
|
|
|
def _ensure_remote_parent_dir(
|
|
client: AGFSSyncClientProtocol,
|
|
path: str,
|
|
*,
|
|
fs_ctx: dict[str, str] | None = None,
|
|
) -> None:
|
|
"""Ensure the parent directory exists for a remote path."""
|
|
parent = "/".join(path.rstrip("/").split("/")[:-1])
|
|
if parent and parent != "/":
|
|
# Try to create parent directory (and its parents)
|
|
_ensure_remote_dir_recursive(client, parent, fs_ctx=fs_ctx)
|
|
|
|
|
|
def _ensure_remote_dir_recursive(
|
|
client: AGFSSyncClientProtocol,
|
|
path: str,
|
|
*,
|
|
fs_ctx: dict[str, str] | None = None,
|
|
) -> None:
|
|
"""Recursively ensure a directory exists in AGFS."""
|
|
if not path or path == "/":
|
|
return
|
|
|
|
# Check if directory already exists
|
|
try:
|
|
info = _call_with_optional_ctx(client.stat, path, ctx=fs_ctx)
|
|
if info.get("isDir", False):
|
|
return # Directory exists
|
|
except Exception:
|
|
# Directory doesn't exist, need to create it
|
|
pass
|
|
|
|
# Ensure parent exists first
|
|
parent = "/".join(path.rstrip("/").split("/")[:-1])
|
|
if parent and parent != "/":
|
|
_ensure_remote_dir_recursive(client, parent, fs_ctx=fs_ctx)
|
|
|
|
# Create this directory
|
|
try:
|
|
_call_with_optional_ctx(client.mkdir, path, ctx=fs_ctx)
|
|
except Exception:
|
|
# Might already exist due to race condition, ignore
|
|
pass
|
|
|
|
|
|
def _iter_file_bytes(data: bytes | AGFSByteStream) -> AGFSByteStream:
|
|
"""Normalize AGFS read results to a byte iterator."""
|
|
if isinstance(data, bytes):
|
|
return iter((data,))
|
|
if isinstance(data, Iterator):
|
|
return data
|
|
return iter(data)
|
|
|
|
|
|
def _iter_local_file_bytes(local_file: Path, chunk_size: int = 8192) -> AGFSByteStream:
|
|
"""Yield local file content in chunks for streaming uploads."""
|
|
with open(local_file, "rb") as file_obj:
|
|
while True:
|
|
chunk = file_obj.read(chunk_size)
|
|
if not chunk:
|
|
break
|
|
yield chunk
|