diff --git a/app/application/image.py b/app/application/image.py index bc9292273..23d2e4477 100644 --- a/app/application/image.py +++ b/app/application/image.py @@ -2,6 +2,7 @@ import io import threading from collections.abc import Mapping from dataclasses import dataclass +from hashlib import sha256 from pathlib import Path from typing import Any, Callable, List, Optional, Protocol @@ -15,6 +16,8 @@ from app.runtime.log import logger WallpaperProvider = Callable[[], Optional[str]] WallpaperListProvider = Callable[[int], List[str]] +_IMAGE_CACHE_PATH_PREFIX = "proxy_" +_IMAGE_CACHE_HASH_LENGTH = 16 @dataclass(frozen=True, slots=True) @@ -373,9 +376,16 @@ class ImageHelper(metaclass=Singleton): @staticmethod def _prepare_cache_path(url: str) -> str: - """缓存路径""" - sanitized_path = SecurityUtils.sanitize_url_path(url) - cache_path = Path(sanitized_path) + """ + 根据图片 URL 生成缓存路径。 + + 根路径或其他无有效文件名的 URL 使用完整 URL 的短哈希兜底,避免 + `Path.with_suffix()` 对空路径抛出异常,并保证不同 URL 不共用该缓存名。 + """ + cache_path = Path(SecurityUtils.sanitize_url_path(url)) + if not cache_path.name: + hash_value = sha256(url.encode()).hexdigest()[:_IMAGE_CACHE_HASH_LENGTH] + cache_path = Path(f"{_IMAGE_CACHE_PATH_PREFIX}{hash_value}") if not cache_path.suffix: cache_path = cache_path.with_suffix(".jpg") return cache_path.as_posix() diff --git a/tests/test_system_image_proxy.py b/tests/test_system_image_proxy.py index 077a5aa25..d0ab85ba6 100644 --- a/tests/test_system_image_proxy.py +++ b/tests/test_system_image_proxy.py @@ -1,6 +1,7 @@ import asyncio import io from collections.abc import Iterator, Mapping +from hashlib import sha256 from types import SimpleNamespace from typing import Any, Optional from unittest.mock import AsyncMock, Mock, patch @@ -238,6 +239,21 @@ def test_async_fetch_image_with_mime_type_validates_network_content_once(): assert transport.async_calls[0][0] == "https://images.example/wallpaper.png" +@pytest.mark.parametrize( + "url", + [ + "https://example.com", + "https://example.com/", + "https://example.com/?param=value", + ], +) +def test_prepare_cache_path_uses_url_hash_for_urls_without_path(url: str) -> None: + """没有有效路径的图片 URL 应生成稳定合法的缓存文件名。""" + expected_hash = sha256(url.encode()).hexdigest()[:16] + + assert ImageHelper._prepare_cache_path(url) == f"proxy_{expected_hash}.jpg" + + def test_image_request_uses_internal_address_port_for_proxy_decision(monkeypatch): """自动代理策略必须通过注入端口判断内部地址。""" internal_address = _FakeInternalAddress(internal=True)