fix: resolve path conflicts and improve self-healing during backup restore and plugin installation (#7737)

* fix(数据备份与恢复): 解决备份恢复和插件安装过程中的路径冲突及自愈问题

1. 修复备份导入时目录条目被误识别为 0 字节文件的问题。
2. 增加插件加载和数据目录创建时的路径冲突自动清理逻辑。
3. 增强插件解压安装过程对现有冲突文件的兼容性。
4. 优化 remove_dir 工具类使其支持同时处理文件和目录的删除。

* fix(core): 根据 CR 建议实现通用的路径冲突自愈机制并增强损坏符号链接的处理能力
This commit is contained in:
Helian Nuits
2026-05-05 01:05:43 +08:00
committed by GitHub
parent 9165278d21
commit 0830f48ae0
5 changed files with 39 additions and 8 deletions
+6
View File
@@ -25,6 +25,7 @@ from astrbot.core.utils.astrbot_path import (
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 @@ class AstrBotImporter:
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:
+2 -1
View File
@@ -37,6 +37,7 @@ from astrbot.core.platform.sources.aiocqhttp.aiocqhttp_platform_adapter import (
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 @@ class StarTools:
)
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
+2 -2
View File
@@ -4,7 +4,7 @@ import zipfile
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
@@ -71,7 +71,7 @@ class PluginUpdator(RepoZipUpdator):
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"Extracting archive: {zip_path}")
with zipfile.ZipFile(zip_path, "r") as z:
+26 -2
View File
@@ -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)
+3 -3
View File
@@ -9,7 +9,7 @@ import certifi
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 @@ class RepoZipUpdator:
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 @@ class RepoZipUpdator:
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]