diff --git a/astrbot/core/backup/importer.py b/astrbot/core/backup/importer.py index e994242a88..7d30d27c39 100644 --- a/astrbot/core/backup/importer.py +++ b/astrbot/core/backup/importer.py @@ -25,6 +25,7 @@ get_astrbot_data_path, get_astrbot_knowledge_base_path, ) +from astrbot.core.utils.io import ensure_dir from astrbot.core.utils.version_comparator import VersionComparator # 从共享常量模块导入 @@ -931,6 +932,11 @@ async def _import_directories( if not _validate_path_within(target_path, target_dir): result.add_warning(f"文件路径越界,已跳过: {name}") continue + + if zf.getinfo(name).is_dir(): + ensure_dir(target_path) + continue + target_path.parent.mkdir(parents=True, exist_ok=True) with zf.open(name) as src, open(target_path, "wb") as dst: diff --git a/astrbot/core/star/star_tools.py b/astrbot/core/star/star_tools.py index 4d85131fc6..fe5563b7dd 100644 --- a/astrbot/core/star/star_tools.py +++ b/astrbot/core/star/star_tools.py @@ -37,6 +37,7 @@ from astrbot.core.star.context import Context from astrbot.core.star.star import star_map from astrbot.core.utils.astrbot_path import get_astrbot_data_path +from astrbot.core.utils.io import ensure_dir class StarTools: @@ -305,7 +306,7 @@ def get_data_dir(cls, plugin_name: str | None = None) -> Path: ) try: - data_dir.mkdir(parents=True, exist_ok=True) + ensure_dir(data_dir) except OSError as e: if isinstance(e, PermissionError): raise RuntimeError(f"无法创建目录 {data_dir}:权限不足") from e diff --git a/astrbot/core/star/updator.py b/astrbot/core/star/updator.py index 2ae0039db0..ee507454e2 100644 --- a/astrbot/core/star/updator.py +++ b/astrbot/core/star/updator.py @@ -4,7 +4,7 @@ from astrbot.core import logger from astrbot.core.utils.astrbot_path import get_astrbot_plugin_path -from astrbot.core.utils.io import on_error, remove_dir +from astrbot.core.utils.io import ensure_dir, on_error, remove_dir from ..star.star import StarMetadata from ..updator import RepoZipUpdator @@ -53,7 +53,7 @@ async def update(self, plugin: StarMetadata, proxy="") -> str: return plugin_path def unzip_file(self, zip_path: str, target_dir: str) -> None: - os.makedirs(target_dir, exist_ok=True) + ensure_dir(target_dir) update_dir = "" logger.info(f"正在解压压缩包: {zip_path}") with zipfile.ZipFile(zip_path, "r") as z: diff --git a/astrbot/core/utils/io.py b/astrbot/core/utils/io.py index b565926749..a34a03bfcd 100644 --- a/astrbot/core/utils/io.py +++ b/astrbot/core/utils/io.py @@ -31,12 +31,36 @@ def on_error(func, path, exc_info) -> None: def remove_dir(file_path: str) -> bool: - if not os.path.exists(file_path): + if not os.path.lexists(file_path): return True - shutil.rmtree(file_path, onerror=on_error) + if os.path.isfile(file_path) or os.path.islink(file_path): + os.remove(file_path) + else: + shutil.rmtree(file_path, onerror=on_error) return True +def ensure_dir(dir_path: str | Path) -> None: + """确保目录存在。如果路径处存在非目录的文件或损坏的符号链接,则先将其删除。""" + p = Path(dir_path) + if (p.exists() or p.is_symlink()) and not p.is_dir(): + logger.warning(f"路径 {p} 已存在但不是目录,正在清理以创建目录。") + try: + if p.is_dir(): + shutil.rmtree(p, onerror=on_error) + else: + p.unlink() + except Exception as e: + logger.error(f"清理冲突路径 {p} 失败: {e!s}") + raise RuntimeError(f"无法清理冲突路径 {p}:{e!s}") from e + + try: + p.mkdir(parents=True, exist_ok=True) + except Exception as e: + logger.error(f"创建目录 {p} 失败: {e!s}") + raise RuntimeError(f"无法创建目录 {p}:{e!s}") from e + + def port_checker(port: int, host: str = "localhost") -> bool: sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sk.settimeout(1) diff --git a/astrbot/core/zip_updator.py b/astrbot/core/zip_updator.py index 116b55c57d..1408a1c454 100644 --- a/astrbot/core/zip_updator.py +++ b/astrbot/core/zip_updator.py @@ -9,7 +9,7 @@ import httpx from astrbot.core import logger -from astrbot.core.utils.io import on_error +from astrbot.core.utils.io import ensure_dir, on_error from astrbot.core.utils.version_comparator import VersionComparator @@ -56,7 +56,7 @@ async def _download_file( self, url: str, path: str, timeout: float = 1800.0 ) -> None: target_path = Path(path) - target_path.parent.mkdir(parents=True, exist_ok=True) + ensure_dir(target_path.parent) try: async with self._create_httpx_client(timeout=timeout) as client: @@ -233,7 +233,7 @@ def parse_github_url(self, url: str): def unzip_file(self, zip_path: str, target_dir: str) -> None: """解压缩文件, 并将压缩包内**第一个**文件夹内的文件移动到 target_dir""" - os.makedirs(target_dir, exist_ok=True) + ensure_dir(target_dir) update_dir = "" with zipfile.ZipFile(zip_path, "r") as z: update_dir = z.namelist()[0]