fix:图片存储问题
This commit is contained in:
@@ -24,125 +24,148 @@ logger = get_logger("emoji")
|
||||
class BaseImageDataModel(BaseDatabaseDataModel[Images]):
|
||||
def __init__(self, full_path: str | Path, image_bytes: Optional[bytes] = None):
|
||||
if not full_path:
|
||||
# 创建时候即检测文件路径合法性
|
||||
raise ValueError("表情包路径不能为空")
|
||||
raise ValueError("图片路径不能为空")
|
||||
if Path(full_path).is_dir() or not Path(full_path).exists():
|
||||
raise FileNotFoundError(f"表情包路径无效: {full_path}")
|
||||
raise FileNotFoundError(f"图片路径无效: {full_path}")
|
||||
|
||||
resolved_path = Path(full_path).absolute().resolve()
|
||||
self.full_path: Path
|
||||
self.dir_path: Path
|
||||
self.file_name: str
|
||||
self._set_full_path(resolved_path)
|
||||
|
||||
self.file_hash: str = None # type: ignore
|
||||
|
||||
self.image_bytes: Optional[bytes] = image_bytes
|
||||
|
||||
self.image_format: str = "" # 图片格式
|
||||
self.image_format: str = ""
|
||||
|
||||
def _set_full_path(self, full_path: Path) -> None:
|
||||
"""同步更新文件路径相关的运行时元数据。"""
|
||||
"""同步刷新路径、目录和文件名等运行时元数据。"""
|
||||
resolved_path = full_path.absolute().resolve()
|
||||
self.full_path = resolved_path
|
||||
self.dir_path = resolved_path.parent.resolve()
|
||||
self.file_name = resolved_path.name
|
||||
|
||||
def _restore_image_format_from_path(self) -> None:
|
||||
"""根据文件扩展名恢复基础图片格式信息。"""
|
||||
"""根据文件扩展名恢复图片格式信息。"""
|
||||
self.image_format = self.full_path.suffix.removeprefix(".").lower()
|
||||
|
||||
def _build_non_conflicting_path(self, target_path: Path) -> Path:
|
||||
"""在目标路径被占用时,生成一个可用的新路径。"""
|
||||
candidate_path = target_path
|
||||
index = 1
|
||||
while candidate_path.exists():
|
||||
candidate_path = target_path.with_name(
|
||||
f"{target_path.stem}_{self.file_hash[:8]}_{index}{target_path.suffix}"
|
||||
)
|
||||
index += 1
|
||||
return candidate_path
|
||||
|
||||
def _rename_file_to_match_format(self) -> None:
|
||||
"""修正文件扩展名,并处理目标文件已存在的冲突。"""
|
||||
new_file_name = ".".join(self.file_name.split(".")[:-1] + [self.image_format])
|
||||
new_full_path = self.dir_path / new_file_name
|
||||
if new_full_path == self.full_path:
|
||||
return
|
||||
|
||||
if new_full_path.exists():
|
||||
existing_file_hash = hashlib.sha256(self.read_image_bytes(new_full_path)).hexdigest()
|
||||
if existing_file_hash == self.file_hash:
|
||||
logger.info(f"[初始化] {new_full_path.name} 已存在且内容一致,复用已有文件")
|
||||
self.full_path.unlink()
|
||||
self._set_full_path(new_full_path)
|
||||
return
|
||||
|
||||
conflict_free_path = self._build_non_conflicting_path(new_full_path)
|
||||
logger.warning(
|
||||
f"[初始化] {new_full_path.name} 已存在且内容不同,改为保存到 {conflict_free_path.name}"
|
||||
)
|
||||
self.full_path.rename(conflict_free_path)
|
||||
self._set_full_path(conflict_free_path)
|
||||
return
|
||||
|
||||
self.full_path.rename(new_full_path)
|
||||
self._set_full_path(new_full_path)
|
||||
|
||||
def read_image_bytes(self, path: Path) -> bytes:
|
||||
"""
|
||||
同步读取图片文件的字节内容
|
||||
同步读取图片文件的字节内容。
|
||||
|
||||
Args:
|
||||
path (Path): 图片文件的完整路径
|
||||
path: 图片文件的完整路径。
|
||||
|
||||
Returns:
|
||||
return (bytes): 图片文件的字节内容
|
||||
Raises:
|
||||
FileNotFoundError: 如果文件不存在则抛出该异常
|
||||
Exception: 其他读取文件时发生的异常
|
||||
图片文件的字节内容。
|
||||
"""
|
||||
try:
|
||||
with open(path, "rb") as f:
|
||||
return f.read()
|
||||
except FileNotFoundError as e:
|
||||
with open(path, "rb") as file:
|
||||
return file.read()
|
||||
except FileNotFoundError as exc:
|
||||
logger.error(f"[读取图片文件] 文件未找到: {path}")
|
||||
raise e
|
||||
except Exception as e:
|
||||
logger.error(f"[读取图片文件] 读取文件时发生错误: {e}")
|
||||
raise e
|
||||
raise exc
|
||||
except Exception as exc:
|
||||
logger.error(f"[读取图片文件] 读取文件时发生错误: {exc}")
|
||||
raise exc
|
||||
|
||||
def get_image_format(self, image_bytes: bytes) -> str:
|
||||
"""
|
||||
获取图片的格式
|
||||
获取图片的实际格式。
|
||||
|
||||
Args:
|
||||
image_bytes (bytes): 图片的字节内容
|
||||
image_bytes: 图片的字节内容。
|
||||
|
||||
Returns:
|
||||
return (str): 图片的格式(小写)
|
||||
|
||||
Raises:
|
||||
ValueError: 如果无法识别图片格式
|
||||
Exception: 其他读取图片格式时发生的异常
|
||||
小写格式名,例如 `png`、`jpeg`。
|
||||
"""
|
||||
try:
|
||||
with PILImage.open(io.BytesIO(image_bytes)) as img:
|
||||
if not img.format:
|
||||
raise ValueError("无法识别图片格式")
|
||||
return img.format.lower()
|
||||
except Exception as e:
|
||||
logger.error(f"[获取图片格式] 读取图片格式时发生错误: {e}")
|
||||
raise e
|
||||
except Exception as exc:
|
||||
logger.error(f"[获取图片格式] 读取图片格式时发生错误: {exc}")
|
||||
raise exc
|
||||
|
||||
async def calculate_hash_format(self) -> bool:
|
||||
"""
|
||||
异步计算表情包的哈希值和格式,初始化后应该执行此方法来确保对象的哈希值和格式正确
|
||||
计算图片哈希和实际格式,并在需要时修正扩展名。
|
||||
|
||||
Returns:
|
||||
return (bool): 如果成功计算哈希值和格式则返回True,否则返回False
|
||||
成功返回 `True`,失败返回 `False`。
|
||||
"""
|
||||
try:
|
||||
# 计算哈希值
|
||||
logger.debug(f"[初始化] 计算 {self.file_name} 的哈希值...")
|
||||
if not self.image_bytes:
|
||||
if self.image_bytes is None:
|
||||
logger.debug(f"[初始化] 正在读取文件: {self.full_path}")
|
||||
image_bytes = await asyncio.to_thread(self.read_image_bytes, self.full_path)
|
||||
else:
|
||||
image_bytes = self.image_bytes
|
||||
|
||||
self.image_bytes = image_bytes
|
||||
self.file_hash = hashlib.sha256(image_bytes).hexdigest()
|
||||
logger.debug(f"[初始化] {self.file_name} 计算哈希值成功: {self.file_hash}")
|
||||
|
||||
# 用PIL读取图片格式
|
||||
logger.debug(f"[初始化] 读取 {self.file_name} 的图片格式...")
|
||||
self.image_format = await asyncio.to_thread(self.get_image_format, image_bytes)
|
||||
logger.debug(f"[初始化] {self.file_name} 读取图片格式成功: {self.image_format}")
|
||||
|
||||
# 比对文件扩展名和实际格式
|
||||
file_ext = self.file_name.split(".")[-1].lower()
|
||||
if file_ext != self.image_format:
|
||||
logger.warning(
|
||||
f"[初始化] {self.file_name} 文件扩展名与实际格式不符: ext`{file_ext}`!=`{self.image_format}`"
|
||||
)
|
||||
# 重命名文件以匹配实际格式
|
||||
new_file_name = ".".join(self.file_name.split(".")[:-1] + [self.image_format])
|
||||
new_full_path = self.dir_path / new_file_name
|
||||
self.full_path.rename(new_full_path)
|
||||
self._set_full_path(new_full_path)
|
||||
self._rename_file_to_match_format()
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"[初始化] 初始化图片时发生错误: {e}")
|
||||
except Exception as exc:
|
||||
logger.error(f"[初始化] 初始化图片时发生错误: {exc}")
|
||||
logger.error(traceback.format_exc())
|
||||
return False
|
||||
|
||||
|
||||
class MaiEmoji(BaseImageDataModel):
|
||||
"""麦麦的表情包对象,仅当**图片文件存在**时才应该创建此对象,数据库记录如果标记为文件不存在`(no_file_flag = True)`则不应该调用 `from_db_instance` 方法来创建此对象"""
|
||||
"""表情包数据模型。"""
|
||||
|
||||
def __init__(self, full_path: str | Path, image_bytes: Optional[bytes] = None):
|
||||
# self.embedding = []
|
||||
self.description: str = ""
|
||||
self.emotion: List[str] = []
|
||||
self.query_count = 0
|
||||
@@ -152,33 +175,26 @@ class MaiEmoji(BaseImageDataModel):
|
||||
|
||||
@classmethod
|
||||
def from_db_instance(cls, db_record: Images):
|
||||
"""从数据库记录创建 MaiEmoji 对象,如果记录标记为文件不存在则**抛出异常**
|
||||
|
||||
调用者应该对数据库记录进行检查,如果 `no_file_flag` 为 True 则不应该调用此方法
|
||||
|
||||
Args:
|
||||
db_record (Images): 数据库中的图片记录
|
||||
Returns:
|
||||
return (MaiEmoji): 包含图片信息的 MaiEmoji 对象
|
||||
Raises:
|
||||
ValueError: 如果数据库记录标记为文件不存在则抛出该异常
|
||||
"""
|
||||
"""从数据库记录构建 `MaiEmoji` 对象。"""
|
||||
if db_record.no_file_flag:
|
||||
raise ValueError(f"数据库记录 {db_record.image_hash} 标记为文件不存在,无法创建 MaiEmoji 对象")
|
||||
|
||||
obj = cls(db_record.full_path)
|
||||
obj.file_hash = db_record.image_hash
|
||||
obj._restore_image_format_from_path()
|
||||
|
||||
description = db_record.description or ""
|
||||
obj.description = description
|
||||
normalized_tags = [
|
||||
str(item).strip()
|
||||
for item in str(description).replace(",", ",").replace("、", ",").replace(";", ",").split(",")
|
||||
for item in str(description).replace(",", ",").replace("。", ",").replace("、", ",").split(",")
|
||||
if str(item).strip()
|
||||
]
|
||||
deduped_tags: List[str] = []
|
||||
for item in normalized_tags:
|
||||
if item not in deduped_tags:
|
||||
deduped_tags.append(item)
|
||||
|
||||
obj.emotion = deduped_tags
|
||||
obj.query_count = db_record.query_count
|
||||
obj.last_used_time = db_record.last_used_time
|
||||
@@ -198,7 +214,7 @@ class MaiEmoji(BaseImageDataModel):
|
||||
|
||||
|
||||
class MaiImage(BaseImageDataModel):
|
||||
"""麦麦图片数据模型,仅当**图片文件存在**时才应该创建此对象,数据库记录如果标记为文件不存在`(no_file_flag = True)`则不应该调用 `from_db_instance` 方法来创建此对象"""
|
||||
"""普通图片数据模型。"""
|
||||
|
||||
def __init__(self, full_path: str | Path, image_bytes: Optional[bytes] = None):
|
||||
self.description: str = ""
|
||||
@@ -207,19 +223,10 @@ class MaiImage(BaseImageDataModel):
|
||||
|
||||
@classmethod
|
||||
def from_db_instance(cls, db_record: Images):
|
||||
"""从数据库记录创建 MaiImage 对象,如果记录标记为文件不存在则**抛出异常**
|
||||
|
||||
调用者应该对数据库记录进行检查,如果 `no_file_flag` 为 True 则不应该调用此方法
|
||||
|
||||
Args:
|
||||
db_record (Images): 数据库中的图片记录
|
||||
Returns:
|
||||
return (MaiImage): 包含图片信息的 MaiImage 对象
|
||||
Raises:
|
||||
ValueError: 如果数据库记录标记为文件不存在则抛出该异常
|
||||
"""
|
||||
"""从数据库记录构建 `MaiImage` 对象。"""
|
||||
if db_record.no_file_flag:
|
||||
raise ValueError(f"数据库记录 {db_record.image_hash} 标记为文件不存在,无法创建 MaiImage 对象")
|
||||
|
||||
obj = cls(db_record.full_path)
|
||||
obj.file_hash = db_record.image_hash
|
||||
obj._set_full_path(Path(db_record.full_path))
|
||||
|
||||
Reference in New Issue
Block a user