Import the pre-repair source tree as the history baseline. Runtime data (data/), virtualenvs, bytecode caches and logs are gitignored so local secrets and user state stay out of the repo.
72 lines
2.6 KiB
Python
72 lines
2.6 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""
|
||
文本/代码文件读取工具 (file_reader)
|
||
编码探测 + 二进制探测 + 大小守卫,供附件系统(拖拽/粘贴/上传文本与代码文件)复用。
|
||
用法:read_text_file(path) -> (content, encoding, size_kb, lines),失败抛 ValueError。
|
||
说明:纯函数模块,不依赖 Qt,便于单元测试。
|
||
"""
|
||
import os
|
||
|
||
# 明确拒绝的二进制/文档格式(Word/Excel/PPT/PDF/压缩包/可执行/媒体等)
|
||
BINARY_EXTS = frozenset({
|
||
# Office 文档(明确排除 Word/Excel)
|
||
'.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
|
||
'.odt', '.ods', '.odp', '.rtf',
|
||
# 电子书(.pdf 已交由 tools/builtin_tools/pdf_reader.py 专门解析,不在此拦截)
|
||
'.epub', '.mobi',
|
||
# 压缩包 / 磁盘镜像
|
||
'.zip', '.rar', '.7z', '.tar', '.gz', '.bz2', '.xz', '.iso', '.dmg',
|
||
# 可执行 / 编译产物
|
||
'.exe', '.dll', '.so', '.dylib', '.msi', '.bin', '.apk', '.jar',
|
||
'.class', '.pyc', '.pyd', '.o', '.a',
|
||
# 音视频
|
||
'.mp3', '.wav', '.flac', '.m4a', '.aac', '.ogg',
|
||
'.mp4', '.avi', '.mov', '.mkv', '.webm', '.wmv',
|
||
# 字体
|
||
'.ttf', '.otf', '.woff', '.woff2', '.eot',
|
||
# 数据库
|
||
'.db', '.sqlite', '.sqlite3', '.mdb',
|
||
# 设计稿
|
||
'.psd', '.ai', '.sketch', '.fig',
|
||
})
|
||
|
||
# 单文件大小上限(字节):超过则拒绝,避免内存与上下文爆炸
|
||
MAX_ATTACH_FILE_BYTES = 2 * 1024 * 1024 # 2 MB
|
||
|
||
# 编码降级链:UTF-8(含BOM) → GB18030(⊇GBK/GB2312) → latin-1(永不失败)
|
||
_ENCODINGS = ('utf-8-sig', 'gb18030', 'latin-1')
|
||
|
||
|
||
def read_text_file(path, max_bytes=MAX_ATTACH_FILE_BYTES):
|
||
"""读取一个文本/代码文件。
|
||
|
||
返回 (content, encoding, size_kb, lines)。
|
||
文件过大或判定为二进制时抛出 ValueError(附带可读原因)。
|
||
"""
|
||
size = os.path.getsize(path)
|
||
if size > max_bytes:
|
||
raise ValueError(
|
||
f"文件过大({size / 1024 / 1024:.1f} MB > {max_bytes / 1024 / 1024:.0f} MB)"
|
||
)
|
||
|
||
with open(path, 'rb') as f:
|
||
raw = f.read()
|
||
|
||
# 二进制探测:前 8KB 含 NUL 字节即判定为二进制(git 同款启发式)
|
||
if b'\x00' in raw[:8192]:
|
||
raise ValueError("二进制文件,无法作为文本读取")
|
||
|
||
text, used_enc = None, 'utf-8'
|
||
for enc in _ENCODINGS:
|
||
try:
|
||
text = raw.decode(enc)
|
||
used_enc = enc
|
||
break
|
||
except UnicodeDecodeError:
|
||
continue
|
||
if text is None: # 理论上 latin-1 兜底永不失败
|
||
raise ValueError("无法识别文件编码")
|
||
|
||
lines = text.count('\n') + 1
|
||
return text, used_enc, round(size / 1024, 2), lines
|