mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-15 17:18:38 +00:00
139 lines
5.2 KiB
Python
139 lines
5.2 KiB
Python
"""Path and URL layout helpers for primary uploads and generated assets."""
|
|
|
|
import hashlib
|
|
import os
|
|
import stat
|
|
from pathlib import Path
|
|
from urllib.parse import quote
|
|
|
|
from deerflow.config.paths import VIRTUAL_PATH_PREFIX
|
|
|
|
UPLOAD_CONVERSIONS_DIRNAME = ".upload-conversions"
|
|
UPLOAD_LOCKS_DIRNAME = ".locks"
|
|
UPLOAD_STAGE_LOCKS_DIRNAME = "stages"
|
|
|
|
|
|
class UnsafeConversionPathError(ValueError):
|
|
"""Raised when the generated-conversion namespace is unsafe."""
|
|
|
|
|
|
def conversion_dir_for_uploads(uploads_dir: Path) -> Path:
|
|
"""Return the system-owned conversion directory beside ``uploads_dir``."""
|
|
return uploads_dir.parent / UPLOAD_CONVERSIONS_DIRNAME
|
|
|
|
|
|
def _truncate_utf8(value: str, max_bytes: int) -> str:
|
|
encoded = value.encode("utf-8")
|
|
if len(encoded) <= max_bytes:
|
|
return value
|
|
return encoded[:max_bytes].decode("utf-8", errors="ignore")
|
|
|
|
|
|
def conversion_filename_for_upload(filename: str) -> str:
|
|
"""Return a deterministic generated-Markdown component within 255 bytes."""
|
|
desired = f"{filename}.md"
|
|
if len(desired.encode("utf-8")) <= 255:
|
|
return desired
|
|
digest = hashlib.sha256(filename.encode("utf-8")).hexdigest()
|
|
marker = f".{digest}.md"
|
|
prefix = _truncate_utf8(filename, 255 - len(marker.encode("utf-8")))
|
|
return f"{prefix}{marker}"
|
|
|
|
|
|
def conversion_path_for_upload(upload_path: Path) -> Path:
|
|
"""Return the generated Markdown path owned by one primary upload."""
|
|
return conversion_dir_for_uploads(upload_path.parent) / conversion_filename_for_upload(upload_path.name)
|
|
|
|
|
|
def validate_conversion_dir(uploads_dir: Path) -> Path | None:
|
|
"""Return an existing real conversion directory without following links."""
|
|
conversion_dir = conversion_dir_for_uploads(uploads_dir)
|
|
try:
|
|
conversion_stat = os.lstat(conversion_dir)
|
|
except FileNotFoundError:
|
|
return None
|
|
if stat.S_ISLNK(conversion_stat.st_mode) or not stat.S_ISDIR(conversion_stat.st_mode):
|
|
raise UnsafeConversionPathError("Unsafe upload conversion directory")
|
|
return conversion_dir
|
|
|
|
|
|
def ensure_conversion_dir(uploads_dir: Path) -> Path:
|
|
"""Create and validate the system-owned conversion directory."""
|
|
conversion_dir = conversion_dir_for_uploads(uploads_dir)
|
|
try:
|
|
conversion_dir.mkdir(mode=0o755)
|
|
except FileExistsError:
|
|
pass
|
|
validated = validate_conversion_dir(uploads_dir)
|
|
if validated is None:
|
|
raise UnsafeConversionPathError("Upload conversion directory disappeared")
|
|
return validated
|
|
|
|
|
|
def upload_lock_dir_for_uploads(uploads_dir: Path) -> Path:
|
|
"""Return the stable per-upload lock directory."""
|
|
return conversion_dir_for_uploads(uploads_dir) / UPLOAD_LOCKS_DIRNAME
|
|
|
|
|
|
def ensure_upload_lock_dir(uploads_dir: Path) -> Path:
|
|
"""Create and validate the system-owned upload lock directory."""
|
|
conversion_dir = ensure_conversion_dir(uploads_dir)
|
|
lock_dir = conversion_dir / UPLOAD_LOCKS_DIRNAME
|
|
try:
|
|
lock_dir.mkdir(mode=0o700)
|
|
except FileExistsError:
|
|
pass
|
|
try:
|
|
lock_stat = os.lstat(lock_dir)
|
|
except FileNotFoundError as exc:
|
|
raise UnsafeConversionPathError("Upload lock directory disappeared") from exc
|
|
if stat.S_ISLNK(lock_stat.st_mode) or not stat.S_ISDIR(lock_stat.st_mode):
|
|
raise UnsafeConversionPathError("Unsafe upload lock directory")
|
|
return lock_dir
|
|
|
|
|
|
def ensure_upload_stage_lock_dir(uploads_dir: Path) -> Path:
|
|
"""Create and validate the liveness-lock directory for upload stages."""
|
|
lock_dir = ensure_upload_lock_dir(uploads_dir)
|
|
stage_lock_dir = lock_dir / UPLOAD_STAGE_LOCKS_DIRNAME
|
|
try:
|
|
stage_lock_dir.mkdir(mode=0o700)
|
|
except FileExistsError:
|
|
pass
|
|
try:
|
|
stage_lock_stat = os.lstat(stage_lock_dir)
|
|
except FileNotFoundError as exc:
|
|
raise UnsafeConversionPathError("Upload stage lock directory disappeared") from exc
|
|
if stat.S_ISLNK(stage_lock_stat.st_mode) or not stat.S_ISDIR(stage_lock_stat.st_mode):
|
|
raise UnsafeConversionPathError("Unsafe upload stage lock directory")
|
|
return stage_lock_dir
|
|
|
|
|
|
def existing_conversion_path_for_upload(upload_path: Path) -> Path | None:
|
|
"""Return an existing safe generated file owned by ``upload_path``."""
|
|
if validate_conversion_dir(upload_path.parent) is None:
|
|
return None
|
|
candidate = conversion_path_for_upload(upload_path)
|
|
try:
|
|
candidate_stat = os.lstat(candidate)
|
|
except FileNotFoundError:
|
|
return None
|
|
if not stat.S_ISREG(candidate_stat.st_mode) or candidate_stat.st_nlink != 1:
|
|
raise UnsafeConversionPathError("Unsafe upload conversion file")
|
|
return candidate
|
|
|
|
|
|
def upload_virtual_path(filename: str) -> str:
|
|
"""Build the sandbox virtual path for a primary upload."""
|
|
return f"{VIRTUAL_PATH_PREFIX}/uploads/{filename}"
|
|
|
|
|
|
def conversion_virtual_path(filename: str) -> str:
|
|
"""Build the sandbox virtual path for an upload's generated Markdown."""
|
|
return f"{VIRTUAL_PATH_PREFIX}/{UPLOAD_CONVERSIONS_DIRNAME}/{conversion_filename_for_upload(filename)}"
|
|
|
|
|
|
def artifact_url_for_virtual_path(thread_id: str, virtual_path: str) -> str:
|
|
"""Build an artifact URL while preserving path separators."""
|
|
return f"/api/threads/{thread_id}/artifacts{quote(virtual_path, safe='/')}"
|