fix: guard ppt audit file integrity and disable preview on corrupted files

This commit is contained in:
OoO
2026-05-15 14:17:33 +08:00
parent 0b415d965b
commit 626a2ed50a
2 changed files with 57 additions and 2 deletions

View File

@@ -1847,6 +1847,7 @@ def ppt_audit_file(filename: str):
action = (request.args.get('action', 'view') or 'view').strip().lower()
try:
import os
import zipfile
from utils.security import safe_join
reports_dir = os.environ.get('REPORTS_DIR', '/app/data/reports')
@@ -1858,6 +1859,17 @@ def ppt_audit_file(filename: str):
if safe_path.suffix.lower() != '.pptx':
return '不支援的檔案格式', 400
if action == 'view':
try:
with zipfile.ZipFile(safe_path, 'r') as zf:
bad = zf.testzip()
if bad is not None:
return f'PPT 檔案損毀,無法預覽(損毀區段:{bad}', 409
except zipfile.BadZipFile:
return 'PPT 檔案損毀,無法預覽(非有效 zip', 409
except Exception as e:
return f'預覽檢查失敗:{type(e).__name__}', 409
return send_file(
str(safe_path),
mimetype='application/vnd.openxmlformats-officedocument.presentationml.presentation',
@@ -2171,6 +2183,7 @@ def budget_update(budget_id: int):
def ppt_audit_history():
"""掃 reports/ 目錄列指定月份 daily 報表 + 從 ppt_audit_results 讀審核歷史Phase 38"""
import os
import zipfile
reports_dir = os.environ.get('REPORTS_DIR', '/app/data/reports')
files = []
audit_records = []
@@ -2231,6 +2244,16 @@ def ppt_audit_history():
next_month_label = f"{next_year:04d}-{next_month:02d}"
show_next_month = (next_year < now.year) or (next_year == now.year and next_month <= now.month)
def _inspect_ppt_file(file_path: str):
try:
with zipfile.ZipFile(file_path, 'r') as zf:
bad = zf.testzip()
return (bad is None, None if bad is None else f'壓縮檔異常:{bad}')
except zipfile.BadZipFile:
return False, 'PPTX 檔案損毀(非有效 zip'
except Exception as e:
return False, f'檢查失敗:{str(e)[:60]}'
try:
if not os.path.isdir(reports_dir):
error = f'{reports_dir} 目錄不存在'
@@ -2248,6 +2271,7 @@ def ppt_audit_history():
try:
mtime = os.path.getmtime(full)
if month_start_ts <= mtime < month_end_ts:
is_valid, check_msg = _inspect_ppt_file(full)
files_by_name[f] = {
'source': 'filesystem',
'name': f,
@@ -2256,6 +2280,8 @@ def ppt_audit_history():
'mtime_ts': mtime,
'file_exists': True,
'file_path': full,
'is_valid_ppt': is_valid,
'file_error': check_msg,
}
except OSError:
continue
@@ -2294,6 +2320,10 @@ def ppt_audit_history():
continue
candidate_path = os.path.join(reports_dir, name)
exists = os.path.isfile(candidate_path)
is_valid = False
check_msg = '檔案未落盤'
if exists:
is_valid, check_msg = _inspect_ppt_file(candidate_path)
files_by_name[name] = {
'source': 'database',
'name': name,
@@ -2303,6 +2333,8 @@ def ppt_audit_history():
'file_exists': exists,
'file_path': candidate_path if exists else file_path,
'report_type': rpt_type,
'is_valid_ppt': is_valid,
'file_error': None if exists else check_msg,
}
finally:
session.close()

View File

@@ -286,19 +286,42 @@
<td><small>{{ f.mtime }}</small></td>
<td>
{% if f.source == 'database' %}
<span class="text-muted">{% if f.file_exists %}資料庫快取 + 檔案可存取{% else %}資料庫快取(檔案未落盤){% endif %}</span>
{% if f.file_exists %}
{% if f.is_valid_ppt %}
<span class="text-success">資料庫快取 + 檔案可預覽</span>
{% else %}
<span class="status-bad">資料庫快取 + 檔案損毀,建議重跑</span>
{% endif %}
{% else %}
<span class="text-muted">資料庫快取(檔案未落盤)</span>
{% endif %}
{% elif f.source == 'both' %}
{% if f.is_valid_ppt %}
<span class="text-success">檔案 + 資料庫</span>
{% else %}
<span class="status-bad">檔案 + 資料庫,檔案損毀</span>
{% endif %}
{% else %}
<span class="text-muted">22:00 掃描落盤</span>
{% if f.is_valid_ppt %}
<span class="text-success">22:00 掃描落盤,可預覽</span>
{% else %}
<span class="status-bad">22:00 掃描落盤,檔案損毀</span>
{% endif %}
{% endif %}
{% if f.file_error %}
<div><small class="text-muted">{{ f.file_error }}</small></div>
{% endif %}
</td>
<td>
<div class="ppt-file-actions">
{% if f.file_exists %}
{% if f.is_valid_ppt %}
<a class="btn btn-outline-primary btn-sm" href="{{ url_for('admin_observability.ppt_audit_file', filename=f.name) }}" target="_blank" rel="noopener">
<i class="fas fa-file-powerpoint me-1"></i>開啟
</a>
{% else %}
<span class="small status-bad">檔案不可預覽</span>
{% endif %}
<a class="btn btn-outline-secondary btn-sm" href="{{ url_for('admin_observability.ppt_audit_file', filename=f.name, action='download') }}">
<i class="fas fa-download me-1"></i>下載
</a>