B-42: 导入功能加固——逐工作表确认、流式上传与诊断证据
This commit is contained in:
+59
-13
@@ -10,6 +10,23 @@ class UnsupportedWorkbookError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
class CorruptWorkbookError(RuntimeError):
|
||||
"""The file cannot be opened as a valid Excel workbook.
|
||||
|
||||
Raised for corrupt/truncated files, wrong signatures and files that
|
||||
exceed the resource limits. The message is user-safe: it never contains
|
||||
server-side paths or the content-addressed storage filename.
|
||||
"""
|
||||
|
||||
|
||||
# Workbook resource guards (defense against decompression bombs and
|
||||
# accidentally giant exports). Bank statements are small; these bounds are
|
||||
# generous enough for real exports while keeping memory bounded.
|
||||
MAX_SHEETS = 50
|
||||
MAX_ROWS_PER_SHEET = 200_000
|
||||
MAX_COLS_PER_SHEET = 64
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RawSheet:
|
||||
name: str
|
||||
@@ -18,11 +35,18 @@ class RawSheet:
|
||||
|
||||
def read_workbook(path: Path) -> tuple[RawSheet, ...]:
|
||||
suffix = path.suffix.lower()
|
||||
if suffix == ".xlsx":
|
||||
return _read_xlsx(path)
|
||||
if suffix == ".xls":
|
||||
return _read_xls(path)
|
||||
raise UnsupportedWorkbookError(f"Unsupported workbook type: {suffix}")
|
||||
try:
|
||||
if suffix == ".xlsx":
|
||||
return _read_xlsx(path)
|
||||
if suffix == ".xls":
|
||||
return _read_xls(path)
|
||||
raise UnsupportedWorkbookError(f"Unsupported workbook type: {suffix}")
|
||||
except (UnsupportedWorkbookError, CorruptWorkbookError):
|
||||
raise
|
||||
except Exception as exc:
|
||||
raise CorruptWorkbookError(
|
||||
"文件无法读取,可能已损坏或不是有效的 Excel 文件。"
|
||||
) from exc
|
||||
|
||||
|
||||
def _read_xlsx(path: Path) -> tuple[RawSheet, ...]:
|
||||
@@ -35,13 +59,24 @@ def _read_xlsx(path: Path) -> tuple[RawSheet, ...]:
|
||||
|
||||
workbook = load_workbook(path, read_only=True, data_only=True)
|
||||
try:
|
||||
return tuple(
|
||||
RawSheet(
|
||||
name=worksheet.title,
|
||||
rows=tuple(tuple(row) for row in worksheet.iter_rows(values_only=True)),
|
||||
)
|
||||
for worksheet in workbook.worksheets
|
||||
)
|
||||
sheets: list[RawSheet] = []
|
||||
for worksheet in workbook.worksheets:
|
||||
rows: list[tuple[Any, ...]] = []
|
||||
for row_index, row in enumerate(worksheet.iter_rows(values_only=True)):
|
||||
if row_index >= MAX_ROWS_PER_SHEET:
|
||||
raise CorruptWorkbookError(
|
||||
f"工作表「{worksheet.title}」超过 {MAX_ROWS_PER_SHEET} 行,已拒绝读取。"
|
||||
)
|
||||
values = tuple(row)
|
||||
if len(values) > MAX_COLS_PER_SHEET:
|
||||
raise CorruptWorkbookError(
|
||||
f"工作表「{worksheet.title}」列数超过 {MAX_COLS_PER_SHEET},已拒绝读取。"
|
||||
)
|
||||
rows.append(values)
|
||||
sheets.append(RawSheet(name=worksheet.title, rows=tuple(rows)))
|
||||
if len(sheets) > MAX_SHEETS:
|
||||
raise CorruptWorkbookError(f"工作簿工作表数量超过 {MAX_SHEETS} 个,已拒绝读取。")
|
||||
return tuple(sheets)
|
||||
finally:
|
||||
workbook.close()
|
||||
|
||||
@@ -57,8 +92,19 @@ def _read_xls(path: Path) -> tuple[RawSheet, ...]:
|
||||
workbook = xlrd.open_workbook(path, on_demand=True)
|
||||
sheets: list[RawSheet] = []
|
||||
try:
|
||||
for sheet_name in workbook.sheet_names():
|
||||
names = workbook.sheet_names()
|
||||
if len(names) > MAX_SHEETS:
|
||||
raise CorruptWorkbookError(f"工作簿工作表数量超过 {MAX_SHEETS} 个,已拒绝读取。")
|
||||
for sheet_name in names:
|
||||
worksheet = workbook.sheet_by_name(sheet_name)
|
||||
if worksheet.nrows > MAX_ROWS_PER_SHEET:
|
||||
raise CorruptWorkbookError(
|
||||
f"工作表「{sheet_name}」超过 {MAX_ROWS_PER_SHEET} 行,已拒绝读取。"
|
||||
)
|
||||
if worksheet.ncols > MAX_COLS_PER_SHEET:
|
||||
raise CorruptWorkbookError(
|
||||
f"工作表「{sheet_name}」列数超过 {MAX_COLS_PER_SHEET},已拒绝读取。"
|
||||
)
|
||||
rows: list[tuple[Any, ...]] = []
|
||||
for row_index in range(worksheet.nrows):
|
||||
values: list[Any] = []
|
||||
|
||||
Reference in New Issue
Block a user